Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 78 additions & 3 deletions lib/GfxRenderer/GfxRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
const int left = glyph->left;
const int top = glyph->top;

// Tiled-grayscale band culling: if this glyph's physical y-extent is entirely
// outside the active strip, skip it before the expensive bitmap decode. This
// is what makes per-band re-rendering cheap. No-op outside strip mode.
if constexpr (rotation == TextRotation::Rotated90CW) {
const int ob = cursorX + fontData->ascender - top;
const int ib = cursorY - left;
if (!renderer.glyphIntersectsStrip(ob, ib - (width - 1), ob + height - 1, ib)) {
return;
}
} else {
const int gx0 = cursorX + left;
const int gy0 = cursorY - top;
if (!renderer.glyphIntersectsStrip(gx0, gy0, gx0 + width - 1, gy0 + height - 1)) {
return;
}
}

const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph);

if (bitmap != nullptr) {
Expand Down Expand Up @@ -240,14 +257,26 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
return;
}

// Tiled grayscale: redirect writes to the strip scratch and clip to the
// current band. Single predictable branch on the hot per-pixel path.
uint8_t* target = frameBuffer;
uint32_t rowY = static_cast<uint32_t>(phyY);
if (_stripActive) {
if (phyY < _stripY0 || phyY >= _stripY0 + _stripRows) {
return; // pixel outside the band currently being rendered
}
target = _stripBuf;
rowY = static_cast<uint32_t>(phyY - _stripY0);
}

// Calculate byte position and bit position
const uint32_t byteIndex = static_cast<uint32_t>(phyY) * panelWidthBytes + (phyX / 8);
const uint32_t byteIndex = rowY * panelWidthBytes + (phyX / 8);
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first

if (state) {
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
target[byteIndex] &= ~(1 << bitPosition); // Clear bit
} else {
frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit
target[byteIndex] |= 1 << bitPosition; // Set bit
}
}

Expand Down Expand Up @@ -966,9 +995,47 @@ static uint32_t start_ms = 0;

void GfxRenderer::clearScreen(const uint8_t color) const {
start_ms = halPlatform.millis();
if (_stripActive) {
// Clear only the active band's scratch, not the shared framebuffer.
memset(_stripBuf, color, static_cast<size_t>(panelWidthBytes) * _stripRows);
return;
}
display.clearScreen(color);
}

void GfxRenderer::beginStripTarget(uint8_t* scratch, int stripY0, int stripRows) const {
// Band is caller-guaranteed in-bounds (the reader's grayscale loop computes
// it); assert catches future misuse in debug before it mis-renders or wraps
// the downstream uint16_t cast in writeGrayscalePlaneStrip.
assert(scratch != nullptr && stripRows > 0 && stripY0 >= 0 && stripY0 <= static_cast<int>(panelHeight) - stripRows);
_stripBuf = scratch;
_stripY0 = stripY0;
_stripRows = stripRows;
_stripActive = true;
}

void GfxRenderer::endStripTarget() const {
_stripActive = false;
_stripBuf = nullptr;
_stripY0 = 0;
_stripRows = 0;
}

bool GfxRenderer::glyphIntersectsStrip(int x0, int y0, int x1, int y1) const {
if (!_stripActive) {
return true;
}
// Rotate the two opposite bbox corners to physical coords. For 90-degree
// orientations the physical bbox stays axis-aligned, so min/max of the two
// rotated corners' Y bounds the glyph's physical y-extent.
int ax, ay, bx, by;
rotateCoordinates(orientation, x0, y0, &ax, &ay, panelWidth, panelHeight);
rotateCoordinates(orientation, x1, y1, &bx, &by, panelWidth, panelHeight);
const int minY = ay < by ? ay : by;
const int maxY = ay > by ? ay : by;
return !(maxY < _stripY0 || minY >= _stripY0 + _stripRows);
}

void GfxRenderer::invertScreen() const {
for (uint32_t i = 0; i < frameBufferSize; i++) {
frameBuffer[i] = ~frameBuffer[i];
Expand Down Expand Up @@ -1370,6 +1437,14 @@ void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuff

void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(fadingFix); }

void GfxRenderer::writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* scratch, int yStart, int numRows) const {
// Guard the uint16_t casts below: a negative would wrap to a huge length.
assert(yStart >= 0 && numRows > 0 && yStart <= static_cast<int>(panelHeight) - numRows);
display.writeGrayscalePlaneStrip(lsbPlane, scratch, static_cast<uint16_t>(yStart), static_cast<uint16_t>(numRows));
}

bool GfxRenderer::supportsStripGrayscale() const { return display.supportsStripGrayscale(); }

void GfxRenderer::freeBwBufferChunks() {
for (auto& bwBufferChunk : bwBufferChunks) {
if (bwBufferChunk) {
Expand Down
43 changes: 43 additions & 0 deletions lib/GfxRenderer/GfxRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ class GfxRenderer {
// 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();
Expand Down Expand Up @@ -114,6 +126,31 @@ class GfxRenderer {
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;
Expand Down Expand Up @@ -172,6 +209,12 @@ class GfxRenderer {
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;
Expand Down
1 change: 1 addition & 0 deletions lib/I18n/translations/english.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ STR_INVERTED: "Inverted"
STR_LANDSCAPE_CCW: "Landscape CCW"
STR_PREV_NEXT: "Prev/Next"
STR_NEXT_PREV: "Next/Prev"
STR_DISABLED: "Disabled"
STR_NOTO_SERIF: "Noto Serif"
STR_NOTO_SANS: "Noto Sans"
STR_COURIER_PRIME: "Courier Prime"
Expand Down
17 changes: 15 additions & 2 deletions lib/ImageDecoder/DirectPixelWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ struct DirectPixelWriter {
uint8_t* fb;
GfxRenderer::RenderMode mode;
uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99)
// Active write target: for tiled grayscale, fb is the band scratch, originY is
// the band's top physical row, and clipRows is the band height. Off-band
// pixels are dropped. With no strip active these collapse to the full frame
// (originY 0, clipRows panelHeight) so the clip doubles as a bounds guard.
int originY;
int clipRows;

// Orientation is collapsed into a linear transform:
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
Expand All @@ -28,7 +34,9 @@ struct DirectPixelWriter {
int rowPhyXBase, rowPhyYBase;

void init(GfxRenderer& renderer) {
fb = renderer.getFrameBuffer();
fb = renderer.getWriteTarget();
originY = renderer.getWriteOriginY();
clipRows = renderer.getWriteRows();
mode = renderer.getRenderMode();
displayWidthBytes = renderer.getDisplayWidthBytes();

Expand Down Expand Up @@ -120,7 +128,12 @@ struct DirectPixelWriter {
const int phyX = rowPhyXBase + logicalX * phyXStepX;
const int phyY = rowPhyYBase + logicalX * phyYStepX;

const uint16_t byteIndex = phyY * displayWidthBytes + (phyX >> 3);
// Band-local row. The unsigned compare drops both off-band pixels (strip
// mode) and any out-of-frame row (full-frame mode) in one branch.
const int sy = phyY - originY;
if (static_cast<unsigned>(sy) >= static_cast<unsigned>(clipRows)) return;

const uint16_t byteIndex = static_cast<uint16_t>(sy * displayWidthBytes + (phyX >> 3));
const uint8_t bitMask = 1 << (7 - (phyX & 7));

if (state) {
Expand Down
7 changes: 7 additions & 0 deletions lib/hal/HalDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@ void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.

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(); }
Expand Down
6 changes: 6 additions & 0 deletions lib/hal/HalDisplay.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ class HalDisplay {

void displayGrayBuffer(bool turnOffScreen = false);

// Tiled grayscale: stream one band of a plane (lsbPlane selects LSB/MSB RAM)
// straight to the controller; supportsStripGrayscale() gates the path. See
// EInkDisplay::writeGrayscalePlaneStrip.
void writeGrayscalePlaneStrip(bool lsbPlane, const uint8_t* rows, uint16_t yStart, uint16_t numRows);
bool supportsStripGrayscale() const;

// Runtime geometry passthrough
uint16_t getDisplayWidth() const;
uint16_t getDisplayHeight() const;
Expand Down
5 changes: 2 additions & 3 deletions src/CrossPointSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ class CrossPointSettings {
};

// Side button layout options
// Default: Previous, Next
// Swapped: Next, Previous
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT };
// Default: Up = Previous, Down = Next
enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTONS_DISABLED = 2, SIDE_BUTTON_LAYOUT_COUNT };

// Font family options (built-in fonts only; SD card fonts use sdFontFamilyName)
enum FONT_FAMILY { NOTOSERIF = 0, NOTOSANS = 1, COURIERPRIME = 2, FONT_FAMILY_COUNT };
Expand Down
38 changes: 19 additions & 19 deletions src/MappedInputManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,8 @@

#include "CrossPointSettings.h"

namespace {
using ButtonIndex = uint8_t;

struct SideLayoutMap {
ButtonIndex pageBack;
ButtonIndex pageForward;
};

// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT.
constexpr SideLayoutMap kSideLayouts[] = {
{HalGPIO::BTN_UP, HalGPIO::BTN_DOWN},
{HalGPIO::BTN_DOWN, HalGPIO::BTN_UP},
};
} // namespace

bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const {
const auto sideLayout = static_cast<CrossPointSettings::SIDE_BUTTON_LAYOUT>(SETTINGS.sideButtonLayout);
const auto& side = kSideLayouts[sideLayout];
const auto sideLayout = SETTINGS.sideButtonLayout;

switch (button) {
case Button::Back:
Expand All @@ -45,10 +29,26 @@ bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint
return (gpio.*fn)(HalGPIO::BTN_POWER);
case Button::PageBack:
// Reader page navigation uses side buttons and can be swapped via settings.
return (gpio.*fn)(side.pageBack);
switch (sideLayout) {
case CrossPointSettings::PREV_NEXT:
return (gpio.*fn)(HalGPIO::BTN_UP);
case CrossPointSettings::NEXT_PREV:
return (gpio.*fn)(HalGPIO::BTN_DOWN);
case CrossPointSettings::SIDE_BUTTONS_DISABLED:
default:
return false;
}
case Button::PageForward:
// Reader page navigation uses side buttons and can be swapped via settings.
return (gpio.*fn)(side.pageForward);
switch (sideLayout) {
case CrossPointSettings::PREV_NEXT:
return (gpio.*fn)(HalGPIO::BTN_DOWN);
case CrossPointSettings::NEXT_PREV:
return (gpio.*fn)(HalGPIO::BTN_UP);
case CrossPointSettings::SIDE_BUTTONS_DISABLED:
default:
return false;
}
}

return false;
Expand Down
3 changes: 2 additions & 1 deletion src/SettingsList.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ inline std::vector<SettingInfo> getSettingsList(const SdCardFontRegistry* regist
"imageRendering", StrId::STR_CAT_READER),
// --- Controls ---
SettingInfo::Enum(StrId::STR_SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout,
{StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV}, "sideButtonLayout", StrId::STR_CAT_CONTROLS),
{StrId::STR_PREV_NEXT, StrId::STR_NEXT_PREV, StrId::STR_DISABLED}, "sideButtonLayout",
StrId::STR_CAT_CONTROLS),
SettingInfo::Toggle(StrId::STR_FRONT_BTN_FOLLOW_ORIENTATION, &CrossPointSettings::frontButtonFollowOrientation,
"frontButtonFollowOrientation", StrId::STR_CAT_CONTROLS),
SettingInfo::Enum(StrId::STR_LONG_PRESS_BEHAVIOR, &CrossPointSettings::longPressButtonBehavior,
Expand Down
Loading
Loading