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
#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,