perf: Eliminate per-pixel overheads in image rendering (#1293)

## 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 >**_
This commit is contained in:
martin brook
2026-03-30 11:03:49 -05:00
committed by GitHub
parent d12f0666d4
commit aa085425af
5 changed files with 215 additions and 32 deletions
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <new>
#include "DirectPixelWriter.h"
#include "DitherUtils.h"
#include "PixelCache.h"
@@ -167,10 +168,21 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
if (dstYStart >= dstYEnd || dstXStart >= dstXEnd) return 1;
// Pre-compute orientation and render-mode state once per callback invocation
DirectPixelWriter pw;
pw.init(renderer);
DirectCacheWriter cw;
if (caching) {
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
}
// === 1:1 fast path: no scaling math ===
if (fineScaleFP == FP_ONE) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const uint8_t* row = &pixels[(dstY - blockY) * stride];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
@@ -182,8 +194,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
return 1;
@@ -203,6 +215,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
const int32_t fy = srcFyFP & FP_MASK;
const int32_t fyInv = FP_ONE - fy;
@@ -239,8 +253,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
@@ -262,8 +276,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
// Right edge (with X boundary clamping)
@@ -288,8 +302,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
return 1;
@@ -298,6 +312,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
int ly = (srcFyFP >> FP_SHIFT) - blockY;
if (ly < 0) ly = 0;
@@ -319,8 +335,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}