display fix

This commit is contained in:
2025-10-09 22:25:51 +02:00
parent 0660c40ec4
commit afff3d0e02
6 changed files with 166 additions and 165 deletions

View File

@@ -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
.mode = 0, // SPI mode 0
.clock_speed_hz = 10 * 1000 * 1000, // Clock out at 10 MHz
.spics_io_num = SPI_DISP_CS, // CS pin
.spics_io_num = SPI_DISP_CS, // CS pin
.flags = SPI_DEVICE_POSITIVE_CS,
.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

View File

@@ -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

View File

@@ -11,14 +11,12 @@
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;
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;
}
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
if (!factory)
@@ -40,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;
@@ -80,8 +78,8 @@ void AppSystem::run() {
const InputState inputNow = context.input.readState();
if (inputsDiffer(inputNow, lastInputState)) {
AppEvent evt{};
evt.type = AppEventType::Button;
evt.timestamp_ms = now;
evt.type = AppEventType::Button;
evt.timestamp_ms = now;
evt.button.current = inputNow;
evt.button.previous = lastInputState;
events.push_back(evt);
@@ -143,7 +141,7 @@ AppTimerHandle AppSystem::scheduleTimer(uint32_t delay_ms, bool repeat) {
if (!current)
return kInvalidAppTimer;
TimerRecord record;
record.id = nextTimerId++;
record.id = nextTimerId++;
if (record.id == kInvalidAppTimer)
record.id = nextTimerId++;
record.generation = currentGeneration;
@@ -160,8 +158,7 @@ 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.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
@@ -170,8 +167,7 @@ void AppSystem::cancelAllTimers() {
if (timer.generation == currentGeneration)
timer.active = false;
}
timers.erase(std::remove_if(timers.begin(), timers.end(),
[](const TimerRecord& rec) { return !rec.active; }),
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
@@ -186,20 +182,19 @@ void AppSystem::processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEv
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;
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;
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.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
}
@@ -235,9 +230,9 @@ AppSystem::TimerRecord* AppSystem::findTimer(AppTimerHandle handle) {
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;
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();

View File

@@ -27,7 +27,7 @@
#include <sys/stat.h>
#include <vector>
#define GAMEBOY_PERF_METRICS 0
#define GAMEBOY_PERF_METRICS 0
#ifndef GAMEBOY_PERF_METRICS
#define GAMEBOY_PERF_METRICS 1
@@ -196,7 +196,7 @@ public:
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
tickTimer = kInvalidAppTimer;
const uint64_t frameStartUs = esp_timer_get_time();
performStep();
const uint64_t frameEndUs = esp_timer_get_time();
@@ -215,7 +215,7 @@ public:
GB_PERF_ONLY(perf.resetForStep();)
GB_PERF_ONLY(const uint64_t inputStartUs = esp_timer_get_time();)
const InputState input = context.input.readState();
const InputState input = context.input.readState();
GB_PERF_ONLY(perf.inputUs = esp_timer_get_time() - inputStartUs;)
const Mode stepMode = mode;
@@ -261,8 +261,8 @@ public:
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.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(),
selectedIndex, browserDirty);)
GB_PERF_ONLY(perf.maybePrintAggregate();)
}
@@ -476,13 +476,13 @@ private:
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) {
#if GAMEBOY_PERF_METRICS
if (instance) {
app = instance;
cbKind = kind;
startUs = esp_timer_get_time();
app = instance;
cbKind = kind;
startUs = esp_timer_get_time();
}
#else
(void)instance;
(void)kind;
(void) instance;
(void) kind;
#endif
}
@@ -497,7 +497,7 @@ private:
private:
#if GAMEBOY_PERF_METRICS
GameboyApp* app = nullptr;
GameboyApp* app = nullptr;
PerfTracker::CallbackKind cbKind{};
uint64_t startUs = 0;
#endif
@@ -518,12 +518,12 @@ private:
std::array<int, LCD_WIDTH> colXEnd{};
};
AppContext& context;
Framebuffer& framebuffer;
PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer;
int64_t frameDelayCarryUs = 0;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
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;
@@ -936,13 +936,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;
@@ -1262,7 +1263,7 @@ private:
GB_PERF_ONLY(const uint64_t waitStartUs = esp_timer_get_time();)
DispTools::draw_to_display_async_wait();
GB_PERF_ONLY(perf.waitUs = esp_timer_get_time() - waitStartUs;)
GB_PERF_ONLY(self->perf.waitUs = esp_timer_get_time() - waitStartUs;)
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) {
const int dstY = yStart;

View File

@@ -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));
@@ -81,17 +81,42 @@ static void delay(unsigned long long loop) {
void Buttons::pooler() {
while (true) {
BaseType_t xResult = xTaskNotifyWait(pdFALSE, ULONG_MAX, nullptr, portMAX_DELAY);
uint8_t reg = 0;
uint8_t buffer;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
_current = buffer;
// read second port too to clear the interrupt
reg = 1;
ESP_ERROR_CHECK(
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
if (_listener)
xTaskNotifyGive(_listener);
while (true) {
auto reset = [&]() {
i2c_master_bus_rm_device(dev_handle);
ESP_ERROR_CHECK(i2c_master_bus_add_device(I2cGlobal::get().get_bus_handle(), &dev_cfg, &dev_handle));
uint8_t buf2[2];
buf2[0] = 6;
buf2[1] = 0xFF;
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
buf2[0] = 7;
buf2[1] = 0x80;
ESP_ERROR_CHECK(i2c_master_transmit(dev_handle, buf2, sizeof(buf2), -1));
};
uint8_t reg = 0;
uint8_t buffer;
auto err = i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer),
1, 100);
if (err != ESP_OK) {
printf("Error reading buttons: %d\n", err);
reset();
continue;
}
_current = buffer;
printf("Read buttons: 0x%02X\n", _current);
// read second port too to clear the interrupt
reg = 1;
err = i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1,
100);
if (err != ESP_OK) {
printf("Error reading buttons: %d\n", err);
reset();
continue;
}
if (_listener)
xTaskNotifyGive(_listener);
break;
}
}
}
uint8_t Buttons::get_pressed() { return _current; }

View File

@@ -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);
@@ -53,87 +56,90 @@ static void clear_task(void*) {
if (xSemaphoreTake(s_clearReqSem, portMAX_DELAY) == pdTRUE) {
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;
}
auto now = esp_timer_get_time();
if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))
assert(false);
if (!xSemaphoreGive(s_clearSem))
assert(false);
assert(!_inFlight);
auto elapsed = esp_timer_get_time() - now;
printf("Waited %" PRIu64 " us\n", elapsed);
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() {}