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

@@ -1,5 +1,10 @@
# Changelog
## 2026-10-19 — GIF 去闪屏
- 双缓冲指针交换发布帧,避免 memcpy 半帧撕裂。
- GIF 用 2× 整数缩放刷新;切图不再每帧 `set_src(nullptr)`。
## 2026-10-19 — 去掉图后条纹
- 样张改为纯色底,去掉装饰横纹。

View File

@@ -1,5 +1,5 @@
name: CYD Photo album (cyd)
git: 6cf42d5
git: f27252c
date: 2026-10-19
board: ESP32-2432S028R (ILI9341)
features: jpg/png/gif album, SoftSPI touch, TF card
@@ -12,5 +12,5 @@ offsets:
sha256:
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
firmware.bin 8BAFA46DA3DF4BF3A15D5B67E3062B80E0D8576A2F59BE2101431747E24438AF
firmware.bin 2A437103D8933CA6ACDAA139336A68A227053E0B73D44FFA10FBB5115F6C7328
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1

Binary file not shown.

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;
}

View File

@@ -85,11 +85,27 @@ static void show_current() {
const PhotoEntry *e = photo_list_get((uint16_t)g_index);
const bool ok = e && photo_view_show(e);
if (img_photo) {
const bool gif = ok && photo_view_is_gif();
// GIF: integer 2x scale (cheap, less tear). Still: full-screen stretch.
if (gif) {
lv_image_set_inner_align(img_photo, LV_IMAGE_ALIGN_DEFAULT);
lv_image_set_scale(img_photo, 512); // 256=1x
lv_image_set_pivot(img_photo, photo_view_canvas_w() / 2,
photo_view_canvas_h() / 2);
lv_obj_set_size(img_photo, photo_view_canvas_w(), photo_view_canvas_h());
lv_obj_align(img_photo, LV_ALIGN_CENTER, 0, 0);
} else {
lv_image_set_scale(img_photo, 256);
lv_image_set_inner_align(img_photo, LV_IMAGE_ALIGN_STRETCH);
lv_obj_set_size(img_photo, UI_HOR_RES, UI_VER_RES);
lv_obj_set_pos(img_photo, 0, 0);
}
const lv_image_dsc_t *dsc = photo_view_dsc();
// Force refresh even when dsc pointer is unchanged
lv_image_set_src(img_photo, nullptr);
if (ok && dsc) {
lv_image_set_src(img_photo, dsc);
} else {
lv_image_set_src(img_photo, nullptr);
}
lv_obj_invalidate(img_photo);
}
@@ -279,6 +295,7 @@ void ui_create() {
void ui_tick() {
if (photo_view_gif_tick() && img_photo) {
// dsc.data already swapped; invalidate only — do not reset src (causes flash).
lv_obj_invalidate(img_photo);
}
}