Files
Crosspoint/lib/GfxRenderer/GfxRenderer.h
T

326 lines
18 KiB
C++

#pragma once
#include <EpdFontFamily.h>
#include <HalDisplay.h>
class FontCacheManager;
class SdCardFont;
#include <atomic>
#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
static constexpr unsigned int REFRESH_OVERRIDE_NONE = 0;
HalDisplay& display;
std::atomic<int> renderMode;
std::atomic<int> orientation;
std::atomic<bool> fadingFix;
// Text darkness for 2-bit grayscale glyph rendering.
std::atomic<uint8_t> textDarkness;
// 0 = Normal — true 4-level AA (raw=1 → light gray, raw=2 → dark gray)
// 1 = Dark — historical default; raw=2 collapses to black
// 2 = Extra Dark — both AA shades go black in the grayscale plane
// 3 = Maximum — grayscale pass skipped entirely; only the hard BW
// pass remains, so AA pixels render as solid black
// with the FAST waveform (no gray-LUT softening)
// Only affects AA pixels in GRAYSCALE_MSB / GRAYSCALE_LSB rendering of 2-bit fonts.
// 1-bit fonts and the BW pass are unchanged. Default is 1 to preserve historical
// rendering. See drawMaskFor2BitMode() in GfxRenderer.cpp for the per-level
// pixel breakdown and a worked example glyph.
uint8_t* frameBuffer = nullptr;
uint16_t panelWidth = 0; // set in begin()
uint16_t panelHeight = 0; // set in begin()
uint16_t panelWidthBytes = 0; // set in begin()
uint32_t frameBufferSize = 0; // set in begin()
uint16_t bwSnapshotRowStart = 0;
uint16_t bwSnapshotRowEnd = 0;
size_t bwSnapshotSizeBytes = 0;
size_t bwBufferChunkSize = BW_BUFFER_CHUNK_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_.
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;
mutable std::atomic<unsigned int> refreshOverride = REFRESH_OVERRIDE_NONE;
// Tiled grayscale strip target. When active, drawPixel(), clearScreen(),
// fillPhysicalHSpanByte() and renderGlyphFast2Bit() write into a caller-owned
// scratch holding one horizontal band of physical rows
// [_stripY0, _stripY0 + _stripRows) (panelWidthBytes wide) instead of the
// shared framebuffer; pixels outside the band are clipped. 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;
// Precomputed orientation→physicalY linear coefficients for the band-cull
// fast path. Latched once in beginStripTarget() and used by every
// glyphIntersectsStrip() call to avoid the 4-case rotateCoordinates switch
// per glyph. phyY = stripPhyYStepX_ * x + stripPhyYStepY_ * y + stripPhyYBase_,
// with steps in {-1, 0, 1}. Orientation can't change mid-pass; the strip
// session is the natural latch point.
mutable int8_t stripPhyYStepX_ = 0;
mutable int8_t stripPhyYStepY_ = 0;
mutable int stripPhyYBase_ = 0;
// Session-owned strip scratch. acquireStripScratch() allocates once (sized
// STRIP_SCRATCH_TARGET_BYTES, rounded to a whole number of rows of
// panelWidthBytes) and the buffer persists until releaseStripScratch().
// Allocating per page turn fragments the tight ESP32-C3 heap badly enough
// to cause AA to suspend after a few pages; hold one buffer for the reader
// session instead. The strip height we pick at acquire time is exposed via
// getStripScratchRows() so the caller plans its band loop around it.
uint8_t* stripScratch_ = nullptr;
int stripScratchRows_ = 0;
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;
// Write a patterned horizontal span directly to the physical framebuffer using byte-level operations.
// phyY: physical row; phyX_start/phyX_end: inclusive physical column range.
// patternByte is repeated across the span; partial edge bytes are blended with existing content.
// Bit layout: MSB-first (bit 7 = phyX=0); 0 bits = dark pixel, 1 bits = white pixel.
void fillPhysicalHSpanByte(int phyY, int phyX_start, int phyX_end, uint8_t patternByte) const;
// Write a solid horizontal span directly to the physical framebuffer using byte-level operations.
// Thin wrapper around fillPhysicalHSpanByte: state=true → 0x00 (dark), false → 0xFF (white).
void fillPhysicalHSpan(int phyY, int phyX_start, int phyX_end, bool state) const;
public:
explicit GfxRenderer(HalDisplay& halDisplay)
: display(halDisplay),
renderMode(static_cast<int>(BW)),
orientation(static_cast<int>(Portrait)),
fadingFix(false),
textDarkness(1) {}
~GfxRenderer() {
freeBwBufferChunks();
releaseStripScratch();
}
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);
void removeFont(int fontId) { fontMap.erase(fontId); }
void setFontCacheManager(FontCacheManager* m) { fontCacheManager_ = m; }
FontCacheManager* getFontCacheManager() const { return fontCacheManager_; }
const std::map<int, EpdFontFamily>& getFontMap() const { return fontMap; }
void registerSdCardFont(int fontId, SdCardFont* font) { sdCardFonts_[fontId] = font; }
void unregisterSdCardFont(int fontId) { sdCardFonts_.erase(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).
void ensureSdCardFontReady(int fontId, const char* utf8Text) const;
// Clear the cumulative SD card font metadata cache built up by repeated
// ensureSdCardFontReady() calls. Call between sections to bound the cumulative
// codepoint set growth across pagination. Safe to call when no SD font is active.
void clearSdCardFontAccumulation() const;
// Orientation control (affects logical width/height and coordinate transforms)
void setOrientation(const Orientation o) { orientation.store(static_cast<int>(o), std::memory_order_relaxed); }
Orientation getOrientation() const { return static_cast<Orientation>(orientation.load(std::memory_order_relaxed)); }
// Fading fix control
void setFadingFix(const bool enabled) { fadingFix.store(enabled, std::memory_order_relaxed); }
// Screen ops
int getScreenWidth() const;
int getScreenHeight() const;
void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const;
void setNextDisplayRefreshMode(HalDisplay::RefreshMode refreshMode) 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;
// 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 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 width, int height) const;
void drawIconInverted(const uint8_t bitmap[], int x, int y, int width, int height) 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) const;
void drawCenteredText(int fontId, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
void drawText(int fontId, int x, int y, const char* text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) 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.store(static_cast<int>(mode), std::memory_order_relaxed);
}
RenderMode getRenderMode() const { return static_cast<RenderMode>(renderMode.load(std::memory_order_relaxed)); }
// Text darkness control:
// 0 = Normal, 1 = Dark, 2 = Extra Dark, 3 = Maximum.
// Only affects anti-aliased pixels in 2-bit (grayscale) glyph rendering;
// 1-bit fonts and the BW pass are unchanged. See drawMaskFor2BitMode() in
// GfxRenderer.cpp for the per-level pixel breakdown and a worked example.
void setTextDarkness(const uint8_t d) { textDarkness.store(d, std::memory_order_relaxed); }
uint8_t getTextDarkness() const { return static_cast<uint8_t>(textDarkness.load(std::memory_order_relaxed)); }
void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const;
// Tiled grayscale (X4 + X3): 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;
// X3-only: trade AA visual fidelity for ~2.2 s faster page-flip wall clock.
// No effect on X4 (its single grayscale LUT already runs at ~500 ms).
void setFastGrayscaleLut(bool fast) const { display.setFastGrayscaleLut(fast); }
bool getFastGrayscaleLut() const { return display.getFastGrayscaleLut(); }
// Tiled grayscale strip target. While active, drawPixel(), clearScreen(),
// fillPhysicalHSpanByte() and renderGlyphFast2Bit() 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;
// Session-owned strip scratch lifecycle. Reader activities call acquire on
// onEnter() and release on onExit(); the buffer is then reused across all
// page-turn AA passes for that session. Acquire is idempotent and returns
// true on success or when the buffer is already held. Sizing uses the
// current panel geometry, so begin() must have run first.
bool acquireStripScratch();
void releaseStripScratch();
uint8_t* getStripScratch() const { return stripScratch_; }
int getStripScratchRows() const { return stripScratchRows_; }
// Target byte budget for the session-owned strip scratch. ~24 KB lands at
// 240 rows on both X4 (100 B/row, panel 480) → 2 bands/plane and X3
// (99 B/row, panel 528) → 3 bands/plane. acquire clamps to panelHeight so
// a smaller panel never over-allocates.
static constexpr int STRIP_SCRATCH_TARGET_BYTES = 24000;
// Active pixel-write target for raw writers 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_ : static_cast<int>(panelHeight); }
bool isStripActive() const { return stripActive_; }
// Band culling. 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 expensive
// bitmap decode. Returns true when no strip is active.
bool glyphIntersectsStrip(int x0, int y0, int x1, int y1) const;
bool storeBwBuffer(); // Returns true if buffer was stored successfully
bool storeBwBufferRect(int x, int y, int width, int height); // Store only rows intersecting logical rect
void restoreBwBuffer(); // Restore and free the stored buffer
void cleanupGrayscaleWithFrameBuffer() const;
// Font helpers
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
// 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 helpers: operate on a logical (orientation-aware) rect and
// copy only the framebuffer bytes it touches. Used by HomeActivity to snapshot
// the cover tile (~16 KB) instead of the full 48 KB framebuffer.
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;
};