mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
Compare commits
8 Commits
ddf5a47c33
...
54d5f85538
| Author | SHA1 | Date | |
|---|---|---|---|
| 54d5f85538 | |||
| c3295b9b01 | |||
| 7fc48e5e93 | |||
| afff3d0e02 | |||
| 0660c40ec4 | |||
| 8520ef556b | |||
| 4e78618556 | |||
| 49455d1b36 |
@@ -17,7 +17,7 @@ idf_component_register(SRCS
|
||||
src/buzzer.cpp
|
||||
src/fs_helper.cpp
|
||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
|
||||
INCLUDE_DIRS "include" "Peanut-GB"
|
||||
INCLUDE_DIRS "include"
|
||||
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
|
||||
|
||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||
@@ -11,6 +11,30 @@
|
||||
|
||||
class AppSystem;
|
||||
|
||||
using AppTimerHandle = std::uint32_t;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = 0;
|
||||
|
||||
enum class AppEventType {
|
||||
Button,
|
||||
Timer,
|
||||
};
|
||||
|
||||
struct AppButtonEvent {
|
||||
InputState current{};
|
||||
InputState previous{};
|
||||
};
|
||||
|
||||
struct AppTimerEvent {
|
||||
AppTimerHandle handle = kInvalidAppTimer;
|
||||
};
|
||||
|
||||
struct AppEvent {
|
||||
AppEventType type;
|
||||
std::uint32_t timestamp_ms = 0;
|
||||
AppButtonEvent button{};
|
||||
AppTimerEvent timer{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
struct BasicAppContext {
|
||||
using Framebuffer = FramebufferT;
|
||||
@@ -40,28 +64,50 @@ struct BasicAppContext {
|
||||
|
||||
bool hasPendingAppSwitch() const { return pendingSwitch; }
|
||||
|
||||
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat = false) {
|
||||
if (!system)
|
||||
return kInvalidAppTimer;
|
||||
return scheduleTimerInternal(delay_ms, repeat);
|
||||
}
|
||||
|
||||
AppTimerHandle scheduleRepeatingTimer(uint32_t interval_ms) {
|
||||
if (!system)
|
||||
return kInvalidAppTimer;
|
||||
return scheduleTimerInternal(interval_ms, true);
|
||||
}
|
||||
|
||||
void cancelTimer(AppTimerHandle handle) {
|
||||
if (!system)
|
||||
return;
|
||||
cancelTimerInternal(handle);
|
||||
}
|
||||
|
||||
void cancelAllTimers() {
|
||||
if (!system)
|
||||
return;
|
||||
cancelAllTimersInternal();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class AppSystem;
|
||||
bool pendingSwitch = false;
|
||||
bool pendingSwitchByName = false;
|
||||
std::size_t pendingAppIndex = 0;
|
||||
std::string pendingAppName;
|
||||
|
||||
AppTimerHandle scheduleTimerInternal(uint32_t delay_ms, bool repeat);
|
||||
void cancelTimerInternal(AppTimerHandle handle);
|
||||
void cancelAllTimersInternal();
|
||||
};
|
||||
|
||||
using AppContext = BasicAppContext<PlatformFramebuffer, PlatformInput, PlatformClock>;
|
||||
|
||||
struct AppSleepPlan {
|
||||
uint32_t slow_ms = 0; // long sleep allowing battery/UI periodic refresh
|
||||
uint32_t normal_ms = 0; // short sleep for responsiveness on input wake
|
||||
};
|
||||
|
||||
class IApp {
|
||||
public:
|
||||
virtual ~IApp() = default;
|
||||
virtual void onStart() {}
|
||||
virtual void onStop() {}
|
||||
virtual void step() = 0;
|
||||
virtual AppSleepPlan sleepPlan(uint32_t now) const { return {}; }
|
||||
virtual void onStart() {}
|
||||
virtual void onStop() {}
|
||||
virtual void handleEvent(const AppEvent& event) = 0;
|
||||
};
|
||||
|
||||
class IAppFactory {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "app_framework.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -25,9 +26,54 @@ public:
|
||||
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
||||
|
||||
private:
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
friend struct BasicAppContext;
|
||||
|
||||
struct TimerRecord {
|
||||
AppTimerHandle id = kInvalidAppTimer;
|
||||
std::uint32_t generation = 0;
|
||||
std::uint32_t due_ms = 0;
|
||||
std::uint32_t interval_ms = 0;
|
||||
bool repeat = false;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat);
|
||||
void cancelTimer(AppTimerHandle handle);
|
||||
void cancelAllTimers();
|
||||
|
||||
void dispatchEvent(const AppEvent& event);
|
||||
|
||||
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
|
||||
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
|
||||
void clearTimersForCurrentApp();
|
||||
TimerRecord* findTimer(AppTimerHandle handle);
|
||||
bool handlePendingSwitchRequest();
|
||||
|
||||
AppContext context;
|
||||
std::vector<std::unique_ptr<IAppFactory>> factories;
|
||||
std::unique_ptr<IApp> current;
|
||||
IAppFactory* activeFactory = nullptr;
|
||||
std::size_t activeIndex = static_cast<std::size_t>(-1);
|
||||
std::vector<TimerRecord> timers;
|
||||
AppTimerHandle nextTimerId = 1;
|
||||
std::uint32_t currentGeneration = 0;
|
||||
InputState lastInputState{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(uint32_t delay_ms, bool repeat) {
|
||||
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelTimerInternal(AppTimerHandle handle) {
|
||||
if (system)
|
||||
system->cancelTimer(handle);
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
|
||||
if (system)
|
||||
system->cancelAllTimers();
|
||||
}
|
||||
|
||||
4042
Firmware/main/include/apps/peanut_gb.h
Normal file
4042
Firmware/main/include/apps/peanut_gb.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ public:
|
||||
void pooler(); // FIXME:
|
||||
uint8_t get_pressed();
|
||||
void install_isr();
|
||||
void register_listener(TaskHandle_t task);
|
||||
|
||||
TaskHandle_t _pooler_task;
|
||||
|
||||
@@ -32,6 +33,7 @@ private:
|
||||
Buttons();
|
||||
|
||||
volatile uint8_t _current;
|
||||
volatile TaskHandle_t _listener = nullptr;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -21,15 +21,15 @@ static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||
static constexpr size_t kLineMultiSingle = (kLineBytes + 2);
|
||||
static constexpr size_t kLineDataBytes = kLineMultiSingle * DISP_HEIGHT + 2;
|
||||
|
||||
extern uint8_t dma_buf[SMD::kLineDataBytes];
|
||||
extern uint8_t* dma_buf;
|
||||
|
||||
void init();
|
||||
// Simplified asynchronous frame pipeline:
|
||||
// Double-buffered asynchronous frame pipeline:
|
||||
// Usage pattern each frame:
|
||||
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer+clear & guarantees pixel area is zeroed
|
||||
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
|
||||
// ... write pixels into dma_buf via set_pixel / surface ...
|
||||
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; when DMA completes it triggers
|
||||
// // a background clear of pixel bytes for next frame
|
||||
// SMD::async_draw_start(); // (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
|
||||
void async_draw_start();
|
||||
void async_draw_wait();
|
||||
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight?
|
||||
@@ -50,41 +50,15 @@ static void set_pixel(int x, int y, bool value) {
|
||||
extern "C" void s_spi_post_cb(spi_transaction_t* trans);
|
||||
|
||||
static inline spi_device_interface_config_t _devcfg = {
|
||||
.mode = 0, // SPI mode 0
|
||||
.clock_speed_hz = 6 * 1000 * 1000, // Clock out at 10 MHz
|
||||
.spics_io_num = SPI_DISP_CS, // CS pin
|
||||
.mode = 0, // SPI mode 0
|
||||
.clock_speed_hz = 10 * 1000 * 1000, // Clock out at 10 MHz
|
||||
.spics_io_num = SPI_DISP_CS, // CS pin
|
||||
.flags = SPI_DEVICE_POSITIVE_CS,
|
||||
.queue_size = 3,
|
||||
.queue_size = 1,
|
||||
.pre_cb = nullptr,
|
||||
.post_cb = s_spi_post_cb,
|
||||
};
|
||||
extern spi_device_handle_t _spi;
|
||||
void ensure_clear_task(); // idempotent; called from init
|
||||
}; // namespace SMD
|
||||
|
||||
class SMDSurface : public Surface<SMDSurface, BwPixel>, public StandardEventQueue<SMDSurface> {
|
||||
public:
|
||||
using PixelType = BwPixel;
|
||||
|
||||
SMDSurface(EventLoop* loop);
|
||||
|
||||
~SMDSurface() override;
|
||||
|
||||
void draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel);
|
||||
|
||||
void clear_impl();
|
||||
|
||||
int get_width_impl() const;
|
||||
|
||||
int get_height_impl() const;
|
||||
|
||||
template<typename T>
|
||||
EventHandlingResult handle(const T& event) {
|
||||
return _window->handle(event);
|
||||
}
|
||||
|
||||
EventHandlingResult handle(SurfaceResizeEvent event);
|
||||
};
|
||||
|
||||
|
||||
#endif // DISPLAY_HPP
|
||||
|
||||
@@ -22,7 +22,7 @@ private:
|
||||
.scl_io_num = I2C_SCL,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.flags = {.enable_internal_pullup = false, .allow_pd = true},
|
||||
.flags = {.enable_internal_pullup = true, .allow_pd = true},
|
||||
};
|
||||
i2c_master_bus_handle_t _bus_handle;
|
||||
}; // namespace i2c_global
|
||||
|
||||
@@ -20,7 +20,7 @@ private:
|
||||
.sclk_io_num = SPI_SCK,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = 400 * 240 * 2};
|
||||
.max_transfer_sz = 12482U};
|
||||
|
||||
}; // namespace spi_global
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <shutdowner.hpp>
|
||||
#include <spi_global.hpp>
|
||||
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
@@ -29,6 +30,160 @@
|
||||
#include "esp_sleep.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
||||
namespace {
|
||||
|
||||
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
|
||||
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
|
||||
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
|
||||
constexpr uint32_t kStatsTaskStack = 4096;
|
||||
constexpr char kStatsTaskName[] = "TaskStats";
|
||||
|
||||
struct TaskRuntimeSample {
|
||||
TaskHandle_t handle;
|
||||
uint32_t runtime;
|
||||
};
|
||||
|
||||
struct TaskUsageRow {
|
||||
std::string name;
|
||||
uint64_t delta;
|
||||
UBaseType_t priority;
|
||||
uint32_t stackHighWaterBytes;
|
||||
bool isIdle;
|
||||
};
|
||||
|
||||
[[nodiscard]] uint64_t deltaWithWrap(uint32_t current, uint32_t previous) {
|
||||
if (current >= previous)
|
||||
return static_cast<uint64_t>(current - previous);
|
||||
return static_cast<uint64_t>(current) + (static_cast<uint64_t>(UINT32_MAX) - previous) + 1ULL;
|
||||
}
|
||||
|
||||
void task_usage_monitor(void*) {
|
||||
static constexpr char tag[] = "TaskUsage";
|
||||
std::vector<TaskRuntimeSample> lastSamples;
|
||||
uint32_t lastTotal = 0;
|
||||
|
||||
vTaskDelay(kStatsWarmupDelay);
|
||||
|
||||
while (true) {
|
||||
vTaskDelay(kStatsTaskDelayTicks);
|
||||
|
||||
const UBaseType_t taskCount = uxTaskGetNumberOfTasks();
|
||||
if (taskCount == 0)
|
||||
continue;
|
||||
|
||||
std::vector<TaskStatus_t> statusBuffer(taskCount);
|
||||
uint32_t totalRuntime = 0;
|
||||
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
|
||||
if (captured == 0)
|
||||
continue;
|
||||
statusBuffer.resize(captured);
|
||||
|
||||
std::vector<TaskRuntimeSample> currentSamples;
|
||||
currentSamples.reserve(statusBuffer.size());
|
||||
|
||||
if (lastTotal == 0) {
|
||||
for (const auto& status: statusBuffer) {
|
||||
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
|
||||
}
|
||||
lastSamples = std::move(currentSamples);
|
||||
lastTotal = totalRuntime;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t totalDelta = deltaWithWrap(totalRuntime, lastTotal);
|
||||
if (totalDelta == 0)
|
||||
continue;
|
||||
|
||||
std::vector<TaskUsageRow> rows;
|
||||
rows.reserve(statusBuffer.size());
|
||||
|
||||
uint64_t idleDelta = 0;
|
||||
uint64_t activeDelta = 0;
|
||||
uint64_t accountedDelta = 0;
|
||||
|
||||
for (const auto& status: statusBuffer) {
|
||||
const auto it = std::find_if(lastSamples.begin(), lastSamples.end(), [&](const TaskRuntimeSample& entry) {
|
||||
return entry.handle == status.xHandle;
|
||||
});
|
||||
|
||||
const uint32_t previousRuntime = (it != lastSamples.end()) ? it->runtime : status.ulRunTimeCounter;
|
||||
const uint64_t taskDelta = (it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
|
||||
|
||||
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
|
||||
|
||||
TaskUsageRow row{
|
||||
.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
|
||||
.delta = taskDelta,
|
||||
.priority = status.uxCurrentPriority,
|
||||
.stackHighWaterBytes = static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
|
||||
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
|
||||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)
|
||||
};
|
||||
|
||||
rows.push_back(std::move(row));
|
||||
|
||||
accountedDelta += taskDelta;
|
||||
if (rows.back().isIdle)
|
||||
idleDelta += taskDelta;
|
||||
else
|
||||
activeDelta += taskDelta;
|
||||
}
|
||||
|
||||
lastSamples = std::move(currentSamples);
|
||||
lastTotal = totalRuntime;
|
||||
|
||||
if (rows.empty())
|
||||
continue;
|
||||
|
||||
std::sort(rows.begin(), rows.end(), [](const TaskUsageRow& a, const TaskUsageRow& b) {
|
||||
return a.delta > b.delta;
|
||||
});
|
||||
|
||||
const double windowMs = static_cast<double>(totalDelta) / 1000.0;
|
||||
|
||||
std::printf("\n[%s] CPU usage over %.1f ms window\n", tag, windowMs);
|
||||
|
||||
for (const auto& row: rows) {
|
||||
if (row.delta == 0 || row.isIdle)
|
||||
continue;
|
||||
const double pct = (static_cast<double>(row.delta) * 100.0) / static_cast<double>(totalDelta);
|
||||
std::printf(" %-16s %6.2f%% (prio=%u stack_free=%lu B)\n", row.name.c_str(), pct, row.priority,
|
||||
static_cast<unsigned long>(row.stackHighWaterBytes));
|
||||
}
|
||||
|
||||
const double idlePct = (idleDelta * 100.0) / static_cast<double>(totalDelta);
|
||||
std::printf(" %-16s %6.2f%% (aggregated idle)\n", "<idle>", idlePct);
|
||||
|
||||
const uint64_t residual = (accountedDelta >= totalDelta) ? 0ULL : (totalDelta - accountedDelta);
|
||||
if (residual > 0) {
|
||||
const double residualPct = (static_cast<double>(residual) * 100.0) / static_cast<double>(totalDelta);
|
||||
std::printf(" %-16s %6.2f%% (ISRs / scheduler)\n", "<isr>", residualPct);
|
||||
}
|
||||
|
||||
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag,
|
||||
(activeDelta * 100.0) / static_cast<double>(totalDelta), idlePct);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
void start_task_usage_monitor() {
|
||||
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr, 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#else
|
||||
inline void start_task_usage_monitor() {}
|
||||
#endif
|
||||
|
||||
extern "C" void app_main() {
|
||||
#ifdef CONFIG_PM_ENABLE
|
||||
// const esp_pm_config_t pm_config = {
|
||||
@@ -71,5 +226,7 @@ extern "C" void app_main() {
|
||||
system.registerApp(apps::createTetrisAppFactory());
|
||||
system.registerApp(apps::createGameboyAppFactory());
|
||||
|
||||
start_task_usage_monitor();
|
||||
|
||||
system.run();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
#include "app_system.hpp"
|
||||
|
||||
#include <power_helper.hpp>
|
||||
#include <buttons.hpp>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
|
||||
context.system = this;
|
||||
namespace {
|
||||
[[nodiscard]] bool inputsDiffer(const InputState& a, const InputState& b) {
|
||||
return a.up != b.up || a.down != b.down || a.left != b.left || a.right != b.right || a.a != b.a || a.b != b.b ||
|
||||
a.select != b.select || a.start != b.start;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
|
||||
|
||||
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
||||
if (!factory)
|
||||
@@ -28,8 +38,8 @@ bool AppSystem::startAppByIndex(std::size_t index) {
|
||||
return false;
|
||||
|
||||
context.system = this;
|
||||
auto& factory = factories[index];
|
||||
auto app = factory->create(context);
|
||||
auto& factory = factories[index];
|
||||
auto app = factory->create(context);
|
||||
if (!app)
|
||||
return false;
|
||||
|
||||
@@ -43,7 +53,9 @@ bool AppSystem::startAppByIndex(std::size_t index) {
|
||||
context.pendingSwitch = false;
|
||||
context.pendingSwitchByName = false;
|
||||
context.pendingAppName.clear();
|
||||
current = std::move(app);
|
||||
clearTimersForCurrentApp();
|
||||
current = std::move(app);
|
||||
lastInputState = context.input.readState();
|
||||
current->onStart();
|
||||
return true;
|
||||
}
|
||||
@@ -54,29 +66,46 @@ void AppSystem::run() {
|
||||
return;
|
||||
}
|
||||
|
||||
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
|
||||
std::vector<AppEvent> events;
|
||||
events.reserve(4);
|
||||
|
||||
while (true) {
|
||||
current->step();
|
||||
if (context.pendingSwitch) {
|
||||
const bool byName = context.pendingSwitchByName;
|
||||
const std::size_t reqIndex = context.pendingAppIndex;
|
||||
const std::string reqName = context.pendingAppName;
|
||||
context.pendingSwitch = false;
|
||||
context.pendingSwitchByName = false;
|
||||
context.pendingAppName.clear();
|
||||
bool switched = false;
|
||||
if (byName) {
|
||||
switched = startApp(reqName);
|
||||
} else {
|
||||
switched = startAppByIndex(reqIndex);
|
||||
events.clear();
|
||||
const std::uint32_t now = context.clock.millis();
|
||||
processDueTimers(now, events);
|
||||
|
||||
const InputState inputNow = context.input.readState();
|
||||
if (inputsDiffer(inputNow, lastInputState)) {
|
||||
AppEvent evt{};
|
||||
evt.type = AppEventType::Button;
|
||||
evt.timestamp_ms = now;
|
||||
evt.button.current = inputNow;
|
||||
evt.button.previous = lastInputState;
|
||||
events.push_back(evt);
|
||||
lastInputState = inputNow;
|
||||
}
|
||||
|
||||
for (const auto& evt: events) {
|
||||
dispatchEvent(evt);
|
||||
if (handlePendingSwitchRequest()) {
|
||||
break;
|
||||
}
|
||||
if (switched)
|
||||
continue;
|
||||
}
|
||||
const auto now = context.clock.millis();
|
||||
auto plan = current->sleepPlan(now);
|
||||
if (plan.slow_ms || plan.normal_ms) {
|
||||
PowerHelper::get().delay(static_cast<int>(plan.slow_ms), static_cast<int>(plan.normal_ms));
|
||||
|
||||
|
||||
const std::uint32_t waitBase = context.clock.millis();
|
||||
const std::uint32_t waitMs = nextTimerDueMs(waitBase);
|
||||
TickType_t waitTicks;
|
||||
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
|
||||
waitTicks = portMAX_DELAY;
|
||||
} else {
|
||||
waitTicks = pdMS_TO_TICKS(waitMs);
|
||||
if (waitTicks == 0)
|
||||
waitTicks = 1;
|
||||
}
|
||||
|
||||
ulTaskNotifyTake(pdTRUE, waitTicks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,3 +124,111 @@ std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
|
||||
}
|
||||
return static_cast<std::size_t>(-1);
|
||||
}
|
||||
|
||||
AppTimerHandle AppSystem::scheduleTimer(uint32_t delay_ms, bool repeat) {
|
||||
if (!current)
|
||||
return kInvalidAppTimer;
|
||||
TimerRecord record;
|
||||
record.id = nextTimerId++;
|
||||
if (record.id == kInvalidAppTimer)
|
||||
record.id = nextTimerId++;
|
||||
record.generation = currentGeneration;
|
||||
const auto now = context.clock.millis();
|
||||
record.due_ms = now + delay_ms;
|
||||
record.interval_ms = repeat ? std::max<std::uint32_t>(1, delay_ms) : 0;
|
||||
record.repeat = repeat;
|
||||
record.active = true;
|
||||
timers.push_back(record);
|
||||
return record.id;
|
||||
}
|
||||
|
||||
void AppSystem::cancelTimer(AppTimerHandle handle) {
|
||||
auto* timer = findTimer(handle);
|
||||
if (timer)
|
||||
timer->active = false;
|
||||
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
|
||||
timers.end());
|
||||
}
|
||||
|
||||
void AppSystem::cancelAllTimers() {
|
||||
for (auto& timer: timers) {
|
||||
if (timer.generation == currentGeneration)
|
||||
timer.active = false;
|
||||
}
|
||||
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
|
||||
timers.end());
|
||||
}
|
||||
|
||||
void AppSystem::dispatchEvent(const AppEvent& event) {
|
||||
if (current)
|
||||
current->handleEvent(event);
|
||||
}
|
||||
|
||||
void AppSystem::processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents) {
|
||||
for (auto& timer: timers) {
|
||||
if (!timer.active || timer.generation != currentGeneration)
|
||||
continue;
|
||||
if (static_cast<std::int32_t>(now - timer.due_ms) >= 0) {
|
||||
AppEvent ev{};
|
||||
ev.type = AppEventType::Timer;
|
||||
ev.timestamp_ms = now;
|
||||
ev.timer.handle = timer.id;
|
||||
outEvents.push_back(ev);
|
||||
if (timer.repeat) {
|
||||
const std::uint32_t interval = timer.interval_ms ? timer.interval_ms : 1;
|
||||
timer.due_ms = now + interval;
|
||||
} else {
|
||||
timer.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
|
||||
timers.end());
|
||||
}
|
||||
|
||||
std::uint32_t AppSystem::nextTimerDueMs(std::uint32_t now) const {
|
||||
std::uint32_t minWait = std::numeric_limits<std::uint32_t>::max();
|
||||
for (const auto& timer: timers) {
|
||||
if (!timer.active || timer.generation != currentGeneration)
|
||||
continue;
|
||||
if (static_cast<std::int32_t>(now - timer.due_ms) >= 0)
|
||||
return 0;
|
||||
const std::uint32_t delta = timer.due_ms - now;
|
||||
if (delta < minWait)
|
||||
minWait = delta;
|
||||
}
|
||||
return minWait;
|
||||
}
|
||||
|
||||
void AppSystem::clearTimersForCurrentApp() {
|
||||
++currentGeneration;
|
||||
timers.clear();
|
||||
}
|
||||
|
||||
AppSystem::TimerRecord* AppSystem::findTimer(AppTimerHandle handle) {
|
||||
for (auto& timer: timers) {
|
||||
if (!timer.active || timer.generation != currentGeneration)
|
||||
continue;
|
||||
if (timer.id == handle)
|
||||
return &timer;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool AppSystem::handlePendingSwitchRequest() {
|
||||
if (!context.pendingSwitch)
|
||||
return false;
|
||||
const bool byName = context.pendingSwitchByName;
|
||||
const std::size_t reqIndex = context.pendingAppIndex;
|
||||
const std::string reqName = context.pendingAppName;
|
||||
context.pendingSwitch = false;
|
||||
context.pendingSwitchByName = false;
|
||||
context.pendingAppName.clear();
|
||||
bool switched = false;
|
||||
if (byName) {
|
||||
switched = startApp(reqName);
|
||||
} else {
|
||||
switched = startAppByIndex(reqIndex);
|
||||
}
|
||||
return switched;
|
||||
}
|
||||
|
||||
@@ -39,43 +39,27 @@ public:
|
||||
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
||||
|
||||
void onStart() override {
|
||||
cancelRefreshTimer();
|
||||
lastSnapshot = {};
|
||||
dirty = true;
|
||||
renderIfNeeded(captureTime());
|
||||
}
|
||||
|
||||
void step() override {
|
||||
const auto snap = captureTime();
|
||||
|
||||
InputState st = context.input.readState();
|
||||
|
||||
if (st.b && !backPrev) {
|
||||
context.requestAppSwitchByName(kMenuAppName);
|
||||
backPrev = st.b;
|
||||
selectPrev = st.select;
|
||||
return;
|
||||
}
|
||||
|
||||
if (st.select && !selectPrev) {
|
||||
use24Hour = !use24Hour;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (!sameSnapshot(snap, lastSnapshot))
|
||||
dirty = true;
|
||||
|
||||
renderIfNeeded(snap);
|
||||
|
||||
backPrev = st.b;
|
||||
selectPrev = st.select;
|
||||
lastSnapshot = snap;
|
||||
refreshTimer = context.scheduleRepeatingTimer(200);
|
||||
}
|
||||
|
||||
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
|
||||
AppSleepPlan plan;
|
||||
plan.slow_ms = 200;
|
||||
plan.normal_ms = 40;
|
||||
return plan;
|
||||
void onStop() override { cancelRefreshTimer(); }
|
||||
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
switch (event.type) {
|
||||
case AppEventType::Button:
|
||||
handleButtonEvent(event.button);
|
||||
break;
|
||||
case AppEventType::Timer:
|
||||
if (event.timer.handle == refreshTimer)
|
||||
updateDisplay();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -83,13 +67,44 @@ private:
|
||||
Framebuffer& framebuffer;
|
||||
Clock& clock;
|
||||
|
||||
bool use24Hour = true;
|
||||
bool dirty = false;
|
||||
bool backPrev = false;
|
||||
bool selectPrev = false;
|
||||
bool use24Hour = true;
|
||||
bool dirty = false;
|
||||
AppTimerHandle refreshTimer = kInvalidAppTimer;
|
||||
|
||||
TimeSnapshot lastSnapshot{};
|
||||
|
||||
void cancelRefreshTimer() {
|
||||
if (refreshTimer != kInvalidAppTimer) {
|
||||
context.cancelTimer(refreshTimer);
|
||||
refreshTimer = kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void handleButtonEvent(const AppButtonEvent& button) {
|
||||
const auto& current = button.current;
|
||||
const auto& previous = button.previous;
|
||||
|
||||
if (current.b && !previous.b) {
|
||||
context.requestAppSwitchByName(kMenuAppName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (current.select && !previous.select) {
|
||||
use24Hour = !use24Hour;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
void updateDisplay() {
|
||||
const auto snap = captureTime();
|
||||
if (!sameSnapshot(snap, lastSnapshot))
|
||||
dirty = true;
|
||||
renderIfNeeded(snap);
|
||||
lastSnapshot = snap;
|
||||
}
|
||||
|
||||
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
|
||||
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#pragma GCC optimize("Ofast")
|
||||
#include "apps/gameboy_app.hpp"
|
||||
#include "apps/peanut_gb.h"
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_system.hpp"
|
||||
#include "font16x8.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
#include <fs_helper.hpp>
|
||||
|
||||
#include <peanut_gb.h>
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
@@ -26,6 +27,18 @@
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
|
||||
#define GAMEBOY_PERF_METRICS 0
|
||||
|
||||
#ifndef GAMEBOY_PERF_METRICS
|
||||
#define GAMEBOY_PERF_METRICS 1
|
||||
#endif
|
||||
|
||||
#if GAMEBOY_PERF_METRICS
|
||||
#define GB_PERF_ONLY(...) __VA_ARGS__
|
||||
#else
|
||||
#define GB_PERF_ONLY(...)
|
||||
#endif
|
||||
|
||||
namespace apps {
|
||||
namespace {
|
||||
|
||||
@@ -159,7 +172,9 @@ public:
|
||||
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
|
||||
|
||||
void onStart() override {
|
||||
perf.resetAll();
|
||||
cancelTick();
|
||||
frameDelayCarryUs = 0;
|
||||
GB_PERF_ONLY(perf.resetAll();)
|
||||
prevInput = {};
|
||||
statusMessage.clear();
|
||||
resetFpsStats();
|
||||
@@ -169,37 +184,57 @@ public:
|
||||
refreshRomList();
|
||||
mode = Mode::Browse;
|
||||
browserDirty = true;
|
||||
scheduleNextTick(0);
|
||||
}
|
||||
|
||||
void onStop() override {
|
||||
perf.maybePrintAggregate(true);
|
||||
cancelTick();
|
||||
frameDelayCarryUs = 0;
|
||||
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
|
||||
unloadRom();
|
||||
}
|
||||
|
||||
void step() override {
|
||||
perf.resetForStep();
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
|
||||
tickTimer = kInvalidAppTimer;
|
||||
const uint64_t frameStartUs = esp_timer_get_time();
|
||||
performStep();
|
||||
const uint64_t frameEndUs = esp_timer_get_time();
|
||||
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
|
||||
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
|
||||
scheduleAfterFrame(elapsedUs);
|
||||
return;
|
||||
}
|
||||
if (event.type == AppEventType::Button) {
|
||||
frameDelayCarryUs = 0;
|
||||
scheduleNextTick(0);
|
||||
}
|
||||
}
|
||||
|
||||
const uint64_t inputStartUs = esp_timer_get_time();
|
||||
const InputState input = context.input.readState();
|
||||
perf.inputUs = esp_timer_get_time() - inputStartUs;
|
||||
void performStep() {
|
||||
GB_PERF_ONLY(perf.resetForStep();)
|
||||
|
||||
GB_PERF_ONLY(const uint64_t inputStartUs = esp_timer_get_time();)
|
||||
const InputState input = context.input.readState();
|
||||
GB_PERF_ONLY(perf.inputUs = esp_timer_get_time() - inputStartUs;)
|
||||
|
||||
const Mode stepMode = mode;
|
||||
|
||||
switch (stepMode) {
|
||||
case Mode::Browse: {
|
||||
const uint64_t handleStartUs = esp_timer_get_time();
|
||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
||||
handleBrowserInput(input);
|
||||
perf.handleUs = esp_timer_get_time() - handleStartUs;
|
||||
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
|
||||
|
||||
const uint64_t renderStartUs = esp_timer_get_time();
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
|
||||
renderBrowser();
|
||||
perf.renderUs = esp_timer_get_time() - renderStartUs;
|
||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
||||
break;
|
||||
}
|
||||
case Mode::Running: {
|
||||
const uint64_t handleStartUs = esp_timer_get_time();
|
||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
||||
handleGameInput(input);
|
||||
perf.handleUs = esp_timer_get_time() - handleStartUs;
|
||||
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
|
||||
|
||||
if (!gbReady) {
|
||||
mode = Mode::Browse;
|
||||
@@ -207,41 +242,28 @@ public:
|
||||
break;
|
||||
}
|
||||
|
||||
const uint64_t geometryStartUs = esp_timer_get_time();
|
||||
GB_PERF_ONLY(const uint64_t geometryStartUs = esp_timer_get_time();)
|
||||
ensureRenderGeometry();
|
||||
perf.geometryUs = esp_timer_get_time() - geometryStartUs;
|
||||
GB_PERF_ONLY(perf.geometryUs = esp_timer_get_time() - geometryStartUs;)
|
||||
|
||||
const uint64_t waitStartUs = esp_timer_get_time();
|
||||
DispTools::draw_to_display_async_wait();
|
||||
perf.waitUs = esp_timer_get_time() - waitStartUs;
|
||||
|
||||
const uint64_t runStartUs = esp_timer_get_time();
|
||||
GB_PERF_ONLY(const uint64_t runStartUs = esp_timer_get_time();)
|
||||
gb_run_frame(&gb);
|
||||
perf.runUs = esp_timer_get_time() - runStartUs;
|
||||
GB_PERF_ONLY(perf.runUs = esp_timer_get_time() - runStartUs;)
|
||||
|
||||
const uint64_t renderStartUs = esp_timer_get_time();
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
|
||||
renderGameFrame();
|
||||
perf.renderUs = esp_timer_get_time() - renderStartUs;
|
||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prevInput = input;
|
||||
|
||||
perf.finishStep();
|
||||
perf.accumulate();
|
||||
perf.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(), selectedIndex,
|
||||
browserDirty);
|
||||
perf.maybePrintAggregate();
|
||||
}
|
||||
|
||||
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
|
||||
if (mode == Mode::Running)
|
||||
return {};
|
||||
AppSleepPlan plan;
|
||||
plan.slow_ms = 140;
|
||||
plan.normal_ms = 50;
|
||||
return plan;
|
||||
GB_PERF_ONLY(perf.finishStep();)
|
||||
GB_PERF_ONLY(perf.accumulate();)
|
||||
GB_PERF_ONLY(perf.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(),
|
||||
selectedIndex, browserDirty);)
|
||||
GB_PERF_ONLY(perf.maybePrintAggregate();)
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -451,22 +473,34 @@ private:
|
||||
|
||||
class ScopedCallbackTimer {
|
||||
public:
|
||||
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) : app(instance), kind(kind) {
|
||||
if (app)
|
||||
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) {
|
||||
#if GAMEBOY_PERF_METRICS
|
||||
if (instance) {
|
||||
app = instance;
|
||||
cbKind = kind;
|
||||
startUs = esp_timer_get_time();
|
||||
}
|
||||
#else
|
||||
(void) instance;
|
||||
(void) kind;
|
||||
#endif
|
||||
}
|
||||
|
||||
~ScopedCallbackTimer() {
|
||||
#if GAMEBOY_PERF_METRICS
|
||||
if (!app)
|
||||
return;
|
||||
const uint64_t end = esp_timer_get_time();
|
||||
app->perf.addCallback(kind, end - startUs);
|
||||
app->perf.addCallback(cbKind, end - startUs);
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
GameboyApp* app;
|
||||
PerfTracker::CallbackKind kind;
|
||||
#if GAMEBOY_PERF_METRICS
|
||||
GameboyApp* app = nullptr;
|
||||
PerfTracker::CallbackKind cbKind{};
|
||||
uint64_t startUs = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
struct RenderGeometry {
|
||||
@@ -484,9 +518,12 @@ private:
|
||||
std::array<int, LCD_WIDTH> colXEnd{};
|
||||
};
|
||||
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
PerfTracker perf{};
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
PerfTracker perf{};
|
||||
AppTimerHandle tickTimer = kInvalidAppTimer;
|
||||
int64_t frameDelayCarryUs = 0;
|
||||
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
|
||||
|
||||
Mode mode = Mode::Browse;
|
||||
ScaleMode scaleMode = ScaleMode::Original;
|
||||
@@ -505,13 +542,47 @@ private:
|
||||
const uint8_t* romDataView = nullptr;
|
||||
std::size_t romDataViewSize = 0;
|
||||
std::vector<uint8_t> cartRam;
|
||||
bool frameDirty = false;
|
||||
uint32_t fpsLastSampleMs = 0;
|
||||
uint32_t fpsFrameCounter = 0;
|
||||
uint32_t fpsCurrent = 0;
|
||||
bool frameDirty = false;
|
||||
uint32_t fpsLastSampleMs = 0;
|
||||
uint32_t fpsFrameCounter = 0;
|
||||
uint32_t totalFrameCounter = 0;
|
||||
uint32_t fpsCurrent = 0;
|
||||
std::string activeRomName;
|
||||
std::string activeRomSavePath;
|
||||
|
||||
void cancelTick() {
|
||||
if (tickTimer != kInvalidAppTimer) {
|
||||
context.cancelTimer(tickTimer);
|
||||
tickTimer = kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
|
||||
void scheduleNextTick(uint32_t delayMs) {
|
||||
cancelTick();
|
||||
tickTimer = context.scheduleTimer(delayMs, false);
|
||||
}
|
||||
|
||||
uint32_t idleDelayMs() const { return browserDirty ? 50 : 140; }
|
||||
|
||||
void scheduleAfterFrame(uint64_t elapsedUs) {
|
||||
if (mode == Mode::Running && gbReady) {
|
||||
int64_t desiredUs = static_cast<int64_t>(kTargetFrameUs) - static_cast<int64_t>(elapsedUs);
|
||||
desiredUs += frameDelayCarryUs;
|
||||
if (desiredUs <= 0) {
|
||||
frameDelayCarryUs = desiredUs;
|
||||
scheduleNextTick(0);
|
||||
return;
|
||||
}
|
||||
frameDelayCarryUs = desiredUs % 1000;
|
||||
desiredUs -= frameDelayCarryUs;
|
||||
uint32_t delayMs = static_cast<uint32_t>(desiredUs / 1000);
|
||||
scheduleNextTick(delayMs);
|
||||
return;
|
||||
}
|
||||
frameDelayCarryUs = 0;
|
||||
scheduleNextTick(idleDelayMs());
|
||||
}
|
||||
|
||||
bool ensureFilesystemReady() {
|
||||
esp_err_t err = FsHelper::get().mount();
|
||||
if (err != ESP_OK) {
|
||||
@@ -866,13 +937,14 @@ private:
|
||||
return false;
|
||||
}
|
||||
|
||||
gb.direct.priv = this;
|
||||
gb.direct.joypad = 0xFF;
|
||||
gb.direct.interlace = false;
|
||||
gb.direct.frame_skip = false;
|
||||
gb.direct.priv = this;
|
||||
gb.direct.joypad = 0xFF;
|
||||
|
||||
gb_init_lcd(&gb, &GameboyApp::lcdDrawLine);
|
||||
|
||||
gb.direct.interlace = false;
|
||||
gb.direct.frame_skip = true;
|
||||
|
||||
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
||||
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
||||
std::string savePath;
|
||||
@@ -971,6 +1043,7 @@ private:
|
||||
ensureRenderGeometry();
|
||||
|
||||
++fpsFrameCounter;
|
||||
++totalFrameCounter;
|
||||
const uint32_t nowMs = context.clock.millis();
|
||||
if (fpsLastSampleMs == 0)
|
||||
fpsLastSampleMs = nowMs;
|
||||
@@ -982,9 +1055,11 @@ private:
|
||||
fpsLastSampleMs = nowMs;
|
||||
}
|
||||
|
||||
char fpsBuf[16];
|
||||
std::snprintf(fpsBuf, sizeof(fpsBuf), "%u FPS", static_cast<unsigned>(fpsCurrent));
|
||||
const std::string fpsText(fpsBuf);
|
||||
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";
|
||||
|
||||
if (scaleMode == ScaleMode::FullHeight) {
|
||||
@@ -998,46 +1073,69 @@ private:
|
||||
const int maxLeftX = std::max(0, screenWidth - rotatedWidth);
|
||||
const int maxRightXBase = std::max(0, screenWidth - rotatedWidth);
|
||||
|
||||
|
||||
const int horizontalPadding = 8;
|
||||
const int fpsLineGap = 4;
|
||||
const int fpsLabelWidth = font16x8::measureText(fpsLabel, 1, 1);
|
||||
const int fpsValueWidth = font16x8::measureText(fpsValue, 1, 1);
|
||||
const int fpsBlockWidth = std::max(fpsLabelWidth, fpsValueWidth);
|
||||
int fpsX = std::max(0, screenWidth - fpsBlockWidth - horizontalPadding);
|
||||
const int fpsY = horizontalPadding;
|
||||
font16x8::drawText(framebuffer, fpsX, fpsY, fpsLabel, 1, true, 1);
|
||||
font16x8::drawText(framebuffer, fpsX, fpsY + font16x8::kGlyphHeight + fpsLineGap, fpsValue, 1, true, 1);
|
||||
const int reservedTop = fpsY + (font16x8::kGlyphHeight * 2) + fpsLineGap + horizontalPadding;
|
||||
|
||||
if (!activeRomName.empty()) {
|
||||
const int textHeight = measureVerticalText(activeRomName, textScale);
|
||||
const int maxOrigin = std::max(0, screenHeight - textHeight);
|
||||
int leftX = std::clamp((leftMargin - rotatedWidth) / 2, 0, maxLeftX);
|
||||
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
|
||||
drawTextRotated(framebuffer, leftX, leftY, activeRomName, false, textScale, true, 1);
|
||||
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);
|
||||
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 int gap = 8;
|
||||
int totalRight = 0;
|
||||
const auto accumulateHeight = [&](std::string_view text) {
|
||||
if (text.empty())
|
||||
return;
|
||||
if (totalRight > 0)
|
||||
totalRight += gap;
|
||||
totalRight += measureVerticalText(text, textScale);
|
||||
};
|
||||
accumulateHeight(fpsText);
|
||||
accumulateHeight("START+SELECT BACK");
|
||||
accumulateHeight(scaleHint);
|
||||
if (!statusMessage.empty())
|
||||
accumulateHeight(statusMessage);
|
||||
|
||||
const int maxRightOrigin = std::max(0, screenHeight - totalRight);
|
||||
int rightY = std::clamp((screenHeight - totalRight) / 2, 0, maxRightOrigin);
|
||||
int rightX = screenWidth - rightMargin + std::max(0, (rightMargin - rotatedWidth) / 2);
|
||||
rightX = std::clamp(rightX, 0, maxRightXBase);
|
||||
std::vector<std::string_view> rightTexts;
|
||||
rightTexts.reserve(2U);
|
||||
rightTexts.emplace_back("START+SELECT BACK");
|
||||
rightTexts.emplace_back(scaleHint);
|
||||
|
||||
const auto drawRight = [&](std::string_view text) {
|
||||
const int gap = 8;
|
||||
int totalRightHeight = 0;
|
||||
for (std::string_view text: rightTexts) {
|
||||
if (text.empty())
|
||||
return;
|
||||
drawTextRotated(framebuffer, rightX, rightY, text, true, textScale, true, 1);
|
||||
rightY += measureVerticalText(text, textScale);
|
||||
rightY += gap;
|
||||
};
|
||||
drawRight(fpsText);
|
||||
drawRight("START+SELECT BACK");
|
||||
drawRight(scaleHint);
|
||||
if (!statusMessage.empty())
|
||||
drawRight(statusMessage);
|
||||
continue;
|
||||
std::string rotated(text.rbegin(), text.rend());
|
||||
if (totalRightHeight > 0)
|
||||
totalRightHeight += gap;
|
||||
totalRightHeight += measureVerticalText(rotated, textScale);
|
||||
}
|
||||
|
||||
const int maxRightOrigin = std::max(0, screenHeight - totalRightHeight);
|
||||
int rightY = std::clamp((screenHeight - totalRightHeight) / 2, 0, maxRightOrigin);
|
||||
if (rightY < reservedTop)
|
||||
rightY = std::min(std::max(reservedTop, 0), maxRightOrigin);
|
||||
|
||||
int rightX = screenWidth - 20;
|
||||
|
||||
for (size_t i = 0; i < rightTexts.size(); ++i) {
|
||||
std::string_view text = rightTexts[i];
|
||||
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);
|
||||
rightX -= 20;
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!activeRomName.empty())
|
||||
font16x8::drawText(framebuffer, 16, 16, activeRomName, 1, true, 1);
|
||||
@@ -1054,8 +1152,8 @@ private:
|
||||
|
||||
if (!statusMessage.empty()) {
|
||||
const int statusWidth = font16x8::measureText(statusMessage, 1, 1);
|
||||
const int statusX = std::max(12, (framebuffer.width() - statusWidth) / 2);
|
||||
const int statusY = std::max(16, instructionY - font16x8::kGlyphHeight - 4);
|
||||
const int statusX = std::max(12, (framebuffer.width() - statusWidth) - 12);
|
||||
const int statusY = instructionY;
|
||||
font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1);
|
||||
}
|
||||
}
|
||||
@@ -1127,6 +1225,19 @@ private:
|
||||
return static_cast<GameboyApp*>(gb->direct.priv);
|
||||
}
|
||||
|
||||
static bool shouldPixelBeOn(int value, int dstX, int dstY, bool useDither) {
|
||||
value &= 0x03;
|
||||
if (!useDither)
|
||||
return value >= 2;
|
||||
if (value >= 3)
|
||||
return true;
|
||||
if (value <= 0)
|
||||
return false;
|
||||
constexpr uint8_t pattern[2][2] = {{0, 2}, {3, 1}};
|
||||
const uint8_t threshold = pattern[dstY & 0x01][dstX & 0x01];
|
||||
return value > threshold;
|
||||
}
|
||||
|
||||
static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) {
|
||||
auto* self = fromGb(gb);
|
||||
if (!self)
|
||||
@@ -1169,7 +1280,8 @@ private:
|
||||
self->unloadRom();
|
||||
}
|
||||
|
||||
static void lcdDrawLine(struct gb_s* gb, const uint8_t pixels[160], const uint_fast8_t line) {
|
||||
__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;
|
||||
@@ -1190,11 +1302,19 @@ private:
|
||||
|
||||
Framebuffer& fb = self->framebuffer;
|
||||
|
||||
GB_PERF_ONLY(const uint64_t waitStartUs = esp_timer_get_time();)
|
||||
DispTools::draw_to_display_async_wait();
|
||||
GB_PERF_ONLY(self->perf.waitUs = esp_timer_get_time() - waitStartUs;)
|
||||
|
||||
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
|
||||
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
|
||||
|
||||
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 = (pixels[x] & 0x03u) >= 2;
|
||||
const int val = (pixels[x] & 0x03u);
|
||||
const bool on = shouldPixelBeOn(val, dstXBase + x, dstY, useDither);
|
||||
fb.drawPixel(dstXBase + x, dstY, on);
|
||||
}
|
||||
return;
|
||||
@@ -1207,10 +1327,11 @@ private:
|
||||
const int drawEnd = colEnd[x];
|
||||
if (drawStart >= drawEnd)
|
||||
continue;
|
||||
const bool on = (pixels[x] & 0x03u) >= 2;
|
||||
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);
|
||||
fb.drawPixel(dstX, dstY, on);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,45 +32,33 @@ public:
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
void step() override {
|
||||
InputState st = context.input.readState();
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
if (event.type != AppEventType::Button)
|
||||
return;
|
||||
|
||||
if (st.left && !leftPrev) {
|
||||
const auto& current = event.button.current;
|
||||
const auto& previous = event.button.previous;
|
||||
|
||||
if (current.left && !previous.left) {
|
||||
moveSelection(-1);
|
||||
} else if (st.right && !rightPrev) {
|
||||
} else if (current.right && !previous.right) {
|
||||
moveSelection(+1);
|
||||
}
|
||||
|
||||
const bool launch = (st.a && !rotatePrev) || (st.select && !selectPrev);
|
||||
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
|
||||
if (launch)
|
||||
launchSelected();
|
||||
|
||||
leftPrev = st.left;
|
||||
rightPrev = st.right;
|
||||
rotatePrev = st.a;
|
||||
selectPrev = st.select;
|
||||
|
||||
renderIfNeeded();
|
||||
}
|
||||
|
||||
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
|
||||
AppSleepPlan plan;
|
||||
plan.slow_ms = 120;
|
||||
plan.normal_ms = 40;
|
||||
return plan;
|
||||
}
|
||||
|
||||
private:
|
||||
AppContext& context;
|
||||
Framebuffer& framebuffer;
|
||||
std::vector<MenuEntry> entries;
|
||||
std::size_t selected = 0;
|
||||
|
||||
bool dirty = false;
|
||||
bool leftPrev = false;
|
||||
bool rightPrev = false;
|
||||
bool rotatePrev = false;
|
||||
bool selectPrev = false;
|
||||
bool dirty = false;
|
||||
|
||||
void moveSelection(int step) {
|
||||
if (entries.empty())
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "apps/tetris_app.hpp"
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_system.hpp"
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "font16x8.hpp"
|
||||
|
||||
@@ -1149,17 +1150,51 @@ namespace {
|
||||
|
||||
class TetrisApp final : public IApp {
|
||||
public:
|
||||
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
|
||||
explicit TetrisApp(AppContext& ctx) : context(ctx), game(ctx) {}
|
||||
|
||||
void step() override { game.step(); }
|
||||
void onStart() override {
|
||||
cancelTick();
|
||||
scheduleNextTick(0);
|
||||
}
|
||||
|
||||
AppSleepPlan sleepPlan(uint32_t now) const override {
|
||||
auto plan = game.recommendedSleepMs(now);
|
||||
return {plan.slow_ms, plan.normal_ms};
|
||||
void onStop() override { cancelTick(); }
|
||||
|
||||
void handleEvent(const AppEvent& event) override {
|
||||
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
|
||||
tickTimer = kInvalidAppTimer;
|
||||
runStep();
|
||||
return;
|
||||
}
|
||||
if (event.type == AppEventType::Button) {
|
||||
scheduleNextTick(0);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
tetris::Game game;
|
||||
AppContext& context;
|
||||
tetris::Game game;
|
||||
AppTimerHandle tickTimer = kInvalidAppTimer;
|
||||
|
||||
void runStep() {
|
||||
game.step();
|
||||
const auto now = context.clock.millis();
|
||||
const auto plan = game.recommendedSleepMs(now);
|
||||
uint32_t delay = plan.normal_ms != 0 ? plan.normal_ms : plan.slow_ms;
|
||||
scheduleNextTick(delay);
|
||||
}
|
||||
|
||||
void scheduleNextTick(uint32_t delayMs) {
|
||||
if (tickTimer != kInvalidAppTimer)
|
||||
context.cancelTimer(tickTimer);
|
||||
tickTimer = context.scheduleTimer(delayMs, false);
|
||||
}
|
||||
|
||||
void cancelTick() {
|
||||
if (tickTimer != kInvalidAppTimer) {
|
||||
context.cancelTimer(tickTimer);
|
||||
tickTimer = kInvalidAppTimer;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class TetrisAppFactory final : public IAppFactory {
|
||||
|
||||
@@ -19,7 +19,7 @@ static i2c_master_dev_handle_t dev_handle;
|
||||
static inline i2c_device_config_t dev_cfg = {
|
||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||
.device_address = 0x20,
|
||||
.scl_speed_hz = 100000,
|
||||
.scl_speed_hz = 50000,
|
||||
};
|
||||
|
||||
Buttons& Buttons::get() {
|
||||
@@ -62,7 +62,7 @@ Buttons::Buttons() {
|
||||
buf2[0] = 7;
|
||||
buf2[1] = 0x80;
|
||||
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
|
||||
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 1, &_pooler_task);
|
||||
xTaskCreate(&start_pooler, "ButtonsPooler", 2048, this, 2, &_pooler_task);
|
||||
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(EXP_INT));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(EXP_INT, GPIO_MODE_INPUT));
|
||||
@@ -90,7 +90,11 @@ void Buttons::pooler() {
|
||||
reg = 1;
|
||||
ESP_ERROR_CHECK(
|
||||
i2c_master_transmit_receive(dev_handle, ®, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
|
||||
if (_listener)
|
||||
xTaskNotifyGive(_listener);
|
||||
}
|
||||
}
|
||||
uint8_t Buttons::get_pressed() { return _current; }
|
||||
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
|
||||
|
||||
void Buttons::register_listener(TaskHandle_t task) { _listener = task; }
|
||||
|
||||
@@ -1,46 +1,49 @@
|
||||
// Simplified display implementation (no async memcpy) ---------------------------------
|
||||
// Double-buffered display implementation with async memcpy ---------------------------------
|
||||
|
||||
#include "display.hpp"
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <driver/gpio.h>
|
||||
#include "disp_tools.hpp"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_async_memcpy.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
DMA_ATTR uint8_t SMD::dma_buf[SMD::kLineDataBytes]{};
|
||||
DMA_ATTR uint8_t dma_buf_template[SMD::kLineDataBytes]{};
|
||||
DMA_ATTR static uint8_t s_dma_buffer0[SMD::kLineDataBytes]{};
|
||||
DMA_ATTR static uint8_t s_dma_buffer1[SMD::kLineDataBytes]{};
|
||||
static uint8_t* s_dma_buffers[2] = {s_dma_buffer0, s_dma_buffer1};
|
||||
DMA_ATTR static uint8_t dma_buf_template[SMD::kLineDataBytes]{};
|
||||
|
||||
uint8_t* SMD::dma_buf = s_dma_buffers[0];
|
||||
|
||||
spi_device_handle_t SMD::_spi;
|
||||
|
||||
static spi_transaction_t _tx{};
|
||||
static SemaphoreHandle_t _txSem = nullptr;
|
||||
static bool _vcom = false;
|
||||
volatile bool _inFlight = false;
|
||||
static TaskHandle_t s_clearTaskHandle = nullptr;
|
||||
static SemaphoreHandle_t s_clearReqSem = nullptr;
|
||||
static SemaphoreHandle_t s_clearSem = nullptr;
|
||||
static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr};
|
||||
|
||||
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
|
||||
// update the maximum data stream supported by underlying DMA engine
|
||||
static async_memcpy_handle_t driver = NULL;
|
||||
static async_memcpy_handle_t driver = nullptr;
|
||||
|
||||
static volatile int s_drawBufIdx = 0;
|
||||
|
||||
static unsigned char reverse_bits3(unsigned char b) { return (b * 0x0202020202ULL & 0x010884422010ULL) % 0x3ff; }
|
||||
|
||||
|
||||
static bool IRAM_ATTR my_async_memcpy_cb(async_memcpy_handle_t mcp_hdl, async_memcpy_event_t* event, void* cb_args) {
|
||||
static bool IRAM_ATTR my_async_memcpy_cb(async_memcpy_handle_t /*mcp_hdl*/, async_memcpy_event_t* /*event*/,
|
||||
void* cb_args) {
|
||||
BaseType_t high_task_wakeup = pdFALSE;
|
||||
_inFlight = false;
|
||||
xSemaphoreGiveFromISR(s_clearSem,
|
||||
&high_task_wakeup); // high_task_wakeup set to pdTRUE if some high priority task unblocked
|
||||
auto sem = static_cast<SemaphoreHandle_t>(cb_args);
|
||||
xSemaphoreGiveFromISR(sem, &high_task_wakeup);
|
||||
return high_task_wakeup == pdTRUE;
|
||||
}
|
||||
|
||||
static void zero_framebuffer_payload() {
|
||||
ESP_ERROR_CHECK(esp_async_memcpy(driver, SMD::dma_buf, dma_buf_template, 12480, my_async_memcpy_cb, nullptr));
|
||||
}
|
||||
|
||||
extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
|
||||
BaseType_t hpw = pdFALSE;
|
||||
xSemaphoreGiveFromISR(s_clearReqSem, &hpw);
|
||||
@@ -51,87 +54,92 @@ extern "C" void IRAM_ATTR s_spi_post_cb(spi_transaction_t* /*t*/) {
|
||||
static void clear_task(void*) {
|
||||
for (;;) {
|
||||
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
|
||||
printf("Started zeroing\n");
|
||||
spi_transaction_t* r = nullptr;
|
||||
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
|
||||
zero_framebuffer_payload();
|
||||
// printf("Zeroing done\n");
|
||||
int bufIdx = (int) r->user;
|
||||
xSemaphoreGive(_txSem);
|
||||
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, 12480U,
|
||||
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SMD::ensure_clear_task() {
|
||||
if (!s_clearReqSem)
|
||||
s_clearReqSem = xSemaphoreCreateBinary();
|
||||
if (!s_clearSem)
|
||||
s_clearSem = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(s_clearSem);
|
||||
|
||||
if (!s_clearTaskHandle)
|
||||
xTaskCreatePinnedToCore(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle, 0);
|
||||
}
|
||||
|
||||
|
||||
void SMD::init() {
|
||||
spi_bus_add_device(SPI_BUS, &_devcfg, &_spi);
|
||||
ensure_clear_task();
|
||||
ESP_ERROR_CHECK(gpio_reset_pin(SPI_DISP_DISP));
|
||||
ESP_ERROR_CHECK(gpio_set_direction(SPI_DISP_DISP, GPIO_MODE_OUTPUT));
|
||||
ESP_ERROR_CHECK(gpio_set_level(SPI_DISP_DISP, 1));
|
||||
ESP_ERROR_CHECK(gpio_hold_en(SPI_DISP_DISP));
|
||||
for (uint8_t i = 0; i < DISP_HEIGHT; i++) {
|
||||
dma_buf[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
|
||||
dma_buf[2 + kLineMultiSingle * i + kLineBytes] = 0;
|
||||
for (int buf = 0; buf < 2; ++buf) {
|
||||
auto* fb = s_dma_buffers[buf];
|
||||
for (uint8_t i = 0; i < DISP_HEIGHT; ++i) {
|
||||
fb[kLineMultiSingle * i + 1] = reverse_bits3(i + 1);
|
||||
fb[2 + kLineMultiSingle * i + kLineBytes] = 0;
|
||||
}
|
||||
fb[kLineDataBytes - 1] = 0;
|
||||
}
|
||||
dma_buf[kLineDataBytes - 1] = 0;
|
||||
|
||||
s_drawBufIdx = 0;
|
||||
dma_buf = s_dma_buffers[s_drawBufIdx];
|
||||
|
||||
for (int y = 0; y < DISP_HEIGHT; ++y)
|
||||
for (int x = 0; x < DISP_WIDTH; ++x)
|
||||
DispTools::set_pixel(x, y, false);
|
||||
set_pixel(x, y, false);
|
||||
|
||||
std::memcpy(dma_buf_template, dma_buf, sizeof(dma_buf_template));
|
||||
std::memcpy(s_dma_buffers[1], dma_buf_template, sizeof(dma_buf_template));
|
||||
ESP_ERROR_CHECK(esp_async_memcpy_install(&config, &driver)); // install driver with default DMA engine
|
||||
|
||||
s_clearReqSem = xSemaphoreCreateBinary();
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
s_bufferSem[i] = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(s_bufferSem[i]);
|
||||
}
|
||||
|
||||
_txSem = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(_txSem);
|
||||
xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle);
|
||||
}
|
||||
|
||||
bool SMD::async_draw_busy() { return _inFlight; }
|
||||
bool SMD::async_draw_busy() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
|
||||
|
||||
void SMD::async_draw_start() {
|
||||
assert(!_inFlight);
|
||||
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
|
||||
assert(driver != nullptr);
|
||||
if (!xSemaphoreTake(_txSem, portMAX_DELAY))
|
||||
assert(false);
|
||||
_vcom = !_vcom;
|
||||
_tx = {};
|
||||
_tx.tx_buffer = dma_buf;
|
||||
_tx.length = SMD::kLineDataBytes * 8;
|
||||
dma_buf[0] = 0b10000000 | (_vcom << 6);
|
||||
_inFlight = true;
|
||||
|
||||
const int sendIdx = s_drawBufIdx;
|
||||
assert(sendIdx >= 0 && sendIdx < 2);
|
||||
|
||||
SemaphoreHandle_t sem = s_bufferSem[sendIdx];
|
||||
if (!xSemaphoreTake(sem, 0))
|
||||
assert(false);
|
||||
|
||||
const int nextDrawIdx = sendIdx ^ 1;
|
||||
|
||||
_vcom = !_vcom;
|
||||
_tx = {};
|
||||
_tx.tx_buffer = s_dma_buffers[sendIdx];
|
||||
_tx.length = SMD::kLineDataBytes * 8;
|
||||
_tx.user = (void*) (sendIdx);
|
||||
s_dma_buffers[sendIdx][0] = 0b10000000 | (_vcom << 6);
|
||||
ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0));
|
||||
|
||||
s_drawBufIdx = nextDrawIdx;
|
||||
dma_buf = s_dma_buffers[nextDrawIdx];
|
||||
}
|
||||
|
||||
void SMD::async_draw_wait() {
|
||||
if (!_inFlight || uxSemaphoreGetCount(s_clearSem)) {
|
||||
// assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight);
|
||||
return;
|
||||
SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx];
|
||||
// uint64_t waitedUs = 0;
|
||||
if (!uxSemaphoreGetCount(sem)) {
|
||||
// uint64_t start = esp_timer_get_time();
|
||||
if (!xSemaphoreTake(sem, portMAX_DELAY))
|
||||
assert(false);
|
||||
if (!xSemaphoreGive(sem))
|
||||
assert(false);
|
||||
// waitedUs = esp_timer_get_time() - start;
|
||||
}
|
||||
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
|
||||
assert(false);
|
||||
if (!xSemaphoreGive(s_clearSem))
|
||||
assert(false);
|
||||
assert(!_inFlight);
|
||||
// if (waitedUs)
|
||||
// printf("Waited %" PRIu64 " us\n", waitedUs);
|
||||
}
|
||||
|
||||
// (clear_in_progress / wait_clear / request_clear removed from public API)
|
||||
|
||||
// Surface implementation ------------------------------------------------------
|
||||
void SMDSurface::draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel) {
|
||||
if (pixel.on)
|
||||
DispTools::set_pixel(x, y);
|
||||
else
|
||||
DispTools::reset_pixel(x, y);
|
||||
}
|
||||
void SMDSurface::clear_impl() { DispTools::clear(); }
|
||||
int SMDSurface::get_width_impl() const { return DISP_WIDTH; }
|
||||
int SMDSurface::get_height_impl() const { return DISP_HEIGHT; }
|
||||
EventHandlingResult SMDSurface::handle(SurfaceResizeEvent event) { return _window->handle(event); }
|
||||
SMDSurface::SMDSurface(EventLoop* loop) :
|
||||
Surface<SMDSurface, BwPixel>(),
|
||||
EventQueue<SMDSurface, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>(loop, this) {}
|
||||
SMDSurface::~SMDSurface() {}
|
||||
|
||||
@@ -1699,9 +1699,13 @@ CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048
|
||||
CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10
|
||||
CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0
|
||||
CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1
|
||||
# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set
|
||||
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
|
||||
CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS=y
|
||||
# CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set
|
||||
# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set
|
||||
# CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID is not set
|
||||
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
|
||||
CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U32=y
|
||||
# CONFIG_FREERTOS_RUN_TIME_COUNTER_TYPE_U64 is not set
|
||||
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
|
||||
CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP=3
|
||||
# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set
|
||||
@@ -1721,6 +1725,7 @@ CONFIG_FREERTOS_TICK_SUPPORT_SYSTIMER=y
|
||||
CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL1=y
|
||||
# CONFIG_FREERTOS_CORETIMER_SYSTIMER_LVL3 is not set
|
||||
CONFIG_FREERTOS_SYSTICK_USES_SYSTIMER=y
|
||||
CONFIG_FREERTOS_RUN_TIME_STATS_USING_ESP_TIMER=y
|
||||
# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set
|
||||
# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set
|
||||
# end of Port
|
||||
|
||||
Reference in New Issue
Block a user