feat(lcd): add cached listening animation and asset guidelines
This commit is contained in:
@@ -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' ||
|
||||
|
||||
Reference in New Issue
Block a user