Eliminate GIF flicker with double-buffer pointer swap.

Publish frames by swapping show/draw buffers and use integer 2x scale instead of full-screen stretch redraws.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-19 18:23:01 +08:00
parent f27252c80b
commit 5475be8e84
5 changed files with 83 additions and 35 deletions

View File

@@ -16,7 +16,10 @@ static constexpr uint16_t CANVAS_W = UI_HOR_RES / 2;
static constexpr uint16_t CANVAS_H = UI_VER_RES / 2;
static constexpr size_t CANVAS_PIXELS = (size_t)CANVAS_W * (size_t)CANVAS_H;
static uint16_t g_pixels[CANVAS_PIXELS];
static uint16_t g_pixels[CANVAS_PIXELS]; // buffer A
static uint16_t *g_buf_b = nullptr; // buffer B (heap, GIF double-buffer)
static uint16_t *g_show = g_pixels; // LVGL reads this
static uint16_t *g_draw = g_pixels; // GIF decoder writes this
static bool g_ready = false;
static lv_image_dsc_t g_dsc;
static uint16_t g_img_w = 0;
@@ -36,21 +39,28 @@ static int g_dst_h = 0;
// GIF worker (core 0): decode must not block LVGL/touch on core 1.
static TaskHandle_t g_gif_task = nullptr;
static uint16_t *g_back = nullptr; // decode target (heap)
static uint16_t *g_draw_dst = g_pixels; // where gif_draw writes
static uint16_t *g_draw_dst = g_pixels; // alias used by gif_draw
static char g_gif_path[PHOTO_PATH_MAX];
static volatile bool g_gif_active = false; // session open (UI-visible)
static volatile bool g_gif_busy = false; // worker running
static volatile bool g_gif_stop = false; // request abort
static volatile bool g_frame_ready = false; // back buffer has a new frame
static volatile bool g_gif_active = false;
static volatile bool g_gif_busy = false;
static volatile bool g_gif_stop = false;
static volatile bool g_frame_ready = false;
static void canvas_clear_buf(uint16_t *buf, uint16_t color) {
if (!buf) {
return;
}
for (size_t i = 0; i < CANVAS_PIXELS; i++) {
buf[i] = color;
}
}
static void canvas_clear(uint16_t color) { canvas_clear_buf(g_pixels, color); }
static void canvas_clear(uint16_t color) {
g_show = g_pixels;
g_draw = g_pixels;
g_draw_dst = g_pixels;
canvas_clear_buf(g_pixels, color);
}
static void canvas_sync_dsc() {
g_dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
@@ -60,7 +70,7 @@ static void canvas_sync_dsc() {
g_dsc.header.h = CANVAS_H;
g_dsc.header.stride = CANVAS_W * 2;
g_dsc.data_size = CANVAS_PIXELS * 2;
g_dsc.data = (const uint8_t *)g_pixels;
g_dsc.data = (const uint8_t *)g_show;
g_dsc.reserved = nullptr;
}
@@ -324,7 +334,10 @@ static void gif_worker_cleanup() {
delete g_gif;
g_gif = nullptr;
}
g_show = g_pixels;
g_draw = g_pixels;
g_draw_dst = g_pixels;
g_dsc.data = (const uint8_t *)g_show;
g_frame_ready = false;
g_gif_busy = false;
g_gif_active = false;
@@ -337,9 +350,9 @@ static void gif_task(void * /*arg*/) {
g_gif_stop = false;
g_frame_ready = false;
if (!g_back) {
g_back = (uint16_t *)malloc(CANVAS_PIXELS * sizeof(uint16_t));
if (!g_back) {
if (!g_buf_b) {
g_buf_b = (uint16_t *)malloc(CANVAS_PIXELS * sizeof(uint16_t));
if (!g_buf_b) {
Serial.println("GIF: back buffer alloc failed");
g_gif_busy = false;
g_gif_active = false;
@@ -368,7 +381,6 @@ static void gif_task(void * /*arg*/) {
continue;
}
// Cover full canvas (sample source into every dest pixel).
g_dst_w = CANVAS_W;
g_dst_h = CANVAS_H;
g_img_w = CANVAS_W;
@@ -376,8 +388,13 @@ static void gif_task(void * /*arg*/) {
g_off_x = 0;
g_off_y = 0;
canvas_clear_buf(g_back, 0x0000);
g_draw_dst = g_back;
// Double-buffer: show A, decode into B (B starts as copy of A).
g_show = g_pixels;
g_draw = g_buf_b;
canvas_clear_buf(g_show, 0x0000);
memcpy(g_draw, g_show, CANVAS_PIXELS * sizeof(uint16_t));
g_draw_dst = g_draw;
g_dsc.data = (const uint8_t *)g_show;
g_gif_active = true;
while (!g_gif_stop) {
@@ -389,16 +406,17 @@ static void gif_task(void * /*arg*/) {
}
g_frame_ready = true;
// Wait until UI consumed the frame (or abort).
// Wait until UI swapped buffers (or abort).
while (g_frame_ready && !g_gif_stop) {
vTaskDelay(pdMS_TO_TICKS(2));
vTaskDelay(pdMS_TO_TICKS(1));
}
if (g_gif_stop) {
break;
}
if (delay_ms < 30) {
delay_ms = 30;
// Keep GIF timing; floor avoids hammering LVGL refresh.
if (delay_ms < 40) {
delay_ms = 40;
}
if (delay_ms > 200) {
delay_ms = 200;
@@ -531,28 +549,26 @@ static bool load_gif(const char *path) {
strncpy(g_gif_path, path, sizeof(g_gif_path) - 1);
g_gif_path[sizeof(g_gif_path) - 1] = '\0';
// Show black immediately; worker fills frames async.
canvas_clear(0x0000);
canvas_sync_dsc();
g_gif_stop = false;
xTaskNotifyGive(g_gif_task);
// Brief wait for first frame (non-blocking beyond a short timeout).
// Wait for first composed frame, then publish via pointer swap (no mid-frame tear).
const uint32_t t0 = millis();
while ((millis() - t0) < 250) {
while ((millis() - t0) < 300) {
if (g_frame_ready) {
break;
}
// Worker finished with no frame => open/decode failed
if ((millis() - t0) > 40 && !g_gif_busy && !g_gif_active) {
return false;
}
vTaskDelay(pdMS_TO_TICKS(5));
}
if (g_frame_ready && g_back) {
memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t));
g_frame_ready = false;
if (g_frame_ready) {
photo_view_gif_tick();
}
canvas_sync_dsc();
return g_gif_active || g_gif_busy;
}
@@ -574,10 +590,13 @@ void photo_view_deinit() {
photo_view_stop();
free_jpeg();
free_png();
if (g_back) {
free(g_back);
g_back = nullptr;
if (g_buf_b) {
free(g_buf_b);
g_buf_b = nullptr;
}
g_show = g_pixels;
g_draw = g_pixels;
g_draw_dst = g_pixels;
g_ready = false;
g_dsc.data = nullptr;
}
@@ -617,10 +636,17 @@ uint16_t photo_view_img_h() { return g_img_h; }
bool photo_view_is_gif() { return g_gif_active || g_gif_busy; }
bool photo_view_gif_tick() {
if (!g_frame_ready || !g_back) {
if (!g_frame_ready || !g_buf_b) {
return false;
}
memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t));
// Publish completed frame by swapping pointers (display never sees a half-copy).
uint16_t *tmp = g_show;
g_show = g_draw;
g_draw = tmp;
g_dsc.data = (const uint8_t *)g_show;
g_draw_dst = g_draw;
// Next decode must start from the frame we just showed (disposal / transparency).
memcpy(g_draw, g_show, CANVAS_PIXELS * sizeof(uint16_t));
g_frame_ready = false;
return true;
}