Files
Crosspoint/lib/hal/HalDisplay.cpp
T
Justin Mitchell 17e4230068 Add async display refresh to overlap CPU work
Introduces non-blocking display refresh methods that allow CPU work (like grayscale rendering) to overlap with the e-ink panel's refresh time. The async path starts the waveform and returns immediately, with the caller responsible for waiting via waitRefreshComplete(). Falls back to blocking refresh when fadingFix is enabled or the panel lacks deferral support.
2026-07-14 04:42:27 -04:00

143 lines
5.5 KiB
C++

#include <HalDisplay.h>
#include <HalGPIO.h>
// Global HalDisplay instance
HalDisplay display;
#define SD_SPI_MISO 7
HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {}
HalDisplay::~HalDisplay() {}
void HalDisplay::begin(bool seamless) {
// Set X3-specific panel mode before initializing.
if (gpio.deviceIsX3()) {
einkDisplay.setDisplayX3();
}
einkDisplay.begin();
if (seamless) {
// Defuse the SDK's X3 _x3InitialFullSyncsRemaining counter (no-op on X4)
// so the first paint isn't promoted to FULL (~770ms). Skips the wakeup-
// gated requestResync() below for the same reason.
einkDisplay.skipInitialResync();
return;
}
// Request resync after specific wakeup events to ensure clean display state.
const auto wakeupReason = gpio.getWakeupReason();
if (wakeupReason == HalGPIO::WakeupReason::PowerButton || wakeupReason == HalGPIO::WakeupReason::AfterFlash ||
wakeupReason == HalGPIO::WakeupReason::Other) {
einkDisplay.requestResync();
}
}
void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); }
void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
bool fromProgmem) const {
einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem);
}
void HalDisplay::drawImageTransparent(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h,
bool fromProgmem) const {
einkDisplay.drawImageTransparent(imageData, x, y, w, h, fromProgmem);
}
EInkDisplay::RefreshMode convertRefreshMode(HalDisplay::RefreshMode mode) {
switch (mode) {
case HalDisplay::FULL_REFRESH:
return EInkDisplay::FULL_REFRESH;
case HalDisplay::HALF_REFRESH:
return EInkDisplay::HALF_REFRESH;
case HalDisplay::FAST_REFRESH:
default:
return EInkDisplay::FAST_REFRESH;
}
}
void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode, bool turnOffScreen) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayBuffer(convertRefreshMode(mode), turnOffScreen);
}
void HalDisplay::displayBufferAsync(HalDisplay::RefreshMode mode) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayBufferAsyncNoShadow(convertRefreshMode(mode));
}
void HalDisplay::waitRefreshComplete() { einkDisplay.waitRefreshComplete(); }
bool HalDisplay::supportsAsyncRefresh() const { return einkDisplay.supportsAsyncRefresh(); }
void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) {
if (gpio.deviceIsX3() && mode == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen);
}
void HalDisplay::deepSleep() { einkDisplay.deepSleep(); }
uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); }
uint8_t* HalDisplay::lendFrameBufferStorage(uint32_t* sizeOut) { return einkDisplay.lendBuildStorage(sizeOut); }
void HalDisplay::returnFrameBufferStorage() { einkDisplay.returnBuildStorage(); }
void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) {
einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer);
}
void HalDisplay::displayGrayscaleBase(RefreshMode fallback, bool turnOffScreen) {
// X3: a HALF fallback means the caller wants a clean base (e.g. the sleep
// cover, a full-screen swap from arbitrary prior content). Without this, the
// X3 grayscale base takes its gentle differential happy path and the prior
// home/reader frame ghosts through the soft aa_pre_bw_mid waveform. Forcing a
// resync makes displayGrayscaleBase clear first, matching displayBuffer(HALF).
// The reader's FAST path is deliberately left on the differential path so
// per-page grayscale stays cheap.
if (gpio.deviceIsX3() && fallback == RefreshMode::HALF_REFRESH) {
einkDisplay.requestResync(1);
}
einkDisplay.displayGrayscaleBase(convertRefreshMode(fallback), turnOffScreen);
}
void HalDisplay::preconditionGrayscale() { einkDisplay.preconditionGrayscale(); }
void HalDisplay::preconditionGrayscale(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
einkDisplay.preconditionGrayscale(x, y, w, h);
}
void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); }
void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); }
void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); }
void HalDisplay::displayGrayBuffer(bool turnOffScreen) { einkDisplay.displayGrayBuffer(turnOffScreen); }
void HalDisplay::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows) {
einkDisplay.writeGrayscalePlaneStrip(lsbPlane ? EInkDisplay::GRAY_PLANE_LSB : EInkDisplay::GRAY_PLANE_MSB, rows,
yStart, numRows);
}
bool HalDisplay::supportsStripGrayscale() const { return einkDisplay.supportsStripGrayscale(); }
uint16_t HalDisplay::getDisplayWidth() const { return einkDisplay.getDisplayWidth(); }
uint16_t HalDisplay::getDisplayHeight() const { return einkDisplay.getDisplayHeight(); }
uint16_t HalDisplay::getDisplayWidthBytes() const { return einkDisplay.getDisplayWidthBytes(); }
uint32_t HalDisplay::getBufferSize() const { return einkDisplay.getBufferSize(); }