diff --git a/CHANGELOG.md b/CHANGELOG.md index b7e4954..9dfbb60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 2026-10-19 — GIF 去闪屏 + +- 双缓冲指针交换发布帧,避免 memcpy 半帧撕裂。 +- GIF 用 2× 整数缩放刷新;切图不再每帧 `set_src(nullptr)`。 + ## 2026-10-19 — 去掉图后条纹 - 样张改为纯色底,去掉装饰横纹。 diff --git a/firmware/cyd/VERSION.txt b/firmware/cyd/VERSION.txt index 51f699b..3cb2bb6 100644 --- a/firmware/cyd/VERSION.txt +++ b/firmware/cyd/VERSION.txt @@ -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 diff --git a/firmware/cyd/firmware.bin b/firmware/cyd/firmware.bin index 81648f2..dcc4f96 100644 Binary files a/firmware/cyd/firmware.bin and b/firmware/cyd/firmware.bin differ diff --git a/src/photo_view.cpp b/src/photo_view.cpp index 3367409..cecbdc1 100644 --- a/src/photo_view.cpp +++ b/src/photo_view.cpp @@ -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; } diff --git a/src/ui.cpp b/src/ui.cpp index b5af1a0..df92f33 100644 --- a/src/ui.cpp +++ b/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); } }