perf(lcd): precompose emote updates and smooth closed eyes

This commit is contained in:
chenjintao
2026-10-09 15:05:05 +08:00
parent e75846ea15
commit b5c8aeb4fe
3 changed files with 264 additions and 41 deletions
+163 -40
View File
@@ -101,7 +101,9 @@
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST #if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#define LCD_DIRECT_HUD_ROWS 40U #define LCD_DIRECT_HUD_ROWS 40U
#define LCD_DIRECT_EMOTE_FPS 30U #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_X 41
#define LCD_DIRECT_EMOTE_EYES_Y 65 #define LCD_DIRECT_EMOTE_EYES_Y 65
#define LCD_DIRECT_EMOTE_OPEN_MS 2000U #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_disp_t *s_emote_disp;
static gfx_obj_t *s_emote_eyes_open; 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_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; static bool s_emote_active;
#endif #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; (void)user_ctx;
BaseType_t higher_priority_task_woken = pdFALSE; 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) { if (s_transfer_done_sem != NULL) {
(void)xSemaphoreGiveFromISR(s_transfer_done_sem, &higher_priority_task_woken); (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); portENTER_CRITICAL(&s_state_lock);
bool active = s_emote_active; bool active = s_emote_active;
if (active) {
s_emote_flush_pending = disp;
}
portEXIT_CRITICAL(&s_state_lock); portEXIT_CRITICAL(&s_state_lock);
if (!active || s_emote_transfer_fault) {
if (!active) {
(void)gfx_disp_flush_ready(disp, false); (void)gfx_disp_flush_ready(disp, false);
return; return;
} }
esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y1, x2, y2, data); /* GFX 已在 PSRAM 中合成完整区域。这里仅复制/传输,不穿插网格计算。
if (err != ESP_OK) { * 单 DMA 暂存区必须等 ISR 确认完成才能复用;整区域发完再通知 GFX。 */
portENTER_CRITICAL(&s_state_lock); const uint16_t *source = data;
if (s_emote_flush_pending == disp) { const size_t width = (size_t)(x2 - x1);
s_emote_flush_pending = NULL; if (width == 0 || width > LCD_DIRECT_WIDTH || y2 <= y1 || s_emote_dma_pixels == NULL) {
} s_emote_flush_error = ESP_ERR_INVALID_SIZE;
portEXIT_CRITICAL(&s_state_lock);
ESP_LOGE(TAG, "esp_emote_gfx flush failed: %s", esp_err_to_name(err));
(void)gfx_disp_flush_ready(disp, false); (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, 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; 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) static esp_err_t lcd_direct_emote_init(void)
{ {
ESP_RETURN_ON_ERROR(lcd_direct_emote_prepare_assets(), TAG, "decode emote assets failed"); 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; 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 = { const gfx_disp_config_t display_config = {
.h_res = LCD_DIRECT_WIDTH, .h_res = LCD_DIRECT_WIDTH,
.v_res = LCD_DIRECT_HEIGHT, .v_res = LCD_DIRECT_HEIGHT,
.flush_cb = lcd_direct_emote_flush, .flush_cb = lcd_direct_emote_flush,
.flags = { .flags = {
.swap = true, .swap = true,
.buff_dma = true, .buff_spiram = true,
.double_buffer = true, .double_buffer = false,
}, },
.buffers = { .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); 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]), 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"); 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, s_emote_hud_pixels = heap_caps_calloc(LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS,
sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); 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"); 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"); ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_hud, &s_emote_hud_image), TAG, "set HUD failed");
s_emote_ready = true; s_emote_ready = true;
ESP_LOGI(TAG, 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_FPS,
(unsigned)LCD_DIRECT_EMOTE_BUFFER_ROWS); (unsigned)LCD_DIRECT_EMOTE_DMA_ROWS);
return ESP_OK; 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) static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
{ {
uint32_t remaining_ms = duration_ms; const int64_t deadline_us = esp_timer_get_time() + (int64_t)duration_ms * 1000;
while (remaining_ms > 0U) { while (esp_timer_get_time() < deadline_us) {
if (lcd_direct_should_stop_idle()) { if (lcd_direct_should_stop_idle()) {
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
} }
@@ -526,17 +613,23 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
bool changed = false; bool changed = false;
esp_err_t hud_err = lcd_direct_emote_update_hud(false, &changed); esp_err_t hud_err = lcd_direct_emote_update_hud(false, &changed);
if (hud_err == ESP_OK && 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); esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (hud_err != ESP_OK || unlock_err != ESP_OK) { if (hud_err != ESP_OK || unlock_err != ESP_OK) {
return hud_err != ESP_OK ? hud_err : unlock_err; 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 uint32_t slice_ms = remaining_ms > LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS ? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
: remaining_ms; : remaining_ms;
vTaskDelay(pdMS_TO_TICKS(slice_ms)); TickType_t ticks = pdMS_TO_TICKS(slice_ms);
remaining_ms -= slice_ms; /* 当前 tick 为 10ms;短余量也至少让出一个 tick,避免忙等。 */
vTaskDelay(ticks > 0 ? ticks : 1);
} }
return ESP_OK; return ESP_OK;
} }
@@ -545,6 +638,15 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
* closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */ * closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */
static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure) 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]; gfx_mesh_img_point_q8_t points[LCD_DIRECT_EMOTE_MESH_POINTS];
/* 上下眼睑对称收向原眼睛中线,包围盒位置补偿保持脸部锚点。 */ /* 上下眼睑对称收向原眼睛中线,包围盒位置补偿保持脸部锚点。 */
const int32_t center_y = 46 * 256; 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) { if (result == ESP_OK) {
gfx_disp_refresh_all(s_emote_disp); 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); esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) { if (result == ESP_OK) {
result = unlock_err; 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 睁开,保持单一轮廓。 */ /* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */
static const uint8_t blink_closure[] = { 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) { if (result != ESP_OK) {
break; 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) { 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); result = gfx_emote_lock(s_emote_handle);
if (result != ESP_OK) { if (result != ESP_OK) {
break; break;
} }
result = lcd_direct_emote_set_eye_closure(blink_closure[i]); result = lcd_direct_emote_set_eye_closure(blink_closure[i]);
if (result == ESP_OK) { if (result == ESP_OK) {
result = gfx_refr_now(s_emote_handle); result = lcd_direct_emote_refresh();
} }
unlock_err = gfx_emote_unlock(s_emote_handle); unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) { if (result == ESP_OK) {
result = unlock_err; result = unlock_err;
} }
if (result == ESP_OK) { 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; ++cycles;
} }
@@ -969,13 +1088,17 @@ static esp_err_t lcd_direct_gpio_init(void)
static esp_err_t lcd_direct_panel_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 = { spi_bus_config_t bus_config = {
.mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO, .mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO,
.miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO, .miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO,
.sclk_io_num = CONFIG_TQ_SCREEN_SPI_CLK_GPIO, .sclk_io_num = CONFIG_TQ_SCREEN_SPI_CLK_GPIO,
.quadwp_io_num = -1, .quadwp_io_num = -1,
.quadhd_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), ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_DIRECT_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO),
TAG, TAG,
+21 -1
View File
@@ -22,7 +22,7 @@
- 右上百分比和电池;未知值显示 --%,充电绿色并带闪电缺口,低电量红色。 - 右上百分比和电池;未知值显示 --%,充电绿色并带闪电缺口,低电量红色。
- SOC、有效性和充电状态来自现有 domain battery 服务,不从 ADC 重算。 - SOC、有效性和充电状态来自现有 domain battery 服务,不从 ADC 重算。
- GFX 顶部 320x40 RGB565 图层,常驻 25,600 B PSRAM;只在分钟或电池状态 - GFX 顶部 320x40 RGB565 图层,常驻 25,600 B PSRAM;只在分钟或电池状态
改变时使图层失效并刷新,眨眼仍只改变眼睛网格,不解码新 PNG。 改变时使图层失效并刷新,眨眼主要改变眼睛网格,接近闭合时切换为程序绘制的抗锯齿弧线,不解码新 PNG。
- PNG 表情复用相同点阵 / 电池绘制器,在每帧合成时更新;二维码和 LCD - PNG 表情复用相同点阵 / 电池绘制器,在每帧合成时更新;二维码和 LCD
边界测试页面不显示状态栏。PNG 无限等待期间不会独立重绘状态栏。 边界测试页面不显示状态栏。PNG 无限等待期间不会独立重绘状态栏。
- 不需要字体包或新图片。旧 PNG 低电量底部告警保留;旧充电 PNG 不再自动 - 不需要字体包或新图片。旧 PNG 低电量底部告警保留;旧充电 PNG 不再自动
@@ -42,3 +42,23 @@ AddressSanitizer / UndefinedBehaviorSanitizer 检查 808 组有效性、充电
4. 动画结束同时到达新请求时不被自动 IDLE 覆盖。 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。
+80
View File
@@ -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 <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#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)