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.
311 lines
17 KiB
C++
311 lines
17 KiB
C++
#pragma once
|
|
|
|
#include <EpdFontFamily.h>
|
|
#include <HalDisplay.h>
|
|
|
|
namespace BidiUtils {
|
|
// Paragraph base direction for the Unicode BiDi algorithm (UAX#9).
|
|
// AUTO: scan text for first strong directional character (P2/P3 rules)
|
|
// LTR: force left-to-right paragraph embedding level
|
|
// RTL: force right-to-left paragraph embedding level
|
|
enum class BidiBaseDir : signed char { AUTO = -1, LTR = 0, RTL = 1 };
|
|
} // namespace BidiUtils
|
|
|
|
class FontCacheManager;
|
|
class SdCardFont;
|
|
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Bitmap.h"
|
|
|
|
// Color representation: uint8_t mapped to 4x4 Bayer matrix dithering levels
|
|
// 0 = transparent, 1-16 = gray levels (white to black)
|
|
enum Color : uint8_t { Clear = 0x00, White = 0x01, LightGray = 0x05, DarkGray = 0x0A, Black = 0x10 };
|
|
|
|
class GfxRenderer {
|
|
public:
|
|
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
|
|
|
|
// Logical screen orientation from the perspective of callers
|
|
enum Orientation {
|
|
Portrait, // 480x800 logical coordinates (current default)
|
|
LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap top/bottom)
|
|
PortraitInverted, // 480x800 logical coordinates, inverted
|
|
LandscapeCounterClockwise // 800x480 logical coordinates, native panel orientation
|
|
};
|
|
|
|
private:
|
|
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
|
|
|
HalDisplay& display;
|
|
RenderMode renderMode;
|
|
Orientation orientation;
|
|
bool fadingFix;
|
|
uint8_t* frameBuffer = nullptr;
|
|
uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH;
|
|
uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT;
|
|
uint16_t panelWidthBytes = HalDisplay::DISPLAY_WIDTH_BYTES;
|
|
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
|
std::vector<uint8_t*> bwBufferChunks;
|
|
std::map<int, EpdFontFamily> fontMap;
|
|
// Mutable because ensureSdCardFontReady() is const (called from layout code
|
|
// that holds a const GfxRenderer&) but triggers SD card reads and heap
|
|
// allocation inside the SdCardFont objects. Same pragmatic compromise as
|
|
// fontCacheManager_ below.
|
|
mutable std::map<int, SdCardFont*> sdCardFonts_;
|
|
|
|
// Mutable because drawText() is const but needs to delegate scan-mode
|
|
// recording to the (non-const) FontCacheManager. Same pragmatic compromise
|
|
// as before, concentrated in a single pointer instead of four fields.
|
|
mutable FontCacheManager* fontCacheManager_ = nullptr;
|
|
|
|
// Tiled grayscale strip target. When active, drawPixel()/clearScreen()
|
|
// operate on a caller-owned scratch holding one horizontal band of physical
|
|
// rows [_stripY0, _stripY0 + _stripRows) (panelWidthBytes wide) instead of
|
|
// the shared framebuffer, clipping pixels outside the band. Lets grayscale
|
|
// planes render band-by-band straight to the controller without destroying
|
|
// the BW framebuffer (no storeBwBuffer). Mutable because the render path is
|
|
// const. See beginStripTarget()/endStripTarget().
|
|
mutable uint8_t* _stripBuf = nullptr;
|
|
mutable int _stripY0 = 0;
|
|
mutable int _stripRows = 0;
|
|
mutable bool _stripActive = false;
|
|
|
|
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
|
|
EpdFontFamily::Style style) const;
|
|
void freeBwBufferChunks();
|
|
template <Color color>
|
|
void drawPixelDither(int x, int y) const;
|
|
template <Color color>
|
|
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
|
|
// Byte-aligned, orientation-specialized rectangle fill. Rotates the rect's
|
|
// two opposing corners into physical-framebuffer space once, then walks each
|
|
// physical row with head-mask / middle memset / tail-mask byte writes — no
|
|
// per-pixel rotation, no per-pixel RMW.
|
|
template <Color color>
|
|
void fillRectImpl(int x, int y, int width, int height) const;
|
|
|
|
public:
|
|
explicit GfxRenderer(HalDisplay& halDisplay)
|
|
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
|
|
~GfxRenderer() { freeBwBufferChunks(); }
|
|
|
|
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
|
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
|
|
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
|
|
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
|
|
|
|
// Setup
|
|
void begin(); // must be called right after display.begin()
|
|
void insertFont(int fontId, EpdFontFamily font);
|
|
// Clears both the flash-font map and any SD-font registration for fontId.
|
|
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
|
|
// the underlying SdCardFont and forget the SD-side unregister.
|
|
void removeFont(int fontId) {
|
|
fontMap.erase(fontId);
|
|
sdCardFonts_.erase(fontId);
|
|
}
|
|
void setFontCacheManager(FontCacheManager* m) { fontCacheManager_ = m; }
|
|
FontCacheManager* getFontCacheManager() const { return fontCacheManager_; }
|
|
bool isFontCacheScanning() const;
|
|
const std::map<int, EpdFontFamily>& getFontMap() const { return fontMap; }
|
|
void registerSdCardFont(int fontId, SdCardFont* font) { sdCardFonts_[fontId] = font; }
|
|
void unregisterSdCardFont(int fontId) { removeFont(fontId); }
|
|
void clearSdCardFonts() { sdCardFonts_.clear(); }
|
|
const std::map<int, SdCardFont*>& getSdCardFonts() const { return sdCardFonts_; }
|
|
bool isSdCardFont(int fontId) const { return sdCardFonts_.count(fontId) > 0; }
|
|
// Ensure SD card font glyph data is loaded for the given text. Called from layout code
|
|
// (which holds a const GfxRenderer&) before measuring word widths. Safe to call on non-SD fonts (no-op).
|
|
// styleMask: bitmask of styles to prepare (bit 0=regular, 1=bold, 2=italic, 3=bold-italic).
|
|
void ensureSdCardFontReady(int fontId, const char* utf8Text, uint8_t styleMask = 0x0F) const;
|
|
void ensureSdCardFontReady(int fontId, const std::vector<std::string>& words, bool includeHyphen,
|
|
uint8_t styleMask = 0x0F) const;
|
|
|
|
// Orientation control (affects logical width/height and coordinate transforms)
|
|
void setOrientation(const Orientation o) { orientation = o; }
|
|
Orientation getOrientation() const { return orientation; }
|
|
|
|
// Fading fix control
|
|
void setFadingFix(const bool enabled) { fadingFix = enabled; }
|
|
|
|
// Screen ops
|
|
int getScreenWidth() const;
|
|
int getScreenHeight() const;
|
|
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
|
// Non-blocking refresh: starts the waveform and returns so CPU work (e.g.
|
|
// grayscale strip rendering) can overlap the panel's refresh time. The
|
|
// framebuffer must stay untouched until waitRefreshComplete(). Falls back to
|
|
// a blocking refresh when fadingFix is enabled or the panel lacks deferral
|
|
// support. See HalDisplay::displayBufferAsync for the baseline contract.
|
|
void displayBufferAsync(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
|
|
void waitRefreshComplete() const;
|
|
// True when displayBufferAsync() genuinely overlaps: panel defers and
|
|
// fadingFix isn't forcing the blocking path. Callers can skip overlap
|
|
// scaffolding (e.g. whole-plane grayscale buffers) when false.
|
|
bool supportsAsyncRefresh() const;
|
|
// EXPERIMENTAL: Windowed update - display only a rectangular region
|
|
// void displayWindow(int x, int y, int width, int height) const;
|
|
void invertScreen() const;
|
|
void clearScreen(uint8_t color = 0xFF) const;
|
|
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
|
|
|
// Tiled grayscale strip target. While active, drawPixel() and clearScreen()
|
|
// operate on `scratch` (panelWidthBytes * stripRows bytes, holding physical
|
|
// rows [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels
|
|
// whose physical row falls outside the band are clipped. The clip is applied
|
|
// after the orientation rotate, so it is orientation-agnostic. Used to render
|
|
// grayscale planes band-by-band without a full second buffer.
|
|
void beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const;
|
|
void endStripTarget() const;
|
|
|
|
// Band culling for tiled grayscale. Takes a glyph bounding box in logical
|
|
// screen coords and returns false only when a strip is active AND the box's
|
|
// physical y-extent lies entirely outside the active band, letting callers
|
|
// skip an expensive bitmap decode. Returns true when no strip is active.
|
|
// Corners are rotated to physical, so it is orientation-aware.
|
|
bool glyphIntersectsStrip(int x0, int y0, int x1, int y1) const;
|
|
|
|
// Active pixel-write target for raw writers (DirectPixelWriter) that bypass
|
|
// drawPixel for speed. When a strip target is active these return the band
|
|
// scratch plus its physical-row origin and extent; otherwise the full
|
|
// framebuffer ([0, panelHeight)). Writers subtract the origin and clip to the
|
|
// extent, so they honor tiled-grayscale banding without per-pixel method calls.
|
|
uint8_t* getWriteTarget() const { return _stripActive ? _stripBuf : frameBuffer; }
|
|
int getWriteOriginY() const { return _stripActive ? _stripY0 : 0; }
|
|
int getWriteRows() const { return _stripActive ? _stripRows : panelHeight; }
|
|
|
|
// Drawing
|
|
void drawPixel(int x, int y, bool state = true) const;
|
|
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
|
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state) const;
|
|
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
|
|
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
|
void drawRect(int x, int y, int width, int height, int lineWidth, bool state) const;
|
|
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool state) const;
|
|
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool roundTopLeft,
|
|
bool roundTopRight, bool roundBottomLeft, bool roundBottomRight, bool state) const;
|
|
void maskRoundedRectOutsideCorners(int x, int y, int width, int height, int radius, Color color = Color::White) const;
|
|
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
|
void fillRectDither(int x, int y, int width, int height, Color color) const;
|
|
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, Color color) const;
|
|
void fillRoundedRect(int x, int y, int width, int height, int cornerRadius, bool roundTopLeft, bool roundTopRight,
|
|
bool roundBottomLeft, bool roundBottomRight, Color color) const;
|
|
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
|
void drawIcon(const uint8_t bitmap[], int x, int y, int size) const;
|
|
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
|
float cropY = 0) const;
|
|
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
|
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
|
|
|
// Text
|
|
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
|
BidiUtils::BidiBaseDir baseDir = BidiUtils::BidiBaseDir::AUTO) const;
|
|
void drawCenteredText(int fontId, int y, const char* text, bool black = true,
|
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
|
BidiUtils::BidiBaseDir baseDir = BidiUtils::BidiBaseDir::AUTO) const;
|
|
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR,
|
|
BidiUtils::BidiBaseDir baseDir = BidiUtils::BidiBaseDir::AUTO) const;
|
|
int getSpaceWidth(int fontId, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
|
/// Returns the total inter-word advance: fp4::toPixel(spaceAdvance + kern(leftCp,' ') + kern(' ',rightCp)).
|
|
/// Using a single snap avoids the +/-1 px rounding error that arises when space advance and kern are
|
|
/// snapped separately and then added as integers.
|
|
int getSpaceAdvance(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
|
|
/// Returns the kerning adjustment between two adjacent codepoints.
|
|
int getKerning(int fontId, uint32_t leftCp, uint32_t rightCp, EpdFontFamily::Style style) const;
|
|
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
|
|
int getFontAscenderSize(int fontId) const;
|
|
int getLineHeight(int fontId) const;
|
|
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
|
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
|
|
/// \p maxWidth pixels. Overflowing words and excess lines are UTF-8-safely
|
|
/// truncated with an ellipsis (U+2026).
|
|
std::vector<std::string> wrappedText(int fontId, const char* text, int maxWidth, int maxLines,
|
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
|
|
|
// Helper for drawing rotated text (90 degrees clockwise, for side buttons)
|
|
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true,
|
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
|
int getTextHeight(int fontId) const;
|
|
|
|
// Grayscale functions
|
|
void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
|
|
RenderMode getRenderMode() const { return renderMode; }
|
|
// Grayscale preconditioning settle pass (no-op on X4). The rect overload
|
|
// takes the gray region in LOGICAL screen coordinates and rotates it to the
|
|
// panel; the no-arg overload settles the full frame. Call after the BW base
|
|
// frame is displayed and before the grayscale planes are written.
|
|
void preconditionGrayscale() const;
|
|
void preconditionGrayscale(int x, int y, int w, int h) const;
|
|
// Display the framebuffer as the base frame for a grayscale overlay that
|
|
// follows (X3: OEM differential base waveform; others: plain display with
|
|
// `fallback`).
|
|
void displayGrayscaleBase(HalDisplay::RefreshMode fallback = HalDisplay::HALF_REFRESH) const;
|
|
void copyGrayscaleLsbBuffers() const;
|
|
void copyGrayscaleMsbBuffers() const;
|
|
void displayGrayBuffer() const;
|
|
|
|
// Tiled grayscale (X4): stream one band of a plane straight to controller RAM
|
|
// from `scratch` (panelWidthBytes * numRows, physical rows [yStart, yStart+
|
|
// numRows)), bypassing the framebuffer. supportsStripGrayscale() gates use.
|
|
void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch, int yStart, int numRows) const;
|
|
bool supportsStripGrayscale() const;
|
|
bool storeBwBuffer(); // Returns true if buffer was stored successfully
|
|
void restoreBwBuffer(); // Restore and free the stored buffer
|
|
void cleanupGrayscaleWithFrameBuffer() const;
|
|
|
|
// Font helpers
|
|
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
|
|
|
|
// Lend the 48 KB framebuffer's bytes to a memory-hungry phase (chapter
|
|
// builds) WITHOUT freeing the allocation, so it never moves and repeated
|
|
// loans cannot fragment the heap. Between release and restore NOTHING may
|
|
// draw or display — the panel keeps showing its last refreshed image. The
|
|
// lent bytes are published via buildscratch::claim() for consumers like
|
|
// InflateStream. restore returns the buffer white, so the caller must
|
|
// redraw the full screen; it cannot fail (no allocation involved).
|
|
void releaseFrameBufferForBuild();
|
|
bool restoreFrameBufferAfterBuild();
|
|
bool hasFrameBuffer() const { return frameBuffer != nullptr; }
|
|
|
|
// RAII form of the loan above, for blocking build regions with early-return
|
|
// error paths: restores on scope exit (or explicitly via end()). Display the
|
|
// popup/screen the panel should hold BEFORE constructing one. Constructing
|
|
// while the framebuffer is already lent yields an inert loan (nesting-safe).
|
|
class FrameBufferLoan {
|
|
public:
|
|
explicit FrameBufferLoan(GfxRenderer& renderer);
|
|
~FrameBufferLoan() { end(); }
|
|
void end();
|
|
FrameBufferLoan(const FrameBufferLoan&) = delete;
|
|
FrameBufferLoan& operator=(const FrameBufferLoan&) = delete;
|
|
|
|
private:
|
|
GfxRenderer& renderer_;
|
|
bool active_ = false;
|
|
};
|
|
|
|
// Low level functions
|
|
uint8_t* getFrameBuffer() const;
|
|
size_t getBufferSize() const;
|
|
uint16_t getDisplayWidth() const { return panelWidth; }
|
|
uint16_t getDisplayHeight() const { return panelHeight; }
|
|
uint16_t getDisplayWidthBytes() const { return panelWidthBytes; }
|
|
|
|
// Region cache: take a logical (orientation-aware) rect, hit the framebuffer
|
|
// bytes that the rect can have touched, and pump them in or out of a caller-
|
|
// supplied buffer. Used by HomeActivity to snapshot just the cover tile
|
|
// (~16 KB in Portrait) instead of cloning the entire 48 KB framebuffer.
|
|
//
|
|
// getRegionByteSize: required buffer length for the rect at current orientation.
|
|
// copyRegionToBuffer / copyBufferToRegion: false if `bufSize` is smaller than that.
|
|
size_t getRegionByteSize(int logicalX, int logicalY, int logicalW, int logicalH) const;
|
|
bool copyRegionToBuffer(int logicalX, int logicalY, int logicalW, int logicalH, uint8_t* buf, size_t bufSize) const;
|
|
bool copyBufferToRegion(int logicalX, int logicalY, int logicalW, int logicalH, const uint8_t* buf,
|
|
size_t bufSize) const;
|
|
};
|