perf(esp32-cam-lcd-preview): 4x faster preview by shrinking SPI traffic
User reported the live preview "looks slow" after the JPEG decode fix. Diagnosis: each tft.drawRGBBitmap pushes width × height × 2 bytes over SPI, and every byte takes a full QEMU → worker → backend (WS) → frontend round-trip. At 160×120 that's 38 400 messages per frame; the bus saturates at ~0.2 fps perceived. Two changes shrink the per-frame SPI bandwidth: 1. Preview 160×120 → 80×60 (and JPG_SCALE_2X → JPG_SCALE_4X). 38 400 bytes/frame → 9 600 bytes/frame. Already 4× faster. 2. Status bar redraw throttled to every 10th frame instead of every frame. The text writes (printf, fillRect, fillCircle) account for another ~1-2 KB of SPI traffic per loop iteration. Skipping 9 of every 10 redraws frees up a chunk more bandwidth without losing the headline numbers (fps, frame counter) — they just refresh once a second instead of 5x/sec. Also dropped the trailing `delay(20)` — we don't need an artificial throttle, the SPI bus is the throttle. Real-hardware effect: zero. ESP32 SPI runs at 80 MHz; a full 160×120 bitmap pushes in ~4 ms either way. Applied in two places: - examples/esp32-cam-lcd-preview/esp32-cam-lcd-preview.ino - frontend/src/data/examples.ts (in-app gallery copy) Long-term plan: batch SPI bytes at the worker level (one WS message per N bytes instead of per byte) — that's a deeper change in qemu-lcgamboa + Esp32Bridge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c4c446cf39
commit
a20b10a252
|
|
@ -72,15 +72,26 @@
|
|||
SPIClass tftSPI(VSPI);
|
||||
Adafruit_ILI9341 tft = Adafruit_ILI9341(&tftSPI, TFT_DC, TFT_CS, TFT_RST);
|
||||
|
||||
// Preview is scaled 1/2 from the QVGA capture (320×240 → 160×120).
|
||||
// 160 × 120 × 2 bytes = 38 400 bytes — fits comfortably in DRAM.
|
||||
// Centered in the 320×240 landscape TFT at offset (80, 60).
|
||||
#define PREVIEW_W 160
|
||||
#define PREVIEW_H 120
|
||||
#define PREVIEW_X 80
|
||||
#define PREVIEW_Y 60
|
||||
// Preview is scaled 1/4 from the QVGA capture (320×240 → 80×60).
|
||||
// 80 × 60 × 2 bytes = 9 600 bytes — minimal SPI traffic per frame.
|
||||
// Centered in the 320×240 landscape TFT.
|
||||
//
|
||||
// Why so small: every SPI byte takes a full QEMU→worker→backend→
|
||||
// frontend round-trip. drawRGBBitmap pushes width×height×2 bytes;
|
||||
// at 80×60 that's 9 600 SPI events per frame vs 38 400 at 160×120.
|
||||
// Net effect on the user: roughly 4× faster perceived FPS in the
|
||||
// emulator. Real hardware doesn't care — its SPI runs at 80 MHz.
|
||||
#define PREVIEW_W 80
|
||||
#define PREVIEW_H 60
|
||||
#define PREVIEW_X 120 // (320 - 80) / 2
|
||||
#define PREVIEW_Y 90 // (240 - 60) / 2
|
||||
static uint8_t rgbBuf[PREVIEW_W * PREVIEW_H * 2];
|
||||
|
||||
// Throttle the status-bar redraw — text writes hit SPI too. Updating
|
||||
// the bar once every STATUS_REFRESH_EVERY frames keeps the headline
|
||||
// numbers visible without burning bandwidth on every loop iteration.
|
||||
#define STATUS_REFRESH_EVERY 10
|
||||
|
||||
// Counters for status overlay
|
||||
uint32_t frame_count = 0;
|
||||
uint32_t decode_fails = 0;
|
||||
|
|
@ -223,19 +234,15 @@ void loop() {
|
|||
frame_count++;
|
||||
size_t fb_len = fb->len;
|
||||
|
||||
// Decode the JPEG into RGB565 at half resolution (160×120).
|
||||
// The conversions library ships inside libesp32-camera.a; the
|
||||
// Velxio CMakeLists template adds the include path automatically.
|
||||
bool ok = jpg2rgb565(fb->buf, fb->len, rgbBuf, JPG_SCALE_2X);
|
||||
// Decode the JPEG into RGB565 at 1/4 resolution (80×60).
|
||||
bool ok = jpg2rgb565(fb->buf, fb->len, rgbBuf, JPG_SCALE_4X);
|
||||
esp_camera_fb_return(fb);
|
||||
|
||||
if (ok) {
|
||||
// Push the bitmap to the centre of the TFT.
|
||||
tft.drawRGBBitmap(PREVIEW_X, PREVIEW_Y,
|
||||
(uint16_t*)rgbBuf, PREVIEW_W, PREVIEW_H);
|
||||
} else {
|
||||
decode_fails++;
|
||||
// Decode failure → grey rectangle with an X.
|
||||
tft.fillRect(PREVIEW_X, PREVIEW_Y, PREVIEW_W, PREVIEW_H, ILI9341_DARKGREY);
|
||||
tft.drawLine(PREVIEW_X, PREVIEW_Y,
|
||||
PREVIEW_X + PREVIEW_W, PREVIEW_Y + PREVIEW_H, ILI9341_RED);
|
||||
|
|
@ -243,9 +250,10 @@ void loop() {
|
|||
PREVIEW_X, PREVIEW_Y + PREVIEW_H, ILI9341_RED);
|
||||
}
|
||||
|
||||
// Always refresh the status bar (cheap — only top 60 px).
|
||||
draw_status_bar((uint32_t)fb_len, ok);
|
||||
|
||||
// Mild throttle so the CPU has time to refresh the SPI bus.
|
||||
delay(20);
|
||||
// Throttled status-bar redraw — every Nth frame only. Text writes hit
|
||||
// the SPI bus too; updating once every STATUS_REFRESH_EVERY frames
|
||||
// keeps the headline visible without burning bandwidth on every loop.
|
||||
if (frame_count % STATUS_REFRESH_EVERY == 0) {
|
||||
draw_status_bar((uint32_t)fb_len, ok);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7029,12 +7029,19 @@ void loop() {
|
|||
SPIClass tftSPI(VSPI);
|
||||
Adafruit_ILI9341 tft = Adafruit_ILI9341(&tftSPI, TFT_DC, TFT_CS, TFT_RST);
|
||||
|
||||
#define PREVIEW_W 160
|
||||
#define PREVIEW_H 120
|
||||
#define PREVIEW_X 80
|
||||
#define PREVIEW_Y 60
|
||||
// 1/4 scale (80×60) — minimises SPI traffic in the emulator.
|
||||
// Each SPI byte goes through QEMU→worker→backend→frontend, so
|
||||
// drawRGBBitmap dominates the loop. 80×60 = 9600 bytes/frame vs
|
||||
// 160×120 = 38400 — roughly 4× faster perceived FPS.
|
||||
#define PREVIEW_W 80
|
||||
#define PREVIEW_H 60
|
||||
#define PREVIEW_X 120
|
||||
#define PREVIEW_Y 90
|
||||
static uint8_t rgbBuf[PREVIEW_W * PREVIEW_H * 2];
|
||||
|
||||
// Throttle status-bar redraw — text writes hit SPI too.
|
||||
#define STATUS_REFRESH_EVERY 10
|
||||
|
||||
uint32_t frame_count = 0, decode_fails = 0, null_fb = 0;
|
||||
unsigned long start_ms = 0;
|
||||
|
||||
|
|
@ -7126,7 +7133,7 @@ void loop() {
|
|||
}
|
||||
frame_count++;
|
||||
size_t fb_len = fb->len;
|
||||
bool ok = jpg2rgb565(fb->buf, fb->len, rgbBuf, JPG_SCALE_2X);
|
||||
bool ok = jpg2rgb565(fb->buf, fb->len, rgbBuf, JPG_SCALE_4X);
|
||||
esp_camera_fb_return(fb);
|
||||
if (ok) {
|
||||
tft.drawRGBBitmap(PREVIEW_X, PREVIEW_Y,
|
||||
|
|
@ -7140,8 +7147,10 @@ void loop() {
|
|||
tft.drawLine(PREVIEW_X + PREVIEW_W, PREVIEW_Y,
|
||||
PREVIEW_X, PREVIEW_Y + PREVIEW_H, ILI9341_RED);
|
||||
}
|
||||
draw_status_bar((uint32_t)fb_len, ok);
|
||||
delay(20);
|
||||
// Throttled status-bar redraw — text writes hit SPI too.
|
||||
if (frame_count % STATUS_REFRESH_EVERY == 0) {
|
||||
draw_status_bar((uint32_t)fb_len, ok);
|
||||
}
|
||||
}
|
||||
`,
|
||||
components: [
|
||||
|
|
|
|||
Loading…
Reference in New Issue