Fix photo stripes and keep GIF playback off the UI thread.
Use cover/stretch display, solid sample assets, core0 GIF decode without blocking touch, and always-commit project rule. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,6 +8,9 @@
|
||||
#include <SD.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
// Half-res canvas (fits ESP32 DRAM). UI shows it at 2x -> 320x240.
|
||||
static constexpr uint16_t CANVAS_W = UI_HOR_RES / 2;
|
||||
static constexpr uint16_t CANVAS_H = UI_VER_RES / 2;
|
||||
@@ -24,8 +27,6 @@ static PNG *g_png = nullptr;
|
||||
static AnimatedGIF *g_gif = nullptr;
|
||||
|
||||
static File g_file;
|
||||
static bool g_gif_open = false;
|
||||
static uint32_t g_gif_next_ms = 0;
|
||||
static int g_off_x = 0;
|
||||
static int g_off_y = 0;
|
||||
static int g_src_w = 0;
|
||||
@@ -33,12 +34,24 @@ static int g_src_h = 0;
|
||||
static int g_dst_w = 0;
|
||||
static int g_dst_h = 0;
|
||||
|
||||
static void canvas_clear(uint16_t color) {
|
||||
// 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 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 void canvas_clear_buf(uint16_t *buf, uint16_t color) {
|
||||
for (size_t i = 0; i < CANVAS_PIXELS; i++) {
|
||||
g_pixels[i] = color;
|
||||
buf[i] = color;
|
||||
}
|
||||
}
|
||||
|
||||
static void canvas_clear(uint16_t color) { canvas_clear_buf(g_pixels, color); }
|
||||
|
||||
static void canvas_sync_dsc() {
|
||||
g_dsc.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
g_dsc.header.cf = LV_COLOR_FORMAT_RGB565;
|
||||
@@ -65,47 +78,40 @@ static void free_png() {
|
||||
}
|
||||
}
|
||||
|
||||
static void gif_close_session() {
|
||||
if (g_gif_open && g_gif) {
|
||||
g_gif->close();
|
||||
g_gif_open = false;
|
||||
// Tightest JPEGDEC scale that still covers the canvas (center-cropped).
|
||||
static int jpeg_scale_cover(int w, int h, int *dw, int *dh) {
|
||||
const int scales[] = {0, JPEG_SCALE_HALF, JPEG_SCALE_QUARTER, JPEG_SCALE_EIGHTH};
|
||||
const int divs[] = {1, 2, 4, 8};
|
||||
int best = 0;
|
||||
int best_dw = w;
|
||||
int best_dh = h;
|
||||
int best_area = 0x7fffffff;
|
||||
bool found = false;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
const int dw_i = w / divs[i];
|
||||
const int dh_i = h / divs[i];
|
||||
if (dw_i < 1 || dh_i < 1) {
|
||||
continue;
|
||||
}
|
||||
if (dw_i >= (int)CANVAS_W && dh_i >= (int)CANVAS_H) {
|
||||
const int area = dw_i * dh_i;
|
||||
if (area < best_area) {
|
||||
best_area = area;
|
||||
best = scales[i];
|
||||
best_dw = dw_i;
|
||||
best_dh = dh_i;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (g_gif) {
|
||||
delete g_gif;
|
||||
g_gif = nullptr;
|
||||
if (!found) {
|
||||
best = 0;
|
||||
best_dw = w;
|
||||
best_dh = h;
|
||||
}
|
||||
}
|
||||
|
||||
static void fit_size(int nw, int nh, int *dw, int *dh) {
|
||||
*dw = nw;
|
||||
*dh = nh;
|
||||
if (*dw <= (int)CANVAS_W && *dh <= (int)CANVAS_H) {
|
||||
return;
|
||||
}
|
||||
const float sx = (float)CANVAS_W / (float)nw;
|
||||
const float sy = (float)CANVAS_H / (float)nh;
|
||||
const float s = sx < sy ? sx : sy;
|
||||
*dw = (int)(nw * s + 0.5f);
|
||||
*dh = (int)(nh * s + 0.5f);
|
||||
if (*dw < 1) {
|
||||
*dw = 1;
|
||||
}
|
||||
if (*dh < 1) {
|
||||
*dh = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int jpeg_scale_for(int w, int h) {
|
||||
if (w > CANVAS_W * 4 || h > CANVAS_H * 4) {
|
||||
return JPEG_SCALE_EIGHTH;
|
||||
}
|
||||
if (w > CANVAS_W * 2 || h > CANVAS_H * 2) {
|
||||
return JPEG_SCALE_QUARTER;
|
||||
}
|
||||
if (w > CANVAS_W || h > CANVAS_H) {
|
||||
return JPEG_SCALE_HALF;
|
||||
}
|
||||
return 0;
|
||||
*dw = best_dw;
|
||||
*dh = best_dh;
|
||||
return best;
|
||||
}
|
||||
|
||||
static void *jpeg_open(const char *filename, int32_t *size) {
|
||||
@@ -192,34 +198,32 @@ static int png_draw(PNGDRAW *p) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Line buffer for source width (cap to avoid stack blow)
|
||||
static uint16_t line[640];
|
||||
if (p->iWidth > 640) {
|
||||
return 0;
|
||||
}
|
||||
g_png->getLineAsRGB565(p, line, PNG_RGB565_LITTLE_ENDIAN, 0x0000);
|
||||
|
||||
// Nearest-neighbor: which dest rows map from this source row?
|
||||
for (int dy = 0; dy < g_dst_h; dy++) {
|
||||
const int sy = (dy * g_src_h) / g_dst_h;
|
||||
if (sy != p->y) {
|
||||
continue;
|
||||
}
|
||||
int dy = 0;
|
||||
while (dy < g_dst_h && (dy * g_src_h) / g_dst_h < p->y) {
|
||||
dy++;
|
||||
}
|
||||
while (dy < g_dst_h && (dy * g_src_h) / g_dst_h == p->y) {
|
||||
const int y = g_off_y + dy;
|
||||
if (y < 0 || y >= (int)CANVAS_H) {
|
||||
continue;
|
||||
}
|
||||
for (int dx = 0; dx < g_dst_w; dx++) {
|
||||
const int sx = (dx * g_src_w) / g_dst_w;
|
||||
if (sx < 0 || sx >= p->iWidth) {
|
||||
continue;
|
||||
if (y >= 0 && y < (int)CANVAS_H) {
|
||||
for (int dx = 0; dx < g_dst_w; dx++) {
|
||||
const int sx = (dx * g_src_w) / g_dst_w;
|
||||
if (sx < 0 || sx >= p->iWidth) {
|
||||
continue;
|
||||
}
|
||||
const int x = g_off_x + dx;
|
||||
if (x < 0 || x >= (int)CANVAS_W) {
|
||||
continue;
|
||||
}
|
||||
g_pixels[(size_t)y * CANVAS_W + (size_t)x] = line[sx];
|
||||
}
|
||||
const int x = g_off_x + dx;
|
||||
if (x < 0 || x >= (int)CANVAS_W) {
|
||||
continue;
|
||||
}
|
||||
g_pixels[(size_t)y * CANVAS_W + (size_t)x] = line[sx];
|
||||
}
|
||||
dy++;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -258,41 +262,182 @@ static int32_t gif_seek(GIFFILE *pFile, int32_t iPosition) {
|
||||
}
|
||||
|
||||
static void gif_draw(GIFDRAW *pDraw) {
|
||||
if (!pDraw || g_src_w <= 0 || g_dst_w <= 0) {
|
||||
if (!pDraw || !g_draw_dst || g_src_w <= 0 || g_dst_w <= 0) {
|
||||
return;
|
||||
}
|
||||
// Map source frame lines into scaled dest canvas
|
||||
if (g_gif_stop) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int src_y = pDraw->iY + pDraw->y;
|
||||
for (int dy = 0; dy < g_dst_h; dy++) {
|
||||
const int sy = (dy * g_src_h) / g_dst_h;
|
||||
if (sy != src_y) {
|
||||
continue;
|
||||
}
|
||||
const int y = g_off_y + dy;
|
||||
uint16_t *dst = g_draw_dst;
|
||||
|
||||
// Fast path: 1:1 into canvas region
|
||||
if (g_src_w == g_dst_w && g_src_h == g_dst_h) {
|
||||
const int y = g_off_y + src_y;
|
||||
if (y < 0 || y >= (int)CANVAS_H) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
for (int dx = 0; dx < g_dst_w; dx++) {
|
||||
const int sx = (dx * g_src_w) / g_dst_w;
|
||||
const int lx = sx - pDraw->iX;
|
||||
if (lx < 0 || lx >= pDraw->iWidth) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t idx = pDraw->pPixels[lx];
|
||||
uint16_t *row = dst + (size_t)y * CANVAS_W + (size_t)g_off_x + pDraw->iX;
|
||||
for (int x = 0; x < pDraw->iWidth; x++) {
|
||||
const uint8_t idx = pDraw->pPixels[x];
|
||||
if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) {
|
||||
continue;
|
||||
}
|
||||
const int x = g_off_x + dx;
|
||||
if (x < 0 || x >= (int)CANVAS_W) {
|
||||
row[x] = pDraw->pPalette[idx];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Scaled: only dest rows that sample this source line
|
||||
int dy = 0;
|
||||
while (dy < g_dst_h && (dy * g_src_h) / g_dst_h < src_y) {
|
||||
dy++;
|
||||
}
|
||||
while (dy < g_dst_h && (dy * g_src_h) / g_dst_h == src_y) {
|
||||
const int y = g_off_y + dy;
|
||||
if (y >= 0 && y < (int)CANVAS_H) {
|
||||
for (int dx = 0; dx < g_dst_w; dx++) {
|
||||
const int sx = (dx * g_src_w) / g_dst_w;
|
||||
const int lx = sx - pDraw->iX;
|
||||
if (lx < 0 || lx >= pDraw->iWidth) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t idx = pDraw->pPixels[lx];
|
||||
if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) {
|
||||
continue;
|
||||
}
|
||||
const int x = g_off_x + dx;
|
||||
if (x < 0 || x >= (int)CANVAS_W) {
|
||||
continue;
|
||||
}
|
||||
dst[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx];
|
||||
}
|
||||
}
|
||||
dy++;
|
||||
}
|
||||
}
|
||||
|
||||
static void gif_worker_cleanup() {
|
||||
if (g_gif) {
|
||||
g_gif->close();
|
||||
delete g_gif;
|
||||
g_gif = nullptr;
|
||||
}
|
||||
g_draw_dst = g_pixels;
|
||||
g_frame_ready = false;
|
||||
g_gif_busy = false;
|
||||
g_gif_active = false;
|
||||
}
|
||||
|
||||
static void gif_task(void * /*arg*/) {
|
||||
for (;;) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
g_gif_busy = true;
|
||||
g_gif_stop = false;
|
||||
g_frame_ready = false;
|
||||
|
||||
if (!g_back) {
|
||||
g_back = (uint16_t *)malloc(CANVAS_PIXELS * sizeof(uint16_t));
|
||||
if (!g_back) {
|
||||
Serial.println("GIF: back buffer alloc failed");
|
||||
g_gif_busy = false;
|
||||
g_gif_active = false;
|
||||
continue;
|
||||
}
|
||||
g_pixels[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx];
|
||||
}
|
||||
|
||||
g_gif = new AnimatedGIF();
|
||||
if (!g_gif) {
|
||||
gif_worker_cleanup();
|
||||
continue;
|
||||
}
|
||||
|
||||
g_gif->begin(GIF_PALETTE_RGB565_LE);
|
||||
if (!g_gif->open(g_gif_path, gif_open, gif_close, gif_read, gif_seek,
|
||||
gif_draw)) {
|
||||
Serial.printf("GIF open fail: %s\n", g_gif_path);
|
||||
gif_worker_cleanup();
|
||||
continue;
|
||||
}
|
||||
|
||||
g_src_w = g_gif->getCanvasWidth();
|
||||
g_src_h = g_gif->getCanvasHeight();
|
||||
if (g_src_w <= 0 || g_src_h <= 0) {
|
||||
gif_worker_cleanup();
|
||||
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;
|
||||
g_img_h = CANVAS_H;
|
||||
g_off_x = 0;
|
||||
g_off_y = 0;
|
||||
|
||||
canvas_clear_buf(g_back, 0x0000);
|
||||
g_draw_dst = g_back;
|
||||
g_gif_active = true;
|
||||
|
||||
while (!g_gif_stop) {
|
||||
int delay_ms = 0;
|
||||
// bSync=false: never delay() inside the library (that froze touch).
|
||||
g_gif->playFrame(false, &delay_ms);
|
||||
if (g_gif_stop) {
|
||||
break;
|
||||
}
|
||||
|
||||
g_frame_ready = true;
|
||||
// Wait until UI consumed the frame (or abort).
|
||||
while (g_frame_ready && !g_gif_stop) {
|
||||
vTaskDelay(pdMS_TO_TICKS(2));
|
||||
}
|
||||
if (g_gif_stop) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (delay_ms < 30) {
|
||||
delay_ms = 30;
|
||||
}
|
||||
if (delay_ms > 200) {
|
||||
delay_ms = 200;
|
||||
}
|
||||
int waited = 0;
|
||||
while (waited < delay_ms && !g_gif_stop) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
waited += 5;
|
||||
}
|
||||
}
|
||||
|
||||
gif_worker_cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
static void gif_ensure_task() {
|
||||
if (g_gif_task) {
|
||||
return;
|
||||
}
|
||||
xTaskCreatePinnedToCore(gif_task, "gif", 8192, nullptr, 1, &g_gif_task, 0);
|
||||
}
|
||||
|
||||
static void gif_stop_and_wait() {
|
||||
g_gif_stop = true;
|
||||
g_frame_ready = false;
|
||||
const uint32_t t0 = millis();
|
||||
while (g_gif_busy && (millis() - t0) < 800) {
|
||||
vTaskDelay(pdMS_TO_TICKS(5));
|
||||
}
|
||||
// If worker hung, force local cleanup of flags (file may still be open briefly)
|
||||
if (g_gif_busy) {
|
||||
Serial.println("GIF: stop timeout");
|
||||
g_gif_busy = false;
|
||||
g_gif_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool load_jpeg(const char *path) {
|
||||
gif_close_session();
|
||||
gif_stop_and_wait();
|
||||
free_png();
|
||||
free_jpeg();
|
||||
|
||||
@@ -310,22 +455,13 @@ static bool load_jpeg(const char *path) {
|
||||
|
||||
const int nw = g_jpeg->getWidth();
|
||||
const int nh = g_jpeg->getHeight();
|
||||
const int scale = jpeg_scale_for(nw, nh);
|
||||
int dw = nw;
|
||||
int dh = nh;
|
||||
if (scale == JPEG_SCALE_HALF) {
|
||||
dw /= 2;
|
||||
dh /= 2;
|
||||
} else if (scale == JPEG_SCALE_QUARTER) {
|
||||
dw /= 4;
|
||||
dh /= 4;
|
||||
} else if (scale == JPEG_SCALE_EIGHTH) {
|
||||
dw /= 8;
|
||||
dh /= 8;
|
||||
}
|
||||
int dw = 0;
|
||||
int dh = 0;
|
||||
const int scale = jpeg_scale_cover(nw, nh, &dw, &dh);
|
||||
|
||||
g_img_w = (uint16_t)dw;
|
||||
g_img_h = (uint16_t)dh;
|
||||
g_img_w = CANVAS_W;
|
||||
g_img_h = CANVAS_H;
|
||||
// Center; negative offsets crop overflow into the canvas (cover).
|
||||
const int xoff = ((int)CANVAS_W - dw) / 2;
|
||||
const int yoff = ((int)CANVAS_H - dh) / 2;
|
||||
|
||||
@@ -341,7 +477,7 @@ static bool load_jpeg(const char *path) {
|
||||
}
|
||||
|
||||
static bool load_png(const char *path) {
|
||||
gif_close_session();
|
||||
gif_stop_and_wait();
|
||||
free_jpeg();
|
||||
free_png();
|
||||
|
||||
@@ -367,11 +503,13 @@ static bool load_png(const char *path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h);
|
||||
g_img_w = (uint16_t)g_dst_w;
|
||||
g_img_h = (uint16_t)g_dst_h;
|
||||
g_off_x = ((int)CANVAS_W - g_dst_w) / 2;
|
||||
g_off_y = ((int)CANVAS_H - g_dst_h) / 2;
|
||||
// Sample source into every canvas pixel (cover / fill).
|
||||
g_dst_w = CANVAS_W;
|
||||
g_dst_h = CANVAS_H;
|
||||
g_img_w = CANVAS_W;
|
||||
g_img_h = CANVAS_H;
|
||||
g_off_x = 0;
|
||||
g_off_y = 0;
|
||||
|
||||
canvas_clear(0x0000);
|
||||
const int drc = g_png->decode(nullptr, 0);
|
||||
@@ -385,44 +523,37 @@ static bool load_png(const char *path) {
|
||||
}
|
||||
|
||||
static bool load_gif(const char *path) {
|
||||
gif_stop_and_wait();
|
||||
free_jpeg();
|
||||
free_png();
|
||||
gif_close_session();
|
||||
|
||||
g_gif = new AnimatedGIF();
|
||||
if (!g_gif) {
|
||||
return false;
|
||||
}
|
||||
|
||||
g_gif->begin(GIF_PALETTE_RGB565_LE);
|
||||
if (!g_gif->open(path, gif_open, gif_close, gif_read, gif_seek, gif_draw)) {
|
||||
Serial.printf("GIF open fail: %s\n", path);
|
||||
gif_close_session();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_src_w = g_gif->getCanvasWidth();
|
||||
g_src_h = g_gif->getCanvasHeight();
|
||||
if (g_src_w <= 0 || g_src_h <= 0) {
|
||||
gif_close_session();
|
||||
return false;
|
||||
}
|
||||
|
||||
fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h);
|
||||
g_img_w = (uint16_t)g_dst_w;
|
||||
g_img_h = (uint16_t)g_dst_h;
|
||||
g_off_x = ((int)CANVAS_W - g_dst_w) / 2;
|
||||
g_off_y = ((int)CANVAS_H - g_dst_h) / 2;
|
||||
g_gif_open = true;
|
||||
gif_ensure_task();
|
||||
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);
|
||||
int delay_ms = 0;
|
||||
g_gif->playFrame(true, &delay_ms);
|
||||
if (delay_ms < 20) {
|
||||
delay_ms = 100;
|
||||
canvas_sync_dsc();
|
||||
g_gif_stop = false;
|
||||
xTaskNotifyGive(g_gif_task);
|
||||
|
||||
// Brief wait for first frame (non-blocking beyond a short timeout).
|
||||
const uint32_t t0 = millis();
|
||||
while ((millis() - t0) < 250) {
|
||||
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));
|
||||
}
|
||||
g_gif_next_ms = millis() + (uint32_t)delay_ms;
|
||||
return true;
|
||||
if (g_frame_ready && g_back) {
|
||||
memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t));
|
||||
g_frame_ready = false;
|
||||
}
|
||||
return g_gif_active || g_gif_busy;
|
||||
}
|
||||
|
||||
bool photo_view_init() {
|
||||
@@ -432,6 +563,7 @@ bool photo_view_init() {
|
||||
canvas_clear(0x0000);
|
||||
canvas_sync_dsc();
|
||||
g_ready = true;
|
||||
gif_ensure_task();
|
||||
Serial.printf("photo_view: canvas %ux%u (%u bytes), free heap %u\n",
|
||||
(unsigned)CANVAS_W, (unsigned)CANVAS_H,
|
||||
(unsigned)(CANVAS_PIXELS * 2), (unsigned)ESP.getFreeHeap());
|
||||
@@ -439,13 +571,19 @@ bool photo_view_init() {
|
||||
}
|
||||
|
||||
void photo_view_deinit() {
|
||||
gif_close_session();
|
||||
photo_view_stop();
|
||||
free_jpeg();
|
||||
free_png();
|
||||
if (g_back) {
|
||||
free(g_back);
|
||||
g_back = nullptr;
|
||||
}
|
||||
g_ready = false;
|
||||
g_dsc.data = nullptr;
|
||||
}
|
||||
|
||||
void photo_view_stop() { gif_stop_and_wait(); }
|
||||
|
||||
bool photo_view_show(const PhotoEntry *entry) {
|
||||
if (!entry || !photo_view_init()) {
|
||||
return false;
|
||||
@@ -476,21 +614,13 @@ uint16_t photo_view_canvas_h() { return CANVAS_H; }
|
||||
uint16_t photo_view_img_w() { return g_img_w; }
|
||||
uint16_t photo_view_img_h() { return g_img_h; }
|
||||
|
||||
bool photo_view_is_gif() { return g_gif_open; }
|
||||
bool photo_view_is_gif() { return g_gif_active || g_gif_busy; }
|
||||
|
||||
bool photo_view_gif_tick() {
|
||||
if (!g_gif_open || !g_gif) {
|
||||
if (!g_frame_ready || !g_back) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t now = millis();
|
||||
if ((int32_t)(now - g_gif_next_ms) < 0) {
|
||||
return false;
|
||||
}
|
||||
int delay_ms = 0;
|
||||
g_gif->playFrame(true, &delay_ms);
|
||||
if (delay_ms < 20) {
|
||||
delay_ms = 100;
|
||||
}
|
||||
g_gif_next_ms = now + (uint32_t)delay_ms;
|
||||
memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t));
|
||||
g_frame_ready = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user