mirror of
https://github.com/meshtastic/firmware.git
synced 2026-11-07 14:54:44 -05:00
1069 lines
42 KiB
C++
1069 lines
42 KiB
C++
#ifdef MESHTASTIC_INCLUDE_INKHUD
|
|
|
|
#include "./Renderer.h"
|
|
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
#include "./Applets/User/BookReader/BookReaderApplet.h"
|
|
#include "PowerFSM.h"
|
|
#endif
|
|
#include "main.h"
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
#include <array>
|
|
#include <esp_heap_caps.h>
|
|
#endif
|
|
|
|
#include "./Applet.h"
|
|
#include "./SystemApplet.h"
|
|
#include "./Tile.h"
|
|
|
|
using namespace NicheGraphics;
|
|
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
#ifndef INKHUD_BOOKREADER_PERF_LOG
|
|
#define INKHUD_BOOKREADER_PERF_LOG 0
|
|
#endif
|
|
|
|
#if INKHUD_BOOKREADER_PERF_LOG
|
|
static uint32_t readerRefreshStartMs = 0;
|
|
#endif
|
|
#endif
|
|
|
|
InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer")
|
|
{
|
|
// Nothing for the timer to do just yet
|
|
OSThread::disable();
|
|
|
|
// Convenient references
|
|
inkhud = InkHUD::getInstance();
|
|
settings = &inkhud->persistence->settings;
|
|
}
|
|
|
|
// Connect the (fully set-up) E-Ink driver to InkHUD
|
|
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is
|
|
// called
|
|
void InkHUD::Renderer::setDriver(Drivers::EInk *driver)
|
|
{
|
|
// Make sure not already set
|
|
if (this->driver) {
|
|
LOG_ERROR("Driver already set");
|
|
delay(2000); // Wait for native serial..
|
|
assert(false);
|
|
}
|
|
|
|
// Store the driver which was created in setupNicheGraphics()
|
|
this->driver = driver;
|
|
displayHealth.maintenanceUpdateType = driver->supports(Drivers::EInk::UpdateTypes::CLEANUP)
|
|
? Drivers::EInk::UpdateTypes::CLEANUP
|
|
: Drivers::EInk::UpdateTypes::FULL;
|
|
|
|
// Determine the dimensions of the image buffer, in bytes.
|
|
// Along rows, pixels are stored 8 per byte.
|
|
// Not all display widths are divisible by 8. Need to make sure bytecount
|
|
// accommodates padding for these.
|
|
imageBufferWidth = ((driver->width - 1) / 8) + 1;
|
|
imageBufferHeight = driver->height;
|
|
|
|
// Allocate the image buffer
|
|
imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight];
|
|
}
|
|
|
|
// Set the target number of FAST display updates in a row, before a FULL update
|
|
// is used for display health This value applies only to updates with an
|
|
// UNSPECIFIED update type If explicitly requested FAST updates exceed this
|
|
// target, the stressMultiplier parameter determines how many subsequent FULL
|
|
// updates will be performed, in an attempt to restore the display's health
|
|
void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
|
|
{
|
|
displayHealth.fastPerFull = fastPerFull;
|
|
displayHealth.stressMultiplier = stressMultiplier;
|
|
}
|
|
|
|
void InkHUD::Renderer::begin()
|
|
{
|
|
forceUpdate(Drivers::EInk::UpdateTypes::FULL, true, false);
|
|
}
|
|
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
void InkHUD::Renderer::setGrayPixel(uint16_t x, uint16_t y, uint8_t luminance)
|
|
{
|
|
uint8_t &byte = grayBuffer[static_cast<size_t>(y) * ((driver->width + 1U) / 2U) + x / 2U];
|
|
const unsigned shift = (x & 1U) ? 0U : 4U;
|
|
const uint8_t previous = (byte >> shift) & 15U;
|
|
grayPixelCount -= previous != 0U && previous != 15U;
|
|
grayPixelCount += luminance != 0U && luminance != 15U;
|
|
byte = (byte & ~(15U << shift)) | (luminance << shift);
|
|
}
|
|
|
|
#if defined(ARCH_ESP32) && defined(__GNUC__)
|
|
// Favor speed for the packed-pixel blit without changing panel timing.
|
|
__attribute__((optimize("O3")))
|
|
#endif
|
|
void InkHUD::Renderer::writeGrayBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap,
|
|
uint16_t bitmapStride, uint16_t sourceX, uint16_t sourceY)
|
|
{
|
|
if (!bitmap || !w || !h)
|
|
return;
|
|
if (!grayBuffer && driver->supportsGrayscale()) {
|
|
grayBuffer = static_cast<uint8_t *>(
|
|
heap_caps_malloc(((driver->width + 1U) / 2U) * driver->height, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
|
if (grayBuffer) {
|
|
memset(grayBuffer, 0xFF, ((driver->width + 1U) / 2U) * driver->height);
|
|
for (uint16_t y = 0; y < driver->height; ++y)
|
|
for (uint16_t x = 0; x < driver->width; ++x)
|
|
setGrayPixel(x, y, (imageBuffer[y * imageBufferWidth + x / 8U] & (0x80U >> (x & 7U))) ? 15U : 0U);
|
|
}
|
|
}
|
|
constexpr uint8_t bayer[] = {0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5};
|
|
const int32_t firstX = std::max<int32_t>(0, -static_cast<int32_t>(screenX));
|
|
const int32_t firstY = std::max<int32_t>(0, -static_cast<int32_t>(screenY));
|
|
const int32_t endX = std::min<int32_t>(w, static_cast<int32_t>(width()) - screenX);
|
|
const int32_t endY = std::min<int32_t>(h, static_cast<int32_t>(height()) - screenY);
|
|
if (firstX >= endX || firstY >= endY)
|
|
return;
|
|
const uint16_t monoStride = imageBufferWidth;
|
|
const uint16_t grayStride = (driver->width + 1U) / 2U;
|
|
uint8_t *const mono = imageBuffer;
|
|
uint8_t *const gray = grayBuffer;
|
|
uint32_t intermediatePixels = grayPixelCount;
|
|
static constexpr auto intermediateCount = [] {
|
|
std::array<uint8_t, 256> counts{};
|
|
for (unsigned pair = 0; pair < counts.size(); ++pair)
|
|
counts[pair] = ((pair >> 4U) != 0U && (pair >> 4U) != 15U) + ((pair & 15U) != 0U && (pair & 15U) != 15U);
|
|
return counts;
|
|
}();
|
|
#if INKHUD_BOOKREADER_PERF_LOG
|
|
const uint32_t blitStartMs = millis();
|
|
uint32_t inkHistogram[16] = {};
|
|
uint32_t blitHash = 2166136261U;
|
|
#endif
|
|
const auto writePixel = [&](uint8_t *monoRow, uint8_t *grayRow, int16_t x, int32_t sx, int32_t sy, uint8_t ink) {
|
|
#if INKHUD_BOOKREADER_PERF_LOG
|
|
++inkHistogram[ink];
|
|
blitHash = (blitHash ^ ink) * 16777619U;
|
|
#endif
|
|
if (grayRow) {
|
|
uint8_t &byte = grayRow[x / 2U];
|
|
const unsigned shift = (x & 1U) ? 0U : 4U;
|
|
const uint8_t previous = (byte >> shift) & 15U;
|
|
intermediatePixels -= previous != 0U && previous != 15U;
|
|
intermediatePixels += ink != 0U && ink != 15U;
|
|
byte = (byte & ~(15U << shift)) | ((15U - ink) << shift);
|
|
} else {
|
|
const bool black = ink * 32U > (bayer[((screenY + sy) & 3U) * 4U + ((screenX + sx) & 3U)] * 2U + 1U) * 15U;
|
|
bitWrite(monoRow[x / 8U], 7U - (x & 7U), !black);
|
|
}
|
|
};
|
|
const auto writeGraySpan = [&](uint8_t *row, int16_t x, int step, const uint8_t *source, int count, int sourceStride) {
|
|
int i = 0;
|
|
if ((x & 1U) != (step < 0)) {
|
|
writePixel(nullptr, row, x, 0, 0, source[0]);
|
|
x += step;
|
|
++i;
|
|
}
|
|
for (; i + 1 < count; i += 2, x += 2 * step) {
|
|
const uint8_t first = source[i * sourceStride];
|
|
const uint8_t second = source[(i + 1) * sourceStride];
|
|
#if INKHUD_BOOKREADER_PERF_LOG
|
|
++inkHistogram[first];
|
|
++inkHistogram[second];
|
|
blitHash = ((blitHash ^ first) * 16777619U ^ second) * 16777619U;
|
|
#endif
|
|
uint8_t &byte = row[x / 2U];
|
|
const uint8_t packedInk = step > 0 ? (first << 4U) | second : (second << 4U) | first;
|
|
intermediatePixels -= intermediateCount[byte];
|
|
intermediatePixels += intermediateCount[packedInk];
|
|
byte = packedInk ^ 0xFFU;
|
|
}
|
|
if (i < count)
|
|
writePixel(nullptr, row, x, 0, 0, source[i * sourceStride]);
|
|
};
|
|
// Transpose small SRAM tiles so rotated output does not thrash the PSRAM cache.
|
|
constexpr int32_t tileSize = 16;
|
|
uint8_t inks[tileSize][tileSize];
|
|
const uint8_t rotation = settings->rotation;
|
|
for (int32_t ty = firstY; ty < endY; ty += tileSize) {
|
|
const int32_t tileH = std::min(tileSize, endY - ty);
|
|
for (int32_t tx = firstX; tx < endX; tx += tileSize) {
|
|
const int32_t tileW = std::min(tileSize, endX - tx);
|
|
for (int32_t sy = 0; sy < tileH; ++sy) {
|
|
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + ty + sy) * bitmapStride;
|
|
uint16_t srcX = sourceX + tx;
|
|
int32_t sx = 0;
|
|
if (srcX & 1U) {
|
|
inks[sy][sx++] = srcRow[srcX++ / 2U] & 15U;
|
|
}
|
|
for (; sx + 1 < tileW; sx += 2, srcX += 2) {
|
|
const uint8_t pair = srcRow[srcX / 2U];
|
|
inks[sy][sx] = pair >> 4U;
|
|
inks[sy][sx + 1] = pair & 15U;
|
|
}
|
|
if (sx < tileW)
|
|
inks[sy][sx] = srcRow[srcX / 2U] >> 4U;
|
|
}
|
|
if (rotation & 1U) {
|
|
for (int32_t sx = 0; sx < tileW; ++sx) {
|
|
int16_t x = screenX + tx + sx;
|
|
int16_t y = screenY + ty;
|
|
rotatePixelCoords(&x, &y);
|
|
uint8_t *const monoRow = mono + static_cast<size_t>(y) * monoStride;
|
|
uint8_t *const grayRow = gray ? gray + static_cast<size_t>(y) * grayStride : nullptr;
|
|
if (grayRow) {
|
|
writeGraySpan(grayRow, x, rotation == 1 ? -1 : 1, reinterpret_cast<const uint8_t *>(inks) + sx, tileH,
|
|
tileSize);
|
|
continue;
|
|
}
|
|
for (int32_t sy = 0; sy < tileH; ++sy) {
|
|
writePixel(monoRow, grayRow, x, tx + sx, ty + sy, inks[sy][sx]);
|
|
x += rotation == 1 ? -1 : 1;
|
|
}
|
|
}
|
|
} else {
|
|
for (int32_t sy = 0; sy < tileH; ++sy) {
|
|
int16_t x = screenX + tx;
|
|
int16_t y = screenY + ty + sy;
|
|
rotatePixelCoords(&x, &y);
|
|
uint8_t *const monoRow = mono + static_cast<size_t>(y) * monoStride;
|
|
uint8_t *const grayRow = gray ? gray + static_cast<size_t>(y) * grayStride : nullptr;
|
|
if (grayRow) {
|
|
writeGraySpan(grayRow, x, rotation == 0 ? 1 : -1, reinterpret_cast<const uint8_t *>(inks) + sy * tileSize,
|
|
tileW, 1);
|
|
continue;
|
|
}
|
|
for (int32_t sx = 0; sx < tileW; ++sx) {
|
|
writePixel(monoRow, grayRow, x, tx + sx, ty + sy, inks[sy][sx]);
|
|
x += rotation == 0 ? 1 : -1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
grayPixelCount = intermediatePixels;
|
|
if (gray)
|
|
monoBufferNeedsSync = true;
|
|
#if INKHUD_BOOKREADER_PERF_LOG
|
|
LOG_INFO("[BookReaderDraw] blit xy=%d,%d size=%ux%u src=%u,%u "
|
|
"clip=%ld,%ld-%ld,%ld rotation=%u gray=%u hash=%08lx ms=%lu",
|
|
screenX, screenY, w, h, sourceX, sourceY, static_cast<long>(firstX), static_cast<long>(firstY),
|
|
static_cast<long>(endX), static_cast<long>(endY), rotation, gray != nullptr, static_cast<unsigned long>(blitHash),
|
|
static_cast<unsigned long>(millis() - blitStartMs));
|
|
LOG_INFO("[BookReaderDraw] image ink bins "
|
|
"white-to-black=%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,%lu,"
|
|
"%lu,%lu,%lu",
|
|
inkHistogram[0], inkHistogram[1], inkHistogram[2], inkHistogram[3], inkHistogram[4], inkHistogram[5],
|
|
inkHistogram[6], inkHistogram[7], inkHistogram[8], inkHistogram[9], inkHistogram[10], inkHistogram[11],
|
|
inkHistogram[12], inkHistogram[13], inkHistogram[14], inkHistogram[15]);
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Fast path for blitting MSB-first 1-bpp bitmaps into imageBuffer.
|
|
void InkHUD::Renderer::writeBitmap(int16_t screenX, int16_t screenY, uint16_t w, uint16_t h, const uint8_t *bitmap,
|
|
uint16_t bitmapStride, uint16_t sourceX, uint16_t sourceY)
|
|
{
|
|
if (!bitmap || w == 0 || h == 0) {
|
|
return;
|
|
}
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
if (grayBuffer) {
|
|
for (uint16_t y = 0; y < h; ++y)
|
|
for (uint16_t x = 0; x < w; ++x) {
|
|
const uint16_t sx = sourceX + x;
|
|
const bool black = bitmap[(sourceY + y) * bitmapStride + sx / 8U] & (0x80U >> (sx & 7U));
|
|
if (screenX + x >= 0 && screenY + y >= 0 && screenX + x < width() && screenY + y < height())
|
|
handlePixel(screenX + x, screenY + y, black ? BLACK : WHITE);
|
|
}
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
switch (settings->rotation) {
|
|
case 0: {
|
|
// No rotation: iterate row-first for sequential imageBuffer access.
|
|
for (uint16_t sy = 0; sy < h; sy++) {
|
|
const int16_t bufY = static_cast<int16_t>(screenY + sy);
|
|
if (bufY < 0 || static_cast<uint16_t>(bufY) >= imageBufferHeight) {
|
|
continue;
|
|
}
|
|
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + sy) * bitmapStride;
|
|
uint8_t *dstRow = imageBuffer + static_cast<size_t>(bufY) * imageBufferWidth;
|
|
for (uint16_t sx = 0; sx < w; sx++) {
|
|
const int16_t bufX = static_cast<int16_t>(screenX + sx);
|
|
if (bufX < 0 || static_cast<uint16_t>(bufX) >= driver->width) {
|
|
continue;
|
|
}
|
|
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
|
const bool black = (srcRow[srcX >> 3] & (static_cast<uint8_t>(0x80U) >> (srcX & 7U))) != 0U;
|
|
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (static_cast<uint16_t>(bufX) & 7U);
|
|
if (black) {
|
|
dstRow[static_cast<uint16_t>(bufX) >> 3] &= static_cast<uint8_t>(~bitMask);
|
|
} else {
|
|
dstRow[static_cast<uint16_t>(bufX) >> 3] |= bitMask;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 1: {
|
|
// 90 degrees CW: iterate column-first so each inner loop writes one
|
|
// physical row.
|
|
const uint16_t dW = driver->width;
|
|
for (uint16_t sx = 0; sx < w; sx++) {
|
|
const int16_t physRow = static_cast<int16_t>(screenX + sx);
|
|
if (physRow < 0 || static_cast<uint16_t>(physRow) >= imageBufferHeight) {
|
|
continue;
|
|
}
|
|
uint8_t *dstRow = imageBuffer + static_cast<size_t>(physRow) * imageBufferWidth;
|
|
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
|
const uint8_t srcMask = static_cast<uint8_t>(0x80U) >> (srcX & 7U);
|
|
const uint16_t srcByteOff = srcX >> 3;
|
|
for (uint16_t sy = 0; sy < h; sy++) {
|
|
const int32_t physColI =
|
|
static_cast<int32_t>(dW - 1U) - (static_cast<int32_t>(screenY) + static_cast<int32_t>(sy));
|
|
if (physColI < 0 || static_cast<uint32_t>(physColI) >= driver->width) {
|
|
continue;
|
|
}
|
|
const uint16_t physCol = static_cast<uint16_t>(physColI);
|
|
const bool black = (bitmap[static_cast<size_t>(sourceY + sy) * bitmapStride + srcByteOff] & srcMask) != 0U;
|
|
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (physCol & 7U);
|
|
if (black) {
|
|
dstRow[physCol >> 3] &= static_cast<uint8_t>(~bitMask);
|
|
} else {
|
|
dstRow[physCol >> 3] |= bitMask;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 2: {
|
|
// 180 degrees: iterate row-first for sequential physical row access.
|
|
const uint32_t physicalWidth = driver->width;
|
|
for (uint16_t sy = 0; sy < h; sy++) {
|
|
const int32_t physRowI =
|
|
static_cast<int32_t>(imageBufferHeight - 1U) - (static_cast<int32_t>(screenY) + static_cast<int32_t>(sy));
|
|
if (physRowI < 0 || static_cast<uint32_t>(physRowI) >= static_cast<uint32_t>(imageBufferHeight)) {
|
|
continue;
|
|
}
|
|
uint8_t *dstRow = imageBuffer + static_cast<size_t>(physRowI) * imageBufferWidth;
|
|
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + sy) * bitmapStride;
|
|
for (uint16_t sx = 0; sx < w; sx++) {
|
|
const int32_t physColI =
|
|
static_cast<int32_t>(physicalWidth - 1U) - (static_cast<int32_t>(screenX) + static_cast<int32_t>(sx));
|
|
if (physColI < 0 || static_cast<uint32_t>(physColI) >= physicalWidth) {
|
|
continue;
|
|
}
|
|
const uint16_t physCol = static_cast<uint16_t>(physColI);
|
|
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
|
const bool black = (srcRow[srcX >> 3] & (static_cast<uint8_t>(0x80U) >> (srcX & 7U))) != 0U;
|
|
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (physCol & 7U);
|
|
if (black) {
|
|
dstRow[physCol >> 3] &= static_cast<uint8_t>(~bitMask);
|
|
} else {
|
|
dstRow[physCol >> 3] |= bitMask;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 3: {
|
|
// 270 degrees CW: iterate column-first so each inner loop writes one
|
|
// physical row.
|
|
for (uint16_t sx = 0; sx < w; sx++) {
|
|
const int32_t physRowI =
|
|
static_cast<int32_t>(imageBufferHeight - 1U) - (static_cast<int32_t>(screenX) + static_cast<int32_t>(sx));
|
|
if (physRowI < 0 || static_cast<uint32_t>(physRowI) >= static_cast<uint32_t>(imageBufferHeight)) {
|
|
continue;
|
|
}
|
|
const uint16_t physRow = static_cast<uint16_t>(physRowI);
|
|
uint8_t *dstRow = imageBuffer + static_cast<size_t>(physRow) * imageBufferWidth;
|
|
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
|
const uint8_t srcMask = static_cast<uint8_t>(0x80U) >> (srcX & 7U);
|
|
const uint16_t srcByteOff = srcX >> 3;
|
|
for (uint16_t sy = 0; sy < h;) {
|
|
const int16_t physCol = static_cast<int16_t>(screenY + sy);
|
|
if (physCol < 0 || static_cast<uint16_t>(physCol) >= driver->width) {
|
|
++sy;
|
|
continue;
|
|
}
|
|
// Transpose eight source rows into one destination byte without
|
|
// changing any pixels.
|
|
if ((physCol & 7) == 0 && static_cast<uint32_t>(sy) + 8U <= h &&
|
|
static_cast<uint32_t>(physCol) + 8U <= driver->width) {
|
|
uint8_t blackBits = 0;
|
|
for (uint16_t row = 0; row < 8U; ++row) {
|
|
if (bitmap[static_cast<size_t>(sourceY + sy + row) * bitmapStride + srcByteOff] & srcMask)
|
|
blackBits |= static_cast<uint8_t>(0x80U >> row);
|
|
}
|
|
dstRow[static_cast<uint16_t>(physCol) >> 3] = static_cast<uint8_t>(~blackBits);
|
|
sy += 8U;
|
|
continue;
|
|
}
|
|
const bool black = (bitmap[static_cast<size_t>(sourceY + sy) * bitmapStride + srcByteOff] & srcMask) != 0U;
|
|
const uint8_t bitMask = static_cast<uint8_t>(0x80U) >> (static_cast<uint16_t>(physCol) & 7U);
|
|
if (black) {
|
|
dstRow[physCol >> 3] &= static_cast<uint8_t>(~bitMask);
|
|
} else {
|
|
dstRow[physCol >> 3] |= bitMask;
|
|
}
|
|
++sy;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
// Unexpected rotation: fall back to per-pixel handlePixel.
|
|
for (uint16_t sy = 0; sy < h; sy++) {
|
|
const uint8_t *srcRow = bitmap + static_cast<size_t>(sourceY + sy) * bitmapStride;
|
|
for (uint16_t sx = 0; sx < w; sx++) {
|
|
const uint16_t srcX = static_cast<uint16_t>(sourceX + sx);
|
|
const bool black = (srcRow[srcX >> 3] & (static_cast<uint8_t>(0x80U) >> (srcX & 7U))) != 0U;
|
|
handlePixel(static_cast<int16_t>(screenX + sx), static_cast<int16_t>(screenY + sy), black ? BLACK : WHITE);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
// Set a flag, which will be picked up by runOnce, ASAP.
|
|
// Quite likely, multiple applets will all want to respond to one event
|
|
// (Observable, etc) Each affected applet can independently call
|
|
// requestUpdate(), and all share the one opportunity to render, at next runOnce
|
|
void InkHUD::Renderer::requestUpdate(bool all)
|
|
{
|
|
requested = true;
|
|
renderAll |= all;
|
|
|
|
// We will run the thread as soon as we loop(),
|
|
// after all Applets have had a chance to observe whatever event set this off
|
|
OSThread::setIntervalFromNow(0);
|
|
OSThread::enabled = true;
|
|
runASAP = true;
|
|
}
|
|
|
|
// requestUpdate will not actually update if no requests were made by applets
|
|
// which are actually visible This can occur, because applets requestUpdate even
|
|
// from the background, in case the user's autoshow settings permit them to be
|
|
// moved to foreground. Sometimes, however, we will want to trigger a display
|
|
// update manually, in the absence of any sort of applet event Display health,
|
|
// for example. In these situations, we use forceUpdate
|
|
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool all, bool async)
|
|
{
|
|
requested = true;
|
|
forced = true;
|
|
renderAll |= all;
|
|
displayHealth.forceUpdateType(type);
|
|
|
|
// Normally, we need to start the timer, in case the display is busy and we
|
|
// briefly defer the update
|
|
if (async) {
|
|
// We will run the thread as soon as we loop(),
|
|
// after all Applets have had a chance to observe whatever event set this
|
|
// off
|
|
OSThread::setIntervalFromNow(0);
|
|
OSThread::enabled = true;
|
|
runASAP = true;
|
|
}
|
|
|
|
// If the update is *not* asynchronous, we begin the render process directly
|
|
// here so that it can block code flow while running
|
|
else
|
|
render(false);
|
|
}
|
|
|
|
// Wait for any in-progress display update to complete before continuing
|
|
void InkHUD::Renderer::awaitUpdate()
|
|
{
|
|
if (driver->busy()) {
|
|
LOG_INFO("Waiting for display");
|
|
driver->await(); // Wait here for update to complete
|
|
}
|
|
}
|
|
|
|
// Set a ready-to-draw pixel into the image buffer
|
|
// All rotations / translations have already taken place: this buffer data is
|
|
// formatted ready for the driver
|
|
void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c)
|
|
{
|
|
rotatePixelCoords(&x, &y);
|
|
|
|
uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte
|
|
uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant)
|
|
// is leftmost pixel of byte.
|
|
|
|
bitWrite(imageBuffer[byteNum], bitNum, c);
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
if (grayBuffer)
|
|
setGrayPixel(x, y, c == WHITE ? 15U : 0U);
|
|
#endif
|
|
}
|
|
|
|
// Width of the display, relative to rotation
|
|
uint16_t InkHUD::Renderer::width()
|
|
{
|
|
if (settings->rotation % 2)
|
|
return driver->height;
|
|
else
|
|
return driver->width;
|
|
}
|
|
|
|
// Height of the display, relative to rotation
|
|
uint16_t InkHUD::Renderer::height()
|
|
{
|
|
if (settings->rotation % 2)
|
|
return driver->width;
|
|
else
|
|
return driver->height;
|
|
}
|
|
|
|
// Runs at regular intervals
|
|
// - postponing render: until next loop(), allowing all applets to be notified
|
|
// of some Mesh event before render
|
|
// - queuing another render: while one is already is progress
|
|
int32_t InkHUD::Renderer::runOnce()
|
|
{
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
maintainRenderActivity();
|
|
#endif
|
|
|
|
// If an applet asked to render, and hardware is able, lets try now
|
|
if (requested && !driver->busy()) {
|
|
render();
|
|
}
|
|
|
|
// If our render() call failed, try again shortly
|
|
// otherwise, stop our thread until next update due
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
if (requested || keepAwakeUntilUpdateComplete)
|
|
#else
|
|
if (requested)
|
|
#endif
|
|
return 25UL;
|
|
else
|
|
return OSThread::disable();
|
|
}
|
|
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
void InkHUD::Renderer::maintainRenderActivity()
|
|
{
|
|
if (keepAwakeUntilUpdateComplete) {
|
|
// Give the reader a full idle timeout after the physical refresh finishes.
|
|
powerFSM.trigger(EVENT_INPUT);
|
|
keepAwakeUntilUpdateComplete = driver->busy();
|
|
BookReaderApplet::setDisplayRefreshActive(keepAwakeUntilUpdateComplete);
|
|
#if INKHUD_BOOKREADER_PERF_LOG
|
|
if (!keepAwakeUntilUpdateComplete)
|
|
LOG_INFO("[BookReaderPerf] refresh END visible=1 ms=%lu timeoutRestarted=1",
|
|
static_cast<unsigned long>(millis() - readerRefreshStartMs));
|
|
#endif
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Applies the system-wide rotation to pixel positions
|
|
// This step is applied to image data which has already been translated by a
|
|
// Tile object This is the final step before the pixel is placed into the image
|
|
// buffer No return: values of the *x and *y parameters are modified by the
|
|
// method
|
|
void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y)
|
|
{
|
|
// Apply a global rotation to pixel locations
|
|
int16_t x1 = 0;
|
|
int16_t y1 = 0;
|
|
switch (settings->rotation) {
|
|
case 0:
|
|
x1 = *x;
|
|
y1 = *y;
|
|
break;
|
|
case 1:
|
|
x1 = (driver->width - 1) - *y;
|
|
y1 = *x;
|
|
break;
|
|
case 2:
|
|
x1 = (driver->width - 1) - *x;
|
|
y1 = (driver->height - 1) - *y;
|
|
break;
|
|
case 3:
|
|
x1 = *y;
|
|
y1 = (driver->height - 1) - *x;
|
|
break;
|
|
}
|
|
*x = x1;
|
|
*y = y1;
|
|
}
|
|
|
|
// Make an attempt to gather image data from some / all applets, and update the
|
|
// display Might not be possible right now, if update already is progress.
|
|
void InkHUD::Renderer::render(bool async)
|
|
{
|
|
// Make sure the display is ready for a new update
|
|
if (async) {
|
|
// Previous update still running, Will try again shortly, via runOnce()
|
|
if (driver->busy())
|
|
return;
|
|
} else {
|
|
// Wait here for previous update to complete
|
|
driver->await();
|
|
}
|
|
|
|
// Determine if a system applet has requested exclusive rights to request an
|
|
// update, or exclusive rights to render
|
|
checkLocks();
|
|
|
|
// (Potentially) change applet to display new info,
|
|
// then check if this newly displayed applet makes a pending notification
|
|
// redundant
|
|
inkhud->autoshow();
|
|
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
// Consume this request before drawing; an applet may queue its next loading
|
|
// pass in onRender().
|
|
requested = false;
|
|
#endif
|
|
|
|
// If an update is justified.
|
|
// We don't know this until after autoshow has run, as new applets may now be
|
|
// in foreground
|
|
if (shouldUpdate()) {
|
|
|
|
// Decide which technique the display will use to change image
|
|
// Done early, as rendering resets the Applets' requested types
|
|
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
|
|
|
|
// Render the new image
|
|
if (renderAll)
|
|
clearBuffer();
|
|
renderUserApplets();
|
|
renderPlaceholders();
|
|
renderSystemApplets();
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
bool readerVisible = false;
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
if (!lockRendering)
|
|
for (Applet *applet : inkhud->userApplets)
|
|
if (applet && applet->isActive() && applet->isForeground() && applet->keepScreenAwakeDuringRender()) {
|
|
readerVisible = true;
|
|
break;
|
|
}
|
|
#endif
|
|
const bool useGrayscale = grayBuffer && (grayPixelCount || readerVisible);
|
|
// Grayscale frames do not consume the monochrome buffer; rebuild it only on a monochrome transition.
|
|
if (!useGrayscale && grayBuffer && monoBufferNeedsSync) {
|
|
[[maybe_unused]] const uint32_t syncStartMs = millis();
|
|
const size_t grayStride = (driver->width + 1U) / 2U;
|
|
for (uint16_t y = 0; y < driver->height; ++y) {
|
|
const uint8_t *row = grayBuffer + static_cast<size_t>(y) * grayStride;
|
|
uint8_t *out = imageBuffer + static_cast<size_t>(y) * imageBufferWidth;
|
|
for (uint16_t bx = 0; bx < imageBufferWidth; ++bx) {
|
|
uint8_t white = 0xFFU;
|
|
for (uint16_t bit = 0; bit < 8U && bx * 8U + bit < driver->width; ++bit) {
|
|
const uint16_t x = bx * 8U + bit;
|
|
if (((row[x / 2U] >> ((x & 1U) ? 0U : 4U)) & 15U) == 0U)
|
|
white &= ~(0x80U >> bit);
|
|
}
|
|
out[bx] = white;
|
|
}
|
|
}
|
|
monoBufferNeedsSync = false;
|
|
#if INKHUD_BOOKREADER_PERF_LOG
|
|
LOG_INFO("[BookReaderPerf] monochrome buffer restored ms=%lu", static_cast<unsigned long>(millis() - syncStartMs));
|
|
#endif
|
|
}
|
|
if (useGrayscale && updateType != Drivers::EInk::UpdateTypes::FULL) {
|
|
displayHealth.forceUpdateType(Drivers::EInk::UpdateTypes::FULL);
|
|
updateType = displayHealth.decideUpdateType();
|
|
}
|
|
#endif
|
|
|
|
// Invert Buffer if set by user
|
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
|
for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) {
|
|
imageBuffer[i] = ~imageBuffer[i];
|
|
}
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
if (useGrayscale)
|
|
for (size_t i = 0; i < ((driver->width + 1U) / 2U) * driver->height; ++i)
|
|
grayBuffer[i] = ~grayBuffer[i];
|
|
#endif
|
|
}
|
|
|
|
// Tell display to begin process of drawing new image
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
if (keepAwakeUntilUpdateComplete)
|
|
BookReaderApplet::setDisplayRefreshActive(true);
|
|
#endif
|
|
LOG_INFO("Updating display");
|
|
#if INKHUD_BOOKREADER_GRAYSCALE && INKHUD_BOOKREADER_PERF_LOG
|
|
static uint32_t frameId = 0;
|
|
LOG_INFO("[BookReaderDraw] submit frame=%lu type=%u gray=%u grayPixels=%lu "
|
|
"reader=%u renderAll=%u locked=%u inverted=%u",
|
|
static_cast<unsigned long>(++frameId), static_cast<unsigned>(updateType), useGrayscale,
|
|
static_cast<unsigned long>(grayPixelCount), readerVisible, renderAll, lockRendering,
|
|
config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
|
#endif
|
|
#if defined(MESHTASTIC_INCLUDE_INKHUD_BOOKREADER) && INKHUD_BOOKREADER_PERF_LOG
|
|
if (keepAwakeUntilUpdateComplete) {
|
|
readerRefreshStartMs = millis();
|
|
LOG_INFO("[BookReaderPerf] refresh BEGIN type=%u async=%u", static_cast<unsigned>(updateType),
|
|
static_cast<unsigned>(async));
|
|
}
|
|
#endif
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
if (useGrayscale)
|
|
driver->updateGrayscale(grayBuffer);
|
|
else
|
|
#endif
|
|
driver->update(imageBuffer, updateType);
|
|
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
// Retained overlay renders must start from the original, uninverted pixels.
|
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
|
driver->await();
|
|
for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i)
|
|
imageBuffer[i] = ~imageBuffer[i];
|
|
if (useGrayscale)
|
|
for (size_t i = 0; i < ((driver->width + 1U) / 2U) * driver->height; ++i)
|
|
grayBuffer[i] = ~grayBuffer[i];
|
|
}
|
|
#endif
|
|
|
|
// If not async, wait here until the update is complete
|
|
if (!async)
|
|
driver->await();
|
|
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
maintainRenderActivity();
|
|
if (keepAwakeUntilUpdateComplete) {
|
|
OSThread::enabled = true;
|
|
OSThread::setIntervalFromNow(25UL);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Our part is done now.
|
|
// If update is async, the display hardware is still performing the update
|
|
// process, but that's all handled by NicheGraphics::Drivers::EInk
|
|
|
|
// Tidy up, ready for a new request
|
|
#ifndef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
requested = false;
|
|
#endif
|
|
forced = false;
|
|
renderAll = false;
|
|
}
|
|
|
|
// Manually fill the image buffer with WHITE
|
|
// Clears any old drawing
|
|
// Note: benchmarking revealed that this is *much* faster than setting pixels
|
|
// individually So much so that it's more efficient to re-render all applets,
|
|
// rather than rendering selectively, and manually blanking a portion of the
|
|
// display
|
|
void InkHUD::Renderer::clearBuffer()
|
|
{
|
|
#if INKHUD_BOOKREADER_GRAYSCALE && INKHUD_BOOKREADER_PERF_LOG
|
|
LOG_INFO("[BookReaderDraw] clear framebuffer gray=%u", grayBuffer != nullptr);
|
|
#endif
|
|
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
if (grayBuffer)
|
|
memset(grayBuffer, 0xFF, ((driver->width + 1U) / 2U) * driver->height);
|
|
grayPixelCount = 0;
|
|
monoBufferNeedsSync = false;
|
|
#endif
|
|
}
|
|
|
|
// Manually clear the pixels below a tile
|
|
void InkHUD::Renderer::clearTile(Tile *t)
|
|
{
|
|
// Rotate the tile dimensions
|
|
int16_t left = 0;
|
|
int16_t top = 0;
|
|
uint16_t tileW = 0;
|
|
uint16_t tileH = 0;
|
|
switch (settings->rotation) {
|
|
case 0:
|
|
left = t->getLeft();
|
|
top = t->getTop();
|
|
tileW = t->getWidth();
|
|
tileH = t->getHeight();
|
|
break;
|
|
case 1:
|
|
left = driver->width - (t->getTop() + t->getHeight());
|
|
top = t->getLeft();
|
|
tileW = t->getHeight();
|
|
tileH = t->getWidth();
|
|
break;
|
|
case 2:
|
|
left = driver->width - (t->getLeft() + t->getWidth());
|
|
top = driver->height - (t->getTop() + t->getHeight());
|
|
tileW = t->getWidth();
|
|
tileH = t->getHeight();
|
|
break;
|
|
case 3:
|
|
left = t->getTop();
|
|
top = driver->height - (t->getLeft() + t->getWidth());
|
|
tileW = t->getHeight();
|
|
tileH = t->getWidth();
|
|
break;
|
|
}
|
|
|
|
// Calculate the bounds to clear
|
|
uint16_t xStart = (left < 0) ? 0 : left;
|
|
uint16_t yStart = (top < 0) ? 0 : top;
|
|
if (xStart >= driver->width || yStart >= driver->height || left + tileW < 0 || top + tileH < 0)
|
|
return; // the box is completely off the screen
|
|
uint16_t xEnd = left + tileW;
|
|
uint16_t yEnd = top + tileH;
|
|
if (xEnd > driver->width)
|
|
xEnd = driver->width;
|
|
if (yEnd > driver->height)
|
|
yEnd = driver->height;
|
|
if (xStart >= xEnd || yStart >= yEnd)
|
|
return;
|
|
#if INKHUD_BOOKREADER_GRAYSCALE && INKHUD_BOOKREADER_PERF_LOG
|
|
LOG_INFO("[BookReaderDraw] clear tile xy=%u,%u-%u,%u gray=%u", xStart, yStart, xEnd, yEnd, grayBuffer != nullptr);
|
|
#endif
|
|
#if INKHUD_BOOKREADER_GRAYSCALE
|
|
if (grayBuffer) {
|
|
if (xStart == 0 && yStart == 0 && xEnd == driver->width && yEnd == driver->height) {
|
|
memset(grayBuffer, 0xFF, ((driver->width + 1U) / 2U) * driver->height);
|
|
grayPixelCount = 0;
|
|
} else {
|
|
const size_t stride = (driver->width + 1U) / 2U;
|
|
const size_t firstByte = (xStart + 1U) / 2U;
|
|
const size_t endByte = xEnd / 2U;
|
|
uint32_t clearedGrayPixels = 0;
|
|
for (uint16_t y = yStart; y < yEnd; ++y) {
|
|
if ((xStart & 1U) && xEnd > xStart)
|
|
setGrayPixel(xStart, y, 15U);
|
|
if ((xEnd & 1U) && xEnd > xStart)
|
|
setGrayPixel(xEnd - 1U, y, 15U);
|
|
uint8_t *row = grayBuffer + static_cast<size_t>(y) * stride;
|
|
for (size_t x = firstByte; x < endByte; ++x) {
|
|
const uint8_t pair = row[x];
|
|
const uint8_t high = pair >> 4U;
|
|
const uint8_t low = pair & 15U;
|
|
clearedGrayPixels += high != 0U && high != 15U;
|
|
clearedGrayPixels += low != 0U && low != 15U;
|
|
}
|
|
if (endByte > firstByte)
|
|
memset(row + firstByte, 0xFF, endByte - firstByte);
|
|
}
|
|
grayPixelCount -= clearedGrayPixels;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Clear the pixels
|
|
if (xStart == 0 && xEnd == driver->width) { // full width box is easier to clear
|
|
memset(imageBuffer + (yStart * imageBufferWidth), 0xFF, (yEnd - yStart) * imageBufferWidth);
|
|
} else {
|
|
const uint16_t byteStart = xStart / 8U;
|
|
const uint16_t byteEnd = (xEnd - 1U) / 8U;
|
|
const uint8_t leadingByte = 0xFFU >> (xStart & 7U);
|
|
const uint8_t trailingByte = 0xFFU << (7U - ((xEnd - 1U) & 7U));
|
|
for (size_t i = static_cast<size_t>(yStart) * imageBufferWidth; i < static_cast<size_t>(yEnd) * imageBufferWidth;
|
|
i += imageBufferWidth) {
|
|
if (byteStart == byteEnd)
|
|
imageBuffer[i + byteStart] |= leadingByte & trailingByte;
|
|
else {
|
|
imageBuffer[i + byteStart] |= leadingByte;
|
|
if (byteStart + 1U < byteEnd)
|
|
memset(imageBuffer + i + byteStart + 1U, 0xFF, byteEnd - byteStart - 1U);
|
|
imageBuffer[i + byteEnd] |= trailingByte;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void InkHUD::Renderer::checkLocks()
|
|
{
|
|
lockRendering = nullptr;
|
|
lockRequests = nullptr;
|
|
|
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
|
if (!lockRendering && sa->lockRendering && sa->isForeground()) {
|
|
lockRendering = sa;
|
|
}
|
|
if (!lockRequests && sa->lockRequests && sa->isForeground()) {
|
|
lockRequests = sa;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool InkHUD::Renderer::shouldUpdate()
|
|
{
|
|
bool should = false;
|
|
|
|
// via forceUpdate
|
|
should |= forced;
|
|
|
|
// via a system applet (which has locked update requests)
|
|
if (lockRequests) {
|
|
should |= lockRequests->wantsToRender();
|
|
return should; // Early exit - no other requests considered
|
|
}
|
|
|
|
// via system applet (not locked)
|
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
|
if (sa->wantsToRender() // This applet requested
|
|
&& sa->isForeground()) // This applet is currently shown
|
|
{
|
|
should = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// via user applet
|
|
for (Applet *ua : inkhud->userApplets) {
|
|
if (ua // Tile has valid applet
|
|
&& ua->wantsToRender() // This applet requested display update
|
|
&& ua->isForeground()) // This applet is currently shown
|
|
{
|
|
should = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return should;
|
|
}
|
|
|
|
// Determine which type of E-Ink update the display will perform, to change the
|
|
// image. Considers the needs of the various applets, then weighs against
|
|
// display health. An update type specified by forceUpdate will be granted with
|
|
// no further questioning.
|
|
Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
|
|
{
|
|
// Ask applets which update type they would prefer
|
|
// Some update types take priority over others
|
|
|
|
// No need to consider the "requests" if somebody already forced an update
|
|
if (!forced) {
|
|
// User applets
|
|
for (Applet *ua : inkhud->userApplets) {
|
|
if (ua && ua->isForeground() && (ua->wantsToRender() || renderAll))
|
|
displayHealth.requestUpdateType(ua->wantsUpdateType());
|
|
}
|
|
// System Applets
|
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
|
if (sa && sa->isForeground() && (sa->wantsToRender() || sa->alwaysRender || renderAll))
|
|
displayHealth.requestUpdateType(sa->wantsUpdateType());
|
|
}
|
|
}
|
|
|
|
return displayHealth.decideUpdateType();
|
|
}
|
|
|
|
// Run the drawing operations of any user applets which are currently displayed
|
|
// Pixel output is placed into the framebuffer, ready for handoff to the EInk
|
|
// driver
|
|
void InkHUD::Renderer::renderUserApplets()
|
|
{
|
|
// Don't render user applets if a system applet has demanded the whole display
|
|
// to itself
|
|
if (lockRendering)
|
|
return;
|
|
|
|
// Render any user applets which are currently visible
|
|
for (Applet *ua : inkhud->userApplets) {
|
|
if (ua && ua->isActive() && ua->isForeground() && (ua->wantsToRender() || renderAll)) {
|
|
|
|
// Clear the tile unless the applet wants to draw over its previous render
|
|
// or everything is getting re-rendered anyways
|
|
if (ua->wantsFullRender() && !renderAll)
|
|
clearTile(ua->getTile());
|
|
|
|
uint32_t start = millis();
|
|
bool full = ua->wantsFullRender() || renderAll;
|
|
#ifdef MESHTASTIC_INCLUDE_INKHUD_BOOKREADER
|
|
if (ua->keepScreenAwakeDuringRender()) {
|
|
keepAwakeUntilUpdateComplete = true;
|
|
powerFSM.trigger(EVENT_INPUT);
|
|
}
|
|
#endif
|
|
ua->render(full); // Draw!
|
|
uint32_t stop = millis();
|
|
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run the drawing operations of any system applets which are currently
|
|
// displayed Pixel output is placed into the framebuffer, ready for handoff to
|
|
// the EInk driver
|
|
void InkHUD::Renderer::renderSystemApplets()
|
|
{
|
|
SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon");
|
|
SystemApplet *menu = inkhud->getSystemApplet("Menu");
|
|
SystemApplet *notifications = inkhud->getSystemApplet("Notification");
|
|
|
|
// Each system applet
|
|
for (SystemApplet *sa : inkhud->systemApplets) {
|
|
|
|
// Skip if not shown
|
|
if (!sa->isForeground())
|
|
continue;
|
|
|
|
if (!sa->wantsToRender() && !sa->alwaysRender && !renderAll)
|
|
continue;
|
|
|
|
// Skip if locked by another applet
|
|
if (lockRendering && lockRendering != sa)
|
|
continue;
|
|
|
|
// Don't draw the battery or notifications overtop the menu
|
|
// Todo: smarter way to handle this
|
|
if (menu->isForeground() && (sa == battery || sa == notifications))
|
|
continue;
|
|
|
|
assert(sa->getTile());
|
|
|
|
// Clear the tile unless the applet wants to draw over its previous render
|
|
// or everything is getting re-rendered anyways
|
|
if (sa->wantsFullRender() && !renderAll)
|
|
clearTile(sa->getTile());
|
|
|
|
// uint32_t start = millis();
|
|
bool full = sa->wantsFullRender() || renderAll;
|
|
sa->render(full); // Draw!
|
|
// uint32_t stop = millis();
|
|
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
|
}
|
|
}
|
|
|
|
// In some situations (e.g. layout or applet selection changes),
|
|
// a user tile can end up without an assigned applet.
|
|
// In this case, we will fill the empty space with diagonal lines.
|
|
void InkHUD::Renderer::renderPlaceholders()
|
|
{
|
|
// Don't fill empty space with placeholders if a system applet wants exclusive
|
|
// use of the display
|
|
if (lockRendering)
|
|
return;
|
|
|
|
// Ask the window manager which tiles are empty
|
|
std::vector<Tile *> emptyTiles = inkhud->getEmptyTiles();
|
|
|
|
// No empty tiles
|
|
if (emptyTiles.size() == 0)
|
|
return;
|
|
|
|
SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder");
|
|
|
|
// uint32_t start = millis();
|
|
for (Tile *t : emptyTiles) {
|
|
t->assignApplet(placeholder);
|
|
// Clear the tile unless everything is getting re-rendered
|
|
if (!renderAll)
|
|
clearTile(t);
|
|
placeholder->render(true); // full render
|
|
t->assignApplet(nullptr);
|
|
}
|
|
// uint32_t stop = millis();
|
|
// LOG_DEBUG("Placeholders took %dms to render", stop - start);
|
|
}
|
|
|
|
#endif
|