From b5c8aeb4fe3c9a9fd081a27357192d74b9f885b9 Mon Sep 17 00:00:00 2001 From: chenjintao Date: Fri, 9 Oct 2026 15:05:05 +0800 Subject: [PATCH] perf(lcd): precompose emote updates and smooth closed eyes --- components/platform/src/lcd_direct.c | 203 +++++++++++++++++++++------ docs/lcd-idle-status.zh-CN.md | 22 ++- tools/test_lcd_emote_transport.py | 80 +++++++++++ 3 files changed, 264 insertions(+), 41 deletions(-) create mode 100644 tools/test_lcd_emote_transport.py diff --git a/components/platform/src/lcd_direct.c b/components/platform/src/lcd_direct.c index b672c31..edfaa64 100644 --- a/components/platform/src/lcd_direct.c +++ b/components/platform/src/lcd_direct.c @@ -101,7 +101,9 @@ #if CONFIG_TQ_LCD_EMOTE_GFX_TEST #define LCD_DIRECT_HUD_ROWS 40U #define LCD_DIRECT_EMOTE_FPS 30U -#define LCD_DIRECT_EMOTE_BUFFER_ROWS 4U +#define LCD_DIRECT_EMOTE_DMA_ROWS 16U +#define LCD_DIRECT_EMOTE_CLOSED_ROWS 14U +#define LCD_DIRECT_EMOTE_CLOSED_THRESHOLD 232U #define LCD_DIRECT_EMOTE_EYES_X 41 #define LCD_DIRECT_EMOTE_EYES_Y 65 #define LCD_DIRECT_EMOTE_OPEN_MS 2000U @@ -254,7 +256,13 @@ static gfx_handle_t s_emote_handle; static gfx_disp_t *s_emote_disp; static gfx_obj_t *s_emote_eyes_open; static gfx_mesh_img_point_q8_t s_emote_eye_rest[LCD_DIRECT_EMOTE_MESH_POINTS]; -static gfx_disp_t *s_emote_flush_pending; +static uint16_t *s_emote_dma_pixels; +static esp_err_t s_emote_flush_error; +static bool s_emote_transfer_fault; +static gfx_obj_t *s_emote_closed_eyes; +static gfx_image_dsc_t s_emote_closed_image; +static uint8_t *s_emote_closed_pixels; +static bool s_emote_show_closed; static bool s_emote_active; #endif @@ -288,16 +296,7 @@ static bool IRAM_ATTR lcd_direct_on_color_trans_done(esp_lcd_panel_io_handle_t p (void)user_ctx; BaseType_t higher_priority_task_woken = pdFALSE; -#if CONFIG_TQ_LCD_EMOTE_GFX_TEST - gfx_disp_t *emote_disp = NULL; - portENTER_CRITICAL_ISR(&s_state_lock); - emote_disp = s_emote_flush_pending; - s_emote_flush_pending = NULL; - portEXIT_CRITICAL_ISR(&s_state_lock); - if (emote_disp != NULL) { - (void)gfx_disp_flush_ready(emote_disp, true); - } -#endif + if (s_transfer_done_sem != NULL) { (void)xSemaphoreGiveFromISR(s_transfer_done_sem, &higher_priority_task_woken); } @@ -314,26 +313,46 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp, { portENTER_CRITICAL(&s_state_lock); bool active = s_emote_active; - if (active) { - s_emote_flush_pending = disp; - } portEXIT_CRITICAL(&s_state_lock); - - if (!active) { + if (!active || s_emote_transfer_fault) { (void)gfx_disp_flush_ready(disp, false); return; } - esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y1, x2, y2, data); - if (err != ESP_OK) { - portENTER_CRITICAL(&s_state_lock); - if (s_emote_flush_pending == disp) { - s_emote_flush_pending = NULL; - } - portEXIT_CRITICAL(&s_state_lock); - ESP_LOGE(TAG, "esp_emote_gfx flush failed: %s", esp_err_to_name(err)); + /* GFX 已在 PSRAM 中合成完整区域。这里仅复制/传输,不穿插网格计算。 + * 单 DMA 暂存区必须等 ISR 确认完成才能复用;整区域发完再通知 GFX。 */ + const uint16_t *source = data; + const size_t width = (size_t)(x2 - x1); + if (width == 0 || width > LCD_DIRECT_WIDTH || y2 <= y1 || s_emote_dma_pixels == NULL) { + s_emote_flush_error = ESP_ERR_INVALID_SIZE; (void)gfx_disp_flush_ready(disp, false); + return; } + const size_t capacity = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS; + for (int y = y1; y < y2;) { + size_t rows = capacity / width; + if (rows > (size_t)(y2 - y)) { + rows = (size_t)(y2 - y); + } + memcpy(s_emote_dma_pixels, source, width * rows * sizeof(uint16_t)); + (void)xSemaphoreTake(s_transfer_done_sem, 0); + esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y, x2, y + rows, + s_emote_dma_pixels); + if (err == ESP_OK && xSemaphoreTake(s_transfer_done_sem, + pdMS_TO_TICKS(LCD_DIRECT_TRANSFER_TIMEOUT_MS)) != pdTRUE) { + err = ESP_ERR_TIMEOUT; + /* 超时后不再复用可能仍被 DMA 读取的缓冲,重启前禁止此路径传输。 */ + s_emote_transfer_fault = true; + } + if (err != ESP_OK) { + s_emote_flush_error = err; + ESP_LOGE(TAG, "composed emote transfer failed: %s", esp_err_to_name(err)); + break; + } + source += width * rows; + y += rows; + } + (void)gfx_disp_flush_ready(disp, false); } static esp_err_t lcd_direct_emote_decode_asset(const lcd_emote_rle_asset_t *asset, @@ -386,6 +405,44 @@ static esp_err_t lcd_direct_emote_prepare_assets(void) return ESP_OK; } +/* 一条带覆盖度的抛物线代替被压成碎像素的双层眼圈。无新图片资源。 */ +static void lcd_direct_emote_draw_closed_eyes(uint8_t *dst, uint16_t width, + uint16_t height, uint16_t color) +{ + const size_t count = (size_t)width * height; + memset(dst, 0, count * 3); + for (int x = 0; x < width; ++x) { + int dx = x - (x < 119 ? 49 : 189); + if (abs(dx) > 46) { + continue; + } + int y_q8 = 3 * 256 + dx * dx * (8 * 256) / (46 * 46); + for (int y = 0; y < height; ++y) { + int coverage = 2 * 256 - abs(y * 256 - y_q8); + if (coverage <= 0) { + continue; + } + if (coverage > 255) { + coverage = 255; + } + size_t index = (size_t)y * width + x; + dst[index * 2] = color >> 8; + dst[index * 2 + 1] = color & 0xFF; + dst[count * 2 + index] = coverage; + } + } +} + +static esp_err_t lcd_direct_emote_refresh(void) +{ + if (s_emote_transfer_fault) { + return ESP_ERR_TIMEOUT; + } + s_emote_flush_error = ESP_OK; + esp_err_t err = gfx_refr_now(s_emote_handle); + return err != ESP_OK ? err : s_emote_flush_error; +} + static esp_err_t lcd_direct_emote_init(void) { ESP_RETURN_ON_ERROR(lcd_direct_emote_prepare_assets(), TAG, "decode emote assets failed"); @@ -402,17 +459,20 @@ static esp_err_t lcd_direct_emote_init(void) return ESP_ERR_NO_MEM; } + s_emote_dma_pixels = heap_caps_malloc(LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS * + sizeof(uint16_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); + ESP_RETURN_ON_FALSE(s_emote_dma_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate emote DMA failed"); const gfx_disp_config_t display_config = { .h_res = LCD_DIRECT_WIDTH, .v_res = LCD_DIRECT_HEIGHT, .flush_cb = lcd_direct_emote_flush, .flags = { .swap = true, - .buff_dma = true, - .double_buffer = true, + .buff_spiram = true, + .double_buffer = false, }, .buffers = { - .buf_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_BUFFER_ROWS, + .buf_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_HEIGHT, }, }; s_emote_disp = gfx_disp_add(s_emote_handle, &display_config); @@ -443,6 +503,33 @@ static esp_err_t lcd_direct_emote_init(void) ESP_RETURN_ON_ERROR(gfx_mesh_img_get_point_q8(s_emote_eyes_open, i, &s_emote_eye_rest[i]), TAG, "read eye mesh failed"); } + const size_t closed_count = idle_eyes_open_rle_asset.width * LCD_DIRECT_EMOTE_CLOSED_ROWS; + s_emote_closed_pixels = heap_caps_malloc(closed_count * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + ESP_RETURN_ON_FALSE(s_emote_closed_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate closed eyes failed"); + uint16_t eye_color = 0xFFFF; + const size_t eye_count = idle_eyes_open_rle_asset.width * idle_eyes_open_rle_asset.height; + for (size_t i = 0; i < eye_count; ++i) { + if (s_emote_eye_pixels[eye_count * 2 + i] >= 200) { + eye_color = ((uint16_t)s_emote_eye_pixels[i * 2] << 8) | s_emote_eye_pixels[i * 2 + 1]; + break; + } + } + lcd_direct_emote_draw_closed_eyes(s_emote_closed_pixels, idle_eyes_open_rle_asset.width, + LCD_DIRECT_EMOTE_CLOSED_ROWS, eye_color); + s_emote_closed_image = (gfx_image_dsc_t) { + .header.cf = GFX_COLOR_FORMAT_RGB565A8, + .header.magic = C_ARRAY_HEADER_MAGIC, + .header.w = idle_eyes_open_rle_asset.width, + .header.h = LCD_DIRECT_EMOTE_CLOSED_ROWS, + .data_size = closed_count * 3, + .data = s_emote_closed_pixels, + }; + s_emote_closed_eyes = gfx_img_create(s_emote_disp); + ESP_RETURN_ON_FALSE(s_emote_closed_eyes != NULL, ESP_ERR_NO_MEM, TAG, "create closed eyes failed"); + ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_closed_eyes, &s_emote_closed_image), TAG, "set closed eyes failed"); + ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_closed_eyes, LCD_DIRECT_EMOTE_EYES_X, + LCD_DIRECT_EMOTE_EYES_Y + 39), TAG, "position closed eyes failed"); + ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, false), TAG, "hide closed eyes failed"); s_emote_hud_pixels = heap_caps_calloc(LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS, sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); ESP_RETURN_ON_FALSE(s_emote_hud_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate HUD failed"); @@ -459,9 +546,9 @@ static esp_err_t lcd_direct_emote_init(void) ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_hud, &s_emote_hud_image), TAG, "set HUD failed"); s_emote_ready = true; ESP_LOGI(TAG, - "esp_emote_gfx idle test ready (%u fps, 2 x %u-row DMA buffers)", + "esp_emote_gfx idle test ready (%u fps, PSRAM composed frame, %u-row DMA staging)", (unsigned)LCD_DIRECT_EMOTE_FPS, - (unsigned)LCD_DIRECT_EMOTE_BUFFER_ROWS); + (unsigned)LCD_DIRECT_EMOTE_DMA_ROWS); return ESP_OK; } @@ -517,8 +604,8 @@ static esp_err_t lcd_direct_emote_update_hud(bool force, bool *changed) static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms) { - uint32_t remaining_ms = duration_ms; - while (remaining_ms > 0U) { + const int64_t deadline_us = esp_timer_get_time() + (int64_t)duration_ms * 1000; + while (esp_timer_get_time() < deadline_us) { if (lcd_direct_should_stop_idle()) { return ESP_ERR_INVALID_STATE; } @@ -526,17 +613,23 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms) bool changed = false; esp_err_t hud_err = lcd_direct_emote_update_hud(false, &changed); if (hud_err == ESP_OK && changed) { - hud_err = gfx_refr_now(s_emote_handle); + hud_err = lcd_direct_emote_refresh(); } esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle); if (hud_err != ESP_OK || unlock_err != ESP_OK) { return hud_err != ESP_OK ? hud_err : unlock_err; } + const int64_t remaining_us = deadline_us - esp_timer_get_time(); + if (remaining_us <= 0) { + break; + } + const uint32_t remaining_ms = (remaining_us + 999) / 1000; uint32_t slice_ms = remaining_ms > LCD_DIRECT_PRESENTATION_WAIT_POLL_MS ? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS : remaining_ms; - vTaskDelay(pdMS_TO_TICKS(slice_ms)); - remaining_ms -= slice_ms; + TickType_t ticks = pdMS_TO_TICKS(slice_ms); + /* 当前 tick 为 10ms;短余量也至少让出一个 tick,避免忙等。 */ + vTaskDelay(ticks > 0 ? ticks : 1); } return ESP_OK; } @@ -545,6 +638,15 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms) * closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */ static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure) { + bool closed = closure >= LCD_DIRECT_EMOTE_CLOSED_THRESHOLD; + if (closed != s_emote_show_closed) { + ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_eyes_open, !closed), TAG, "toggle mesh eyes failed"); + ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, closed), TAG, "toggle closed eyes failed"); + s_emote_show_closed = closed; + } + if (closed) { + return ESP_OK; + } gfx_mesh_img_point_q8_t points[LCD_DIRECT_EMOTE_MESH_POINTS]; /* 上下眼睑对称收向原眼睛中线,包围盒位置补偿保持脸部锚点。 */ const int32_t center_y = 46 * 256; @@ -594,13 +696,13 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque } if (result == ESP_OK) { gfx_disp_refresh_all(s_emote_disp); - result = gfx_refr_now(s_emote_handle); + result = lcd_direct_emote_refresh(); } esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle); if (result == ESP_OK) { result = unlock_err; } - ESP_LOGI(TAG, "esp_emote_gfx idle playback started (single-mesh blink)"); + ESP_LOGI(TAG, "esp_emote_gfx idle playback started (composed mesh/arc blink)"); /* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */ static const uint8_t blink_closure[] = { @@ -616,23 +718,40 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque if (result != ESP_OK) { break; } + uint64_t draw_us_total = 0; + uint32_t late_frames = 0; for (size_t i = 0; i < sizeof(blink_closure) / sizeof(blink_closure[0]) && result == ESP_OK; ++i) { + const int64_t frame_start_us = esp_timer_get_time(); result = gfx_emote_lock(s_emote_handle); if (result != ESP_OK) { break; } result = lcd_direct_emote_set_eye_closure(blink_closure[i]); if (result == ESP_OK) { - result = gfx_refr_now(s_emote_handle); + result = lcd_direct_emote_refresh(); } unlock_err = gfx_emote_unlock(s_emote_handle); if (result == ESP_OK) { result = unlock_err; } if (result == ESP_OK) { - result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_STEP_MS); + const uint32_t elapsed_us = (uint32_t)(esp_timer_get_time() - frame_start_us); + draw_us_total += elapsed_us; + const uint32_t period_us = LCD_DIRECT_EMOTE_STEP_MS * 1000U; + if (elapsed_us < period_us) { + /* 绘制时间属于本帧预算,不再额外固定睡 34ms。 */ + result = lcd_direct_emote_wait((period_us - elapsed_us + 999U) / 1000U); + } else { + ++late_frames; + /* 过载仍给其他任务调度机会,并在下一步检查 pending。 */ + result = lcd_direct_emote_wait(1U); + } } } + if (result == ESP_OK) { + ESP_LOGI(TAG, "emote blink composed draw_us_avg=%llu late_frames=%u/12", + (unsigned long long)(draw_us_total / 12U), (unsigned)late_frames); + } ++cycles; } @@ -969,13 +1088,17 @@ static esp_err_t lcd_direct_gpio_init(void) static esp_err_t lcd_direct_panel_init(void) { /* 面板命令、方向和颜色格式在显示开启前一次性配置完成。 */ + size_t max_transfer_pixels = LCD_DIRECT_PIXELS_PER_TRANSFER; +#if CONFIG_TQ_LCD_EMOTE_GFX_TEST + max_transfer_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS; +#endif spi_bus_config_t bus_config = { .mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO, .miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO, .sclk_io_num = CONFIG_TQ_SCREEN_SPI_CLK_GPIO, .quadwp_io_num = -1, .quadhd_io_num = -1, - .max_transfer_sz = LCD_DIRECT_PIXELS_PER_TRANSFER * sizeof(uint16_t), + .max_transfer_sz = max_transfer_pixels * sizeof(uint16_t), }; ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_DIRECT_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO), TAG, diff --git a/docs/lcd-idle-status.zh-CN.md b/docs/lcd-idle-status.zh-CN.md index dbd5b4c..cd46e11 100644 --- a/docs/lcd-idle-status.zh-CN.md +++ b/docs/lcd-idle-status.zh-CN.md @@ -22,7 +22,7 @@ - 右上百分比和电池;未知值显示 --%,充电绿色并带闪电缺口,低电量红色。 - SOC、有效性和充电状态来自现有 domain battery 服务,不从 ADC 重算。 - GFX 顶部 320x40 RGB565 图层,常驻 25,600 B PSRAM;只在分钟或电池状态 - 改变时使图层失效并刷新,眨眼仍只改变眼睛网格,不解码新 PNG。 + 改变时使图层失效并刷新,眨眼主要改变眼睛网格,接近闭合时切换为程序绘制的抗锯齿弧线,不解码新 PNG。 - PNG 表情复用相同点阵 / 电池绘制器,在每帧合成时更新;二维码和 LCD 边界测试页面不显示状态栏。PNG 无限等待期间不会独立重绘状态栏。 - 不需要字体包或新图片。旧 PNG 低电量底部告警保留;旧充电 PNG 不再自动 @@ -42,3 +42,23 @@ AddressSanitizer / UndefinedBehaviorSanitizer 检查 808 组有效性、充电 4. 动画结束同时到达新请求时不被自动 IDLE 覆盖。 编译 / 主机测试不等同于上述实机验证。 + +## 无 TE 的软件减撕裂试验 + +- GFX 使用单个 153,600 B PSRAM 合成缓冲(partition 模式,不是 RGB 全帧模式), ++ 普通眼睛更新先完成整个 dirty 区域,再进入 flush;不会在分块传输之间计算网格。 ++- flush 使用单个 10,240 B 内部 DMA 暂存区;按最多 5,120 像素连续搬运, ++ 每批等待 ISR 完成后才复用,整区域发完才通知 GFX。PNG 原有 8 行块不变。 ++- 相对上一版移除 2×2,560 B DMA 小缓冲,增加 5,120 B DMA 暂存净容量 ++ (不含 SPI 驱动描述符);新增合成缓冲和 9,996 B 闭眼纹理共 163,596 B PSRAM。 ++- closure >=232 时隐藏压缩眼圈并显示同色的抗锯齿闭眼弧线,避免极薄纹理抖动。 ++- 34ms 帧预算包含绘制/传输时间,超时不会再固定额外等 34ms。 ++- 每轮日志 emote blink composed draw_us_avg=... late_frames=.../12, ++ 仅反映绘制预算,不是面板实际扫描帧率。 ++- DMA 超时会锁住 GFX 传输路径,不复用可能仍被硬件读取的缓冲;需重启恢复。 ++- 此方案不能保证面板扫描无撕裂。实机比较眨眼上下断层、闭眼亮点及原有 ++ 表情切换/状态栏行为;无 TE 时不能把软件预合成称为硬件同步双缓冲。 + +主机传输/闭眼回归:`python3 tools/test_lcd_emote_transport.py`,检查 320 种 +区域宽度、DMA 分块/数据顺序、错误/超时处理及闭眼对称性。当前 RTOS tick +为 10ms,实际眨眼间隔仍有 tick 量化及任务调度抖动,日志不是实测 FPS。 diff --git a/tools/test_lcd_emote_transport.py b/tools/test_lcd_emote_transport.py new file mode 100644 index 0000000..6c5fa56 --- /dev/null +++ b/tools/test_lcd_emote_transport.py @@ -0,0 +1,80 @@ +from pathlib import Path +import re,subprocess,tempfile +src=(Path(__file__).resolve().parents[1]/'components/platform/src/lcd_direct.c').read_text() +def extract(name): + m=re.search(r'static [^;{}]+\b'+name+r'\([^;{}]*\)\s*\{',src) + a=m.start(); b=src.index('{',a)+1; depth=1 + while depth: + if src[b]=='{': depth+=1 + if src[b]=='}': depth-=1 + b+=1 + return src[a:b] +prefix='''#include +#include +#include +#include +#include +#include +#define LCD_DIRECT_WIDTH 320 +#define LCD_DIRECT_EMOTE_DMA_ROWS 16 +#define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100 +#define portENTER_CRITICAL(x) ((void)0) +#define portEXIT_CRITICAL(x) ((void)0) +#define pdMS_TO_TICKS(x) (x) +#define ESP_LOGE(...) ((void)0) +#define ESP_OK 0 +#define ESP_ERR_INVALID_SIZE 1 +#define ESP_ERR_TIMEOUT 2 +#define pdTRUE 1 +typedef int esp_err_t; +typedef int gfx_disp_t; +static bool s_emote_active, s_emote_transfer_fault; +static int s_emote_flush_error, s_panel, s_transfer_done_sem; +static uint16_t dma[5120]; +static uint16_t *s_emote_dma_pixels=dma; +static uint16_t capture[320*240]; +static int calls,ready,fail_call,timeout_call,origin_y,width; +static bool gfx_disp_flush_ready(gfx_disp_t *d,bool swap) {++ready;return true;} +static int xSemaphoreTake(int sem,int ticks) {return ticks && calls!=timeout_call;} +static int esp_lcd_panel_draw_bitmap(int panel,int x1,int y1,int x2,int y2,const void *data) { + ++calls; assert(ready==0); assert((x2-x1)*(y2-y1)<=5120); assert(data==dma); + if(calls==fail_call) return 3; + memcpy(capture+(y1-origin_y)*width,data,(x2-x1)*(y2-y1)*2); + return 0; +} +''' +test='''int main(void) { + uint16_t source[320*240]; + for(int i=0;i<320*240;++i) source[i]=(uint16_t)i; + for(width=1;width<=320;++width) { + calls=ready=fail_call=timeout_call=0;origin_y=65;s_emote_active=true; + s_emote_transfer_fault=false;s_emote_flush_error=0; + lcd_direct_emote_flush(NULL,0,65,width,158,source); + assert(ready==1 && s_emote_flush_error==0); + assert(memcmp(source,capture,width*93*2)==0); + assert(calls==(93+5120/width-1)/(5120/width)); + } + width=238;calls=ready=0;timeout_call=2;s_emote_flush_error=0; + lcd_direct_emote_flush(NULL,41,65,279,158,source); + assert(ready==1 && s_emote_transfer_fault && s_emote_flush_error==2 && calls==2); + lcd_direct_emote_flush(NULL,41,65,279,158,source); + assert(calls==2 && ready==2); + s_emote_transfer_fault=false;timeout_call=0;fail_call=1;calls=ready=0; + lcd_direct_emote_flush(NULL,41,65,279,158,source); + assert(calls==1 && ready==1 && s_emote_flush_error==3); + calls=ready=0;s_emote_active=false; + lcd_direct_emote_flush(NULL,41,65,279,158,source);assert(calls==0 && ready==1); + uint8_t eyes[238*14*3+2];memset(eyes,123,sizeof(eyes)); + lcd_direct_emote_draw_closed_eyes(eyes+1,238,14,0xffe0); + assert(eyes[0]==123 && eyes[sizeof(eyes)-1]==123); + const uint8_t *alpha=eyes+1+238*14*2; + for(int y=0;y<14;++y) for(int x=3;x<=95;++x) + assert(alpha[y*238+x]==alpha[y*238+x+140]); + assert(alpha[3*238+49]==255 && alpha[11*238+3]==255); + puts("PASS: 320 transfer widths, DMA bounds/order, timeout latch, inactive/error completion and symmetric closed-eye raster"); +}''' +with tempfile.TemporaryDirectory(prefix='emote-transport-') as tmp: + c=Path(tmp)/'test.c';exe=Path(tmp)/'test' + c.write_text(prefix+extract('lcd_direct_emote_flush')+'\n'+extract('lcd_direct_emote_draw_closed_eyes')+'\n'+test) + subprocess.run(['clang','-std=c11','-fsanitize=address,undefined',str(c),'-o',str(exe)],check=True) + subprocess.run([str(exe)],check=True)