feat(lcd): add cached listening animation and asset guidelines

This commit is contained in:
chenjintao
2026-10-09 18:01:32 +08:00
parent c0770ac459
commit 22f5ac70a2
32 changed files with 3384 additions and 35 deletions
+171 -12
View File
@@ -36,6 +36,7 @@
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#include "gfx.h"
#include "lcd_emote_assets.h"
#include "lcd_emote_listening.h"
#endif
#define LCD_DIRECT_SPI_HOST SPI2_HOST
@@ -261,6 +262,15 @@ static bool s_emote_hud_valid;
static gfx_handle_t s_emote_handle;
static gfx_disp_t *s_emote_disp;
static gfx_obj_t *s_emote_eyes_open;
static gfx_obj_t *s_emote_background;
/* listening 复用引擎、合成缓冲、DMA 和 HUD;只有底图及局部眼型独立。 */
static gfx_image_dsc_t s_listening_base_image;
static uint8_t *s_listening_base_pixels;
static gfx_image_dsc_t s_listening_images[LCD_DIRECT_EMOTE_CACHE_COUNT];
static uint8_t *s_listening_pixels[LCD_DIRECT_EMOTE_CACHE_COUNT];
static gfx_obj_t *s_listening_eyes;
static bool s_listening_ready;
static int s_listening_current = -1;
static gfx_mesh_img_point_q8_t s_emote_eye_rest[LCD_DIRECT_EMOTE_MESH_POINTS];
static uint16_t *s_emote_dma_pixels;
static esp_err_t s_emote_flush_error;
@@ -347,6 +357,7 @@ static void lcd_direct_emote_capture(uint16_t *dst, int x1, int y1, int x2, int
}
static esp_err_t lcd_direct_emote_prepare_cache(void);
static esp_err_t lcd_direct_listening_prepare(void);
static void lcd_direct_emote_flush(gfx_disp_t *disp,
int x1,
@@ -543,7 +554,8 @@ static esp_err_t lcd_direct_emote_init(void)
s_emote_disp = gfx_disp_add(s_emote_handle, &display_config);
ESP_RETURN_ON_FALSE(s_emote_disp != NULL, ESP_ERR_NO_MEM, TAG, "esp_emote_gfx display add failed");
gfx_obj_t *background = gfx_img_create(s_emote_disp);
s_emote_background = gfx_img_create(s_emote_disp);
gfx_obj_t *background = s_emote_background;
s_emote_eyes_open = gfx_mesh_img_create(s_emote_disp);
ESP_RETURN_ON_FALSE(background != NULL && s_emote_eyes_open != NULL,
ESP_ERR_NO_MEM, TAG, "esp_emote_gfx image create failed");
@@ -613,6 +625,10 @@ static esp_err_t lcd_direct_emote_init(void)
if (cache_err != ESP_OK) {
ESP_LOGW(TAG, "Eye cache unavailable, retain live mesh: %s", esp_err_to_name(cache_err));
}
esp_err_t listening_err = lcd_direct_listening_prepare();
if (listening_err != ESP_OK) {
ESP_LOGW(TAG, "Listening GFX unavailable, retain PNG: %s", esp_err_to_name(listening_err));
}
s_emote_ready = true;
ESP_LOGI(TAG,
"esp_emote_gfx idle test ready (%u fps, PSRAM composed frame, %u-row DMA staging)",
@@ -866,6 +882,137 @@ static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure)
return err;
}
/* 初始化一次;关键图只在生成缓存时存在,完成后释放,不占运行期常驻内存。 */
static esp_err_t lcd_direct_listening_prepare(void)
{
const lcd_emote_rle_asset_t *assets[3] = {
&listening_eyes_open_rle_asset, &listening_eyes_half_rle_asset,
&listening_eyes_closed_rle_asset,
};
gfx_image_dsc_t keys[3] = {0};
uint8_t *key_pixels[3] = {0};
const unsigned width = assets[0]->width, height = assets[0]->height;
const size_t bytes = (size_t)width * height * 2;
const int64_t start_us = esp_timer_get_time();
esp_err_t result = lcd_direct_emote_decode_asset(&listening_base_rle_asset,
&s_listening_base_image, &s_listening_base_pixels);
for (size_t i = 0; i < 3 && result == ESP_OK; ++i) {
if (assets[i]->width != width || assets[i]->height != height ||
assets[i]->x != assets[0]->x || assets[i]->y != assets[0]->y ||
width == 0 || width > LCD_DIRECT_WIDTH || height == 0 || height > LCD_DIRECT_HEIGHT ||
assets[i]->x + width > LCD_DIRECT_WIDTH || assets[i]->y + height > LCD_DIRECT_HEIGHT) {
result = ESP_ERR_INVALID_SIZE;
break;
}
result = lcd_direct_emote_decode_asset(assets[i], &keys[i], &key_pixels[i]);
}
const uint8_t *sources[3] = {key_pixels[0], key_pixels[1], key_pixels[2]};
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT && result == ESP_OK; ++i) {
s_listening_pixels[i] = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (s_listening_pixels[i] == NULL) {
result = ESP_ERR_NO_MEM;
break;
}
if (!lcd_emote_listening_build_frame(s_listening_pixels[i], width, height,
sources, s_emote_cache_closures[i])) {
result = ESP_ERR_INVALID_SIZE;
break;
}
/* 缓存是黑背景不透明 RGB565。恢复窗口里的眉毛/胡须,避免黑块盖掉静态层。 */
const unsigned bw = listening_base_rle_asset.width;
const unsigned bh = listening_base_rle_asset.height;
for (unsigned y = 0; y < height; ++y) {
for (unsigned x = 0; x < width; ++x) {
size_t index = ((size_t)y * width + x) * 2;
int bx = (int)(assets[0]->x + x) - listening_base_rle_asset.x;
int by = (int)(assets[0]->y + y) - listening_base_rle_asset.y;
if (s_listening_pixels[i][index] == 0 && s_listening_pixels[i][index + 1] == 0 &&
bx >= 0 && by >= 0 && bx < bw && by < bh) {
size_t source = ((size_t)by * bw + bx) * 2;
s_listening_pixels[i][index] = s_listening_base_pixels[source];
s_listening_pixels[i][index + 1] = s_listening_base_pixels[source + 1];
}
}
}
s_listening_images[i] = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565, .header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = width, .header.h = height,
.data_size = bytes, .data = s_listening_pixels[i],
};
}
for (size_t i = 0; i < 3; ++i) free(key_pixels[i]);
if (result == ESP_OK) {
result = gfx_emote_lock(s_emote_handle);
if (result == ESP_OK) {
s_listening_eyes = gfx_img_create(s_emote_disp);
result = s_listening_eyes != NULL ? ESP_OK : ESP_ERR_NO_MEM;
if (result == ESP_OK) result = gfx_img_set_src(s_listening_eyes, &s_listening_images[0]);
if (result == ESP_OK) result = gfx_obj_set_pos(s_listening_eyes, assets[0]->x, assets[0]->y);
if (result == ESP_OK) result = gfx_obj_set_visible(s_listening_eyes, false);
if (result != ESP_OK && s_listening_eyes != NULL) {
(void)gfx_obj_delete(s_listening_eyes);
s_listening_eyes = NULL;
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) result = unlock_err;
}
}
if (result != ESP_OK) {
/* 若 unlock 异常,已经绑定的对象不能引用释放后的像素;保留到设备重启。 */
if (s_listening_eyes == NULL) {
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT; ++i) {
free(s_listening_pixels[i]);
s_listening_pixels[i] = NULL;
}
free(s_listening_base_pixels);
s_listening_base_pixels = NULL;
}
return result;
}
s_listening_ready = true;
ESP_LOGI(TAG, "Listening cache ready frames=%u window=%ux%u PSRAM=%uB "
"temporary_keys=%uB compressed=%uB prepare_us=%llu",
(unsigned)LCD_DIRECT_EMOTE_CACHE_COUNT, width, height,
(unsigned)(bytes * LCD_DIRECT_EMOTE_CACHE_COUNT + s_listening_base_image.data_size),
(unsigned)(width * height * 9),
(unsigned)(listening_base_rle_asset.compressed_size + assets[0]->compressed_size +
assets[1]->compressed_size + assets[2]->compressed_size),
(unsigned long long)(esp_timer_get_time() - start_us));
return ESP_OK;
}
/* 调用方持锁。进入状态时切换静态层;所有后端共用同一个显示与 HUD。 */
static esp_err_t lcd_direct_emote_select_face(bool listening)
{
const lcd_emote_rle_asset_t *base = listening ? &listening_base_rle_asset : &idle_base_rle_asset;
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_background,
listening ? &s_listening_base_image : &s_emote_base_image), TAG, "switch face base failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_background, base->x, base->y), TAG, "switch face position failed");
if (s_listening_eyes != NULL) {
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_listening_eyes, listening), TAG, "switch listening eyes failed");
}
if (s_emote_cached_eyes != NULL) {
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_cached_eyes, !listening), TAG, "switch idle cache failed");
}
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_eyes_open, !listening && !s_emote_cache_ready),
TAG, "switch live eyes failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, false), TAG, "hide closed eyes failed");
s_emote_show_closed = false;
return ESP_OK;
}
static esp_err_t lcd_direct_emote_set_face_closure(bool listening, uint8_t closure)
{
if (!listening) return lcd_direct_emote_set_eye_closure(closure);
const int index = lcd_direct_emote_cache_index(closure);
ESP_RETURN_ON_FALSE(index >= 0 && s_listening_ready, ESP_ERR_INVALID_STATE,
TAG, "listening eye cache unavailable");
if (index == s_listening_current) return ESP_OK;
esp_err_t err = gfx_img_set_src(s_listening_eyes, &s_listening_images[index]);
if (err == ESP_OK) s_listening_current = index;
return err;
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
static uint64_t lcd_direct_emote_fps_x100(uint32_t frames, uint64_t elapsed_us)
{
@@ -875,6 +1022,9 @@ static uint64_t lcd_direct_emote_fps_x100(uint32_t frames, uint64_t elapsed_us)
static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_request_t *request)
{
const bool listening = request != NULL && strcmp(request->asset_dir, "listening") == 0;
static const uint8_t listening_closure[] = {24, 80, 160, 232, 232, 192, 128, 64, 16, 0};
const uint32_t step_ms = listening ? 30U : LCD_DIRECT_EMOTE_STEP_MS;
ESP_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL,
ESP_ERR_INVALID_STATE,
TAG,
@@ -883,9 +1033,10 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
portENTER_CRITICAL(&s_state_lock);
s_emote_active = true;
portEXIT_CRITICAL(&s_state_lock);
esp_err_t result = lcd_direct_emote_update_hud(true, NULL);
esp_err_t result = lcd_direct_emote_select_face(listening);
if (result == ESP_OK) result = lcd_direct_emote_update_hud(true, NULL);
if (result == ESP_OK) {
result = lcd_direct_emote_set_eye_closure(0U);
result = lcd_direct_emote_set_face_closure(listening, 0U);
}
if (result == ESP_OK) {
gfx_disp_refresh_all(s_emote_disp);
@@ -895,7 +1046,8 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
if (result == ESP_OK) {
result = unlock_err;
}
ESP_LOGI(TAG, "esp_emote_gfx idle playback started (cached=%d)", s_emote_cache_ready);
ESP_LOGI(TAG, "esp_emote_gfx %s playback started (cached=%d)",
listening ? "listening" : "idle", listening ? s_listening_ready : s_emote_cache_ready);
/* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */
static const uint8_t blink_closure[] = {
@@ -903,9 +1055,11 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
232U, 192U, 128U, 64U, 16U, 0U,
};
uint16_t cycles = 0;
const uint8_t *timeline = listening ? listening_closure : blink_closure;
const size_t step_count = listening ? sizeof(listening_closure) : sizeof(blink_closure);
const uint16_t repeats = request != NULL ? request->repeat_count : 0;
const uint32_t open_ms = repeats > 0 && request->frame_ms > 0
? request->frame_ms : LCD_DIRECT_EMOTE_OPEN_MS;
? request->frame_ms : listening ? 2400U : LCD_DIRECT_EMOTE_OPEN_MS;
while (result == ESP_OK && (repeats == 0 || cycles < repeats)) {
result = lcd_direct_emote_wait(open_ms);
if (result != ESP_OK) {
@@ -921,7 +1075,7 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
uint64_t cost_total_us[4] = {0};
uint32_t cost_max_us[4] = {0};
#endif
for (size_t i = 0; i < sizeof(blink_closure) / sizeof(blink_closure[0]) && result == ESP_OK; ++i) {
for (size_t i = 0; i < step_count && result == ESP_OK; ++i) {
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const int64_t frame_start_us = esp_timer_get_time();
#endif
@@ -935,7 +1089,7 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
const uint64_t spi_before_us = s_emote_spi_us;
const uint32_t regions_before = s_emote_completed_regions;
#endif
result = lcd_direct_emote_set_eye_closure(blink_closure[i]);
result = lcd_direct_emote_set_face_closure(listening, timeline[i]);
if (result == ESP_OK) {
result = lcd_direct_emote_refresh();
}
@@ -973,32 +1127,33 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
}
#endif
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (elapsed_us >= LCD_DIRECT_EMOTE_STEP_MS * 1000U) {
if (elapsed_us >= step_ms * 1000U) {
++late_frames;
}
#endif
/* 所有步骤锚定同一时间轴,tick/调度超时不累积到后续截止时间。
* 绘制确实过慢时不跳帧,也不增加固定等待;下一步继续检查取消。 */
result = lcd_direct_emote_wait_until(
lcd_direct_emote_step_deadline(blink_start_us, i));
listening ? blink_start_us + (int64_t)(i + 1U) * step_ms * 1000 :
lcd_direct_emote_step_deadline(blink_start_us, i));
}
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (result == ESP_OK) {
const uint64_t blink_us = esp_timer_get_time() - blink_start_us;
const uint32_t steps = sizeof(blink_closure) / sizeof(blink_closure[0]);
const uint32_t steps = step_count;
const uint64_t step_fps = lcd_direct_emote_fps_x100(steps, blink_us);
const uint64_t update_fps = lcd_direct_emote_fps_x100(eye_updates, blink_us);
/* 整轮结束再打日志,串口输出不落在被测眨眼窗口中。 */
ESP_LOGI(TAG, "emote_fps blink_us=%llu steps=%u step_fps=%llu.%02llu "
"eye_updates=%u update_fps=%llu.%02llu draw_avg_us=%llu "
"draw_max_us=%u late_steps=%u/%u",
"draw_max_us=%u late_steps=%u/%u face=%s",
(unsigned long long)blink_us, (unsigned)steps,
(unsigned long long)(step_fps / 100), (unsigned long long)(step_fps % 100),
(unsigned)eye_updates,
(unsigned long long)(update_fps / 100), (unsigned long long)(update_fps % 100),
(unsigned long long)(draw_us_total / steps), (unsigned)draw_us_max,
(unsigned)late_frames, (unsigned)steps);
(unsigned)late_frames, (unsigned)steps, listening ? "listening" : "idle");
ESP_LOGI(TAG, "emote_cost lock_avg_us=%llu lock_max_us=%u "
"compose_avg_us=%llu compose_max_us=%u "
"copy_avg_us=%llu copy_max_us=%u spi_avg_us=%llu spi_max_us=%u",
@@ -2301,6 +2456,10 @@ static esp_err_t lcd_direct_play_asset_sequence(const lcd_direct_request_t *requ
strcmp(request->asset_dir, "jichubiaoqing") == 0) {
return lcd_direct_play_emote_face_until_pending(request);
}
if (s_emote_ready && s_listening_ready && request->duration_ms == 0U &&
strcmp(request->asset_dir, "listening") == 0) {
return lcd_direct_play_emote_face_until_pending(request);
}
#endif
if (request->asset_dir[0] == '\0' ||