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:
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-10-19 — GIF 去闪屏
|
||||
|
||||
- 双缓冲指针交换发布帧,避免 memcpy 半帧撕裂。
|
||||
- GIF 用 2× 整数缩放刷新;切图不再每帧 `set_src(nullptr)`。
|
||||
|
||||
## 2026-10-19 — 去掉图后条纹
|
||||
|
||||
- 样张改为纯色底,去掉装饰横纹。
|
||||
|
||||
@@ -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.
@@ -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;
|
||||
}
|
||||
|
||||
21
src/ui.cpp
21
src/ui.cpp
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user