## Summary Replace per-pixel getRenderMode() + rotateCoordinates() + bounds checks with a DirectPixelWriter struct that pre-computes orientation and render mode state once per row. Use bitwise ops instead of division/modulo for cache pixel packing. Skip PNG cache allocation when buffer exceeds 48KB (framebuffer size) since PNG decode is fast enough that caching provides minimal benefit, and the large buffer competes with the 44KB PNG decoder for heap. ## Additional Context Measured improvements on ESP32-C3 @ 160MHz: - JPEG decode: 5-7% faster (1:1 scale) - PNG decode: 15-20% faster (1:1 scale) - Cache renders: 3-6% faster across both formats - Eliminates "Failed to allocate cache buffer" errors for large PNGs --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_
157 lines
5.0 KiB
C
157 lines
5.0 KiB
C
#pragma once
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <HalDisplay.h>
|
|
#include <stdint.h>
|
|
|
|
// Direct framebuffer writer that eliminates per-pixel overhead from the image
|
|
// rendering hot path. Pre-computes orientation transform as linear coefficients
|
|
// and caches render-mode state so the inner loop is: one multiply, one add,
|
|
// one shift, and one AND per pixel — no branches, no method calls.
|
|
//
|
|
// Caller is responsible for ensuring (outX, outY) are within screen bounds.
|
|
// ImageBlock::render() already validates this before entering the pixel loop,
|
|
// and the JPEG/PNG callbacks pre-clamp destination ranges to screen bounds.
|
|
struct DirectPixelWriter {
|
|
uint8_t* fb;
|
|
GfxRenderer::RenderMode mode;
|
|
|
|
// Orientation is collapsed into a linear transform:
|
|
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
|
// phyY = phyYBase + x * phyYStepX + y * phyYStepY
|
|
int phyXBase, phyYBase;
|
|
int phyXStepX, phyYStepX; // per logical-X step
|
|
int phyXStepY, phyYStepY; // per logical-Y step
|
|
|
|
// Row-precomputed: the Y-dependent portion of the physical coords
|
|
int rowPhyXBase, rowPhyYBase;
|
|
|
|
void init(GfxRenderer& renderer) {
|
|
fb = renderer.getFrameBuffer();
|
|
mode = renderer.getRenderMode();
|
|
|
|
switch (renderer.getOrientation()) {
|
|
case GfxRenderer::Portrait:
|
|
// phyX = y, phyY = (DISPLAY_HEIGHT-1) - x
|
|
phyXBase = 0;
|
|
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
|
|
phyXStepX = 0;
|
|
phyYStepX = -1;
|
|
phyXStepY = 1;
|
|
phyYStepY = 0;
|
|
break;
|
|
case GfxRenderer::LandscapeClockwise:
|
|
// phyX = (DISPLAY_WIDTH-1) - x, phyY = (DISPLAY_HEIGHT-1) - y
|
|
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
|
|
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
|
|
phyXStepX = -1;
|
|
phyYStepX = 0;
|
|
phyXStepY = 0;
|
|
phyYStepY = -1;
|
|
break;
|
|
case GfxRenderer::PortraitInverted:
|
|
// phyX = (DISPLAY_WIDTH-1) - y, phyY = x
|
|
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
|
|
phyYBase = 0;
|
|
phyXStepX = 0;
|
|
phyYStepX = 1;
|
|
phyXStepY = -1;
|
|
phyYStepY = 0;
|
|
break;
|
|
case GfxRenderer::LandscapeCounterClockwise:
|
|
// phyX = x, phyY = y
|
|
phyXBase = 0;
|
|
phyYBase = 0;
|
|
phyXStepX = 1;
|
|
phyYStepX = 0;
|
|
phyXStepY = 0;
|
|
phyYStepY = 1;
|
|
break;
|
|
default:
|
|
// Fallback to LandscapeCounterClockwise (identity transform)
|
|
phyXBase = 0;
|
|
phyYBase = 0;
|
|
phyXStepX = 1;
|
|
phyYStepX = 0;
|
|
phyXStepY = 0;
|
|
phyYStepY = 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Call once per row before the column loop.
|
|
// Pre-computes the Y-dependent portion so writePixel() only needs the X part.
|
|
inline void beginRow(int logicalY) {
|
|
rowPhyXBase = phyXBase + logicalY * phyXStepY;
|
|
rowPhyYBase = phyYBase + logicalY * phyYStepY;
|
|
}
|
|
|
|
// Write a single 2-bit dithered pixel value to the framebuffer.
|
|
// Must be called after beginRow() for the current row.
|
|
// No bounds checking — caller guarantees coordinates are valid.
|
|
inline void writePixel(int logicalX, uint8_t pixelValue) const {
|
|
// Determine whether to draw based on render mode
|
|
bool draw;
|
|
bool state;
|
|
switch (mode) {
|
|
case GfxRenderer::BW:
|
|
draw = (pixelValue < 3);
|
|
state = true;
|
|
break;
|
|
case GfxRenderer::GRAYSCALE_MSB:
|
|
draw = (pixelValue == 1 || pixelValue == 2);
|
|
state = false;
|
|
break;
|
|
case GfxRenderer::GRAYSCALE_LSB:
|
|
draw = (pixelValue == 1);
|
|
state = false;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if (!draw) return;
|
|
|
|
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
|
const int phyY = rowPhyYBase + logicalX * phyYStepX;
|
|
|
|
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX >> 3);
|
|
const uint8_t bitMask = 1 << (7 - (phyX & 7));
|
|
|
|
if (state) {
|
|
fb[byteIndex] &= ~bitMask; // Clear bit (draw black)
|
|
} else {
|
|
fb[byteIndex] |= bitMask; // Set bit (draw white)
|
|
}
|
|
}
|
|
};
|
|
|
|
// Direct cache writer that eliminates per-pixel overhead from PixelCache::setPixel().
|
|
// Pre-computes row pointer so the inner loop is just byte index + bit manipulation.
|
|
//
|
|
// Caller guarantees coordinates are within cache bounds.
|
|
struct DirectCacheWriter {
|
|
uint8_t* buffer;
|
|
int bytesPerRow;
|
|
int originX;
|
|
uint8_t* rowPtr; // Pre-computed for current row
|
|
|
|
void init(uint8_t* cacheBuffer, int cacheBytesPerRow, int cacheOriginX) {
|
|
buffer = cacheBuffer;
|
|
bytesPerRow = cacheBytesPerRow;
|
|
originX = cacheOriginX;
|
|
rowPtr = nullptr;
|
|
}
|
|
|
|
// Call once per row before the column loop.
|
|
inline void beginRow(int screenY, int cacheOriginY) { rowPtr = buffer + (screenY - cacheOriginY) * bytesPerRow; }
|
|
|
|
// Write a 2-bit pixel value. No bounds checking.
|
|
inline void writePixel(int screenX, uint8_t value) const {
|
|
const int localX = screenX - originX;
|
|
const int byteIdx = localX >> 2; // localX / 4
|
|
const int bitShift = 6 - (localX & 3) * 2; // MSB first: pixel 0 at bits 6-7
|
|
rowPtr[byteIdx] = (rowPtr[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
|
}
|
|
};
|