From 2b0d071217ffb97736adb65f54e21af482b7d040 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sat, 21 Feb 2026 00:07:39 +0100 Subject: [PATCH 1/2] Use byte operations --- lib/GfxRenderer/GfxRenderer.cpp | 112 ++++++++++++++++++++++++++++++-- lib/GfxRenderer/GfxRenderer.h | 3 + 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index b385bc03..028845fd 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -3,6 +3,8 @@ #include #include +#include + const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const { if (fontData->groups != nullptr) { if (!fontDecompressor) { @@ -236,15 +238,34 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con if (y2 < y1) { std::swap(y1, y2); } - for (int y = y1; y <= y2; y++) { - drawPixel(x1, y, state); + // In Portrait/PortraitInverted a logical vertical line maps to a physical horizontal span. + switch (orientation) { + case Portrait: + fillPhysicalHSpan(HalDisplay::DISPLAY_HEIGHT - 1 - x1, y1, y2, state); + return; + case PortraitInverted: + fillPhysicalHSpan(x1, HalDisplay::DISPLAY_WIDTH - 1 - y2, HalDisplay::DISPLAY_WIDTH - 1 - y1, state); + return; + default: + for (int y = y1; y <= y2; y++) drawPixel(x1, y, state); + return; } } else if (y1 == y2) { if (x2 < x1) { std::swap(x1, x2); } - for (int x = x1; x <= x2; x++) { - drawPixel(x, y1, state); + // In Landscape a logical horizontal line maps to a physical horizontal span. + switch (orientation) { + case LandscapeCounterClockwise: + fillPhysicalHSpan(y1, x1, x2, state); + return; + case LandscapeClockwise: + fillPhysicalHSpan(HalDisplay::DISPLAY_HEIGHT - 1 - y1, HalDisplay::DISPLAY_WIDTH - 1 - x2, + HalDisplay::DISPLAY_WIDTH - 1 - x1, state); + return; + default: + for (int x = x1; x <= x2; x++) drawPixel(x, y1, state); + return; } } else { // Bresenham's line algorithm — integer arithmetic only @@ -373,9 +394,88 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con } } +// Write a solid horizontal span directly into the physical framebuffer with byte-level operations. +// Handles partial left/right bytes and fills the aligned middle with memset. +// Bit layout: MSB-first (bit 7 = phyX=0, bit 0 = phyX=7); state=true clears bits (dark pixel). +void GfxRenderer::fillPhysicalHSpan(const int phyY, const int phyX_start, const int phyX_end, const bool state) const { + const int cX0 = std::max(phyX_start, 0); + const int cX1 = std::min(phyX_end, (int)HalDisplay::DISPLAY_WIDTH - 1); + if (cX0 > cX1 || phyY < 0 || phyY >= (int)HalDisplay::DISPLAY_HEIGHT) return; + + uint8_t* const row = frameBuffer + phyY * HalDisplay::DISPLAY_WIDTH_BYTES; + const int startByte = cX0 >> 3; + const int endByte = cX1 >> 3; + const int leftBits = cX0 & 7; // first bit index within startByte + const int rightBits = cX1 & 7; // last bit index within endByte + + if (startByte == endByte) { + // Both endpoints in the same byte + const uint8_t mask = (0xFF >> leftBits) & ~(0xFF >> (rightBits + 1)); + if (state) + row[startByte] &= ~mask; + else + row[startByte] |= mask; + return; + } + + // Left partial byte + if (leftBits != 0) { + const uint8_t mask = 0xFF >> leftBits; + if (state) + row[startByte] &= ~mask; + else + row[startByte] |= mask; + } + + // Full bytes in the middle + const int fullStart = (leftBits == 0) ? startByte : startByte + 1; + const int fullEnd = (rightBits == 7) ? endByte : endByte - 1; + if (fullStart <= fullEnd) { + memset(row + fullStart, state ? 0x00 : 0xFF, fullEnd - fullStart + 1); + } + + // Right partial byte + if (rightBits != 7) { + const uint8_t mask = ~(0xFF >> (rightBits + 1)); + if (state) + row[endByte] &= ~mask; + else + row[endByte] |= mask; + } +} + void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { - for (int fillY = y; fillY < y + height; fillY++) { - drawLine(x, fillY, x + width - 1, fillY, state); + if (width <= 0 || height <= 0) return; + + // For each orientation, one logical dimension maps to a constant physical row, allowing the + // perpendicular dimension to be written as a byte-level span — eliminating per-pixel overhead. + switch (orientation) { + case Portrait: + // Logical column x → physical row (479-x); logical y range → physical x span + for (int lx = x; lx < x + width; lx++) { + fillPhysicalHSpan(HalDisplay::DISPLAY_HEIGHT - 1 - lx, y, y + height - 1, state); + } + return; + case PortraitInverted: + // Logical column x → physical row x; logical y range → physical x span (mirrored) + for (int lx = x; lx < x + width; lx++) { + fillPhysicalHSpan(lx, HalDisplay::DISPLAY_WIDTH - 1 - (y + height - 1), HalDisplay::DISPLAY_WIDTH - 1 - y, + state); + } + return; + case LandscapeCounterClockwise: + // Logical row y → physical row y; logical x range → physical x span + for (int ly = y; ly < y + height; ly++) { + fillPhysicalHSpan(ly, x, x + width - 1, state); + } + return; + case LandscapeClockwise: + // Logical row y → physical row (479-y); logical x range → physical x span (mirrored) + for (int ly = y; ly < y + height; ly++) { + fillPhysicalHSpan(HalDisplay::DISPLAY_HEIGHT - 1 - ly, HalDisplay::DISPLAY_WIDTH - 1 - (x + width - 1), + HalDisplay::DISPLAY_WIDTH - 1 - x, state); + } + return; } } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index e2d05d03..618ffd91 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -45,6 +45,9 @@ class GfxRenderer { void drawPixelDither(int x, int y) const; template void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const; + // Write a solid horizontal span directly to the physical framebuffer using byte-level operations. + // phyY: physical row; phyX_start/phyX_end: inclusive physical column range; state: true=dark. + void fillPhysicalHSpan(int phyY, int phyX_start, int phyX_end, bool state) const; public: explicit GfxRenderer(HalDisplay& halDisplay) From 71269b73255aa13ebabf1c1afeb27204ba8be275 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sat, 21 Feb 2026 00:41:55 +0100 Subject: [PATCH 2/2] Add dither too --- lib/GfxRenderer/GfxRenderer.cpp | 121 ++++++++++++++++++++++++-------- lib/GfxRenderer/GfxRenderer.h | 7 +- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 028845fd..e567d911 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -394,10 +394,11 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con } } -// Write a solid horizontal span directly into the physical framebuffer with byte-level operations. -// Handles partial left/right bytes and fills the aligned middle with memset. -// Bit layout: MSB-first (bit 7 = phyX=0, bit 0 = phyX=7); state=true clears bits (dark pixel). -void GfxRenderer::fillPhysicalHSpan(const int phyY, const int phyX_start, const int phyX_end, const bool state) const { +// Write a patterned horizontal span directly into the physical framebuffer with byte-level operations. +// patternByte is repeated across the full span; partial edge bytes are blended with existing content. +// Bit layout: MSB-first (bit 7 = phyX=0, bit 0 = phyX=7); 0 bits = dark pixel, 1 bits = white pixel. +void GfxRenderer::fillPhysicalHSpanByte(const int phyY, const int phyX_start, const int phyX_end, + const uint8_t patternByte) const { const int cX0 = std::max(phyX_start, 0); const int cX1 = std::min(phyX_end, (int)HalDisplay::DISPLAY_WIDTH - 1); if (cX0 > cX1 || phyY < 0 || phyY >= (int)HalDisplay::DISPLAY_HEIGHT) return; @@ -410,40 +411,36 @@ void GfxRenderer::fillPhysicalHSpan(const int phyY, const int phyX_start, const if (startByte == endByte) { // Both endpoints in the same byte - const uint8_t mask = (0xFF >> leftBits) & ~(0xFF >> (rightBits + 1)); - if (state) - row[startByte] &= ~mask; - else - row[startByte] |= mask; + const uint8_t fillMask = (0xFF >> leftBits) & ~(0xFF >> (rightBits + 1)); + row[startByte] = (row[startByte] & ~fillMask) | (patternByte & fillMask); return; } // Left partial byte if (leftBits != 0) { - const uint8_t mask = 0xFF >> leftBits; - if (state) - row[startByte] &= ~mask; - else - row[startByte] |= mask; + const uint8_t fillMask = 0xFF >> leftBits; + row[startByte] = (row[startByte] & ~fillMask) | (patternByte & fillMask); } // Full bytes in the middle const int fullStart = (leftBits == 0) ? startByte : startByte + 1; const int fullEnd = (rightBits == 7) ? endByte : endByte - 1; if (fullStart <= fullEnd) { - memset(row + fullStart, state ? 0x00 : 0xFF, fullEnd - fullStart + 1); + memset(row + fullStart, patternByte, fullEnd - fullStart + 1); } // Right partial byte if (rightBits != 7) { - const uint8_t mask = ~(0xFF >> (rightBits + 1)); - if (state) - row[endByte] &= ~mask; - else - row[endByte] |= mask; + const uint8_t fillMask = ~(0xFF >> (rightBits + 1)); + row[endByte] = (row[endByte] & ~fillMask) | (patternByte & fillMask); } } +// Thin wrapper: state=true → 0x00 (all dark), false → 0xFF (all white). +void GfxRenderer::fillPhysicalHSpan(const int phyY, const int phyX_start, const int phyX_end, const bool state) const { + fillPhysicalHSpanByte(phyY, phyX_start, phyX_end, state ? 0x00 : 0xFF); +} + void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const { if (width <= 0 || height <= 0) return; @@ -512,17 +509,81 @@ void GfxRenderer::fillRectDither(const int x, const int y, const int width, cons fillRect(x, y, width, height, true); } else if (color == Color::White) { fillRect(x, y, width, height, false); - } else if (color == Color::LightGray) { - for (int fillY = y; fillY < y + height; fillY++) { - for (int fillX = x; fillX < x + width; fillX++) { - drawPixelDither(fillX, fillY); - } - } } else if (color == Color::DarkGray) { - for (int fillY = y; fillY < y + height; fillY++) { - for (int fillX = x; fillX < x + width; fillX++) { - drawPixelDither(fillX, fillY); - } + // Pattern: dark where (phyX + phyY) % 2 == 0 (alternating checkerboard). + // Byte patterns (phyY even / phyY odd): + // Portrait / PortraitInverted: 0xAA / 0x55 + // LandscapeCW / LandscapeCCW: 0x55 / 0xAA + switch (orientation) { + case Portrait: + for (int lx = x; lx < x + width; lx++) { + const int phyY = HalDisplay::DISPLAY_HEIGHT - 1 - lx; + const uint8_t pb = (phyY % 2 == 0) ? 0xAA : 0x55; + fillPhysicalHSpanByte(phyY, y, y + height - 1, pb); + } + return; + case PortraitInverted: + for (int lx = x; lx < x + width; lx++) { + const int phyY = lx; + const uint8_t pb = (phyY % 2 == 0) ? 0xAA : 0x55; + fillPhysicalHSpanByte(phyY, HalDisplay::DISPLAY_WIDTH - 1 - (y + height - 1), + HalDisplay::DISPLAY_WIDTH - 1 - y, pb); + } + return; + case LandscapeCounterClockwise: + for (int ly = y; ly < y + height; ly++) { + const int phyY = ly; + const uint8_t pb = (phyY % 2 == 0) ? 0x55 : 0xAA; + fillPhysicalHSpanByte(phyY, x, x + width - 1, pb); + } + return; + case LandscapeClockwise: + for (int ly = y; ly < y + height; ly++) { + const int phyY = HalDisplay::DISPLAY_HEIGHT - 1 - ly; + const uint8_t pb = (phyY % 2 == 0) ? 0x55 : 0xAA; + fillPhysicalHSpanByte(phyY, HalDisplay::DISPLAY_WIDTH - 1 - (x + width - 1), + HalDisplay::DISPLAY_WIDTH - 1 - x, pb); + } + return; + } + } else if (color == Color::LightGray) { + // Pattern: dark where phyX % 2 == 0 && phyY % 2 == 0 (1-in-4 pixels dark). + // Byte patterns (phyY even / phyY odd) — 0xFF rows write no dark pixels and are skipped: + // Portrait: 0xFF (skip) / 0x55 + // PortraitInverted: 0xAA / 0xFF (skip) + // LandscapeCCW: 0x55 / 0xFF (skip) + // LandscapeCW: 0xFF (skip) / 0xAA + switch (orientation) { + case Portrait: + for (int lx = x; lx < x + width; lx++) { + const int phyY = HalDisplay::DISPLAY_HEIGHT - 1 - lx; + if (phyY % 2 == 0) continue; // all-white row — no dark pixels to write + fillPhysicalHSpanByte(phyY, y, y + height - 1, 0x55); + } + return; + case PortraitInverted: + for (int lx = x; lx < x + width; lx++) { + const int phyY = lx; + if (phyY % 2 != 0) continue; // all-white row + fillPhysicalHSpanByte(phyY, HalDisplay::DISPLAY_WIDTH - 1 - (y + height - 1), + HalDisplay::DISPLAY_WIDTH - 1 - y, 0xAA); + } + return; + case LandscapeCounterClockwise: + for (int ly = y; ly < y + height; ly++) { + const int phyY = ly; + if (phyY % 2 != 0) continue; // all-white row + fillPhysicalHSpanByte(phyY, x, x + width - 1, 0x55); + } + return; + case LandscapeClockwise: + for (int ly = y; ly < y + height; ly++) { + const int phyY = HalDisplay::DISPLAY_HEIGHT - 1 - ly; + if (phyY % 2 == 0) continue; // all-white row + fillPhysicalHSpanByte(phyY, HalDisplay::DISPLAY_WIDTH - 1 - (x + width - 1), + HalDisplay::DISPLAY_WIDTH - 1 - x, 0xAA); + } + return; } } } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 618ffd91..e936a613 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -45,8 +45,13 @@ class GfxRenderer { void drawPixelDither(int x, int y) const; template 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. - // phyY: physical row; phyX_start/phyX_end: inclusive physical column range; state: true=dark. + // 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: