feat(lcd): add compressed GFX idle expression animation

Integrate esp_emote_gfx for idle rendering with symmetric mesh-based blinking and LCD DMA flush synchronization.

Store cropped face layers with lossless RLE, decode once into PSRAM, and retain the PNG backend for other expressions. Include source layers and asset generation tools.
This commit is contained in:
chenjintao
2026-10-09 14:19:16 +08:00
parent 7bc9a3dc76
commit f6408be05b
21 changed files with 1554 additions and 5 deletions
+339 -1
View File
@@ -32,6 +32,11 @@
#include "sdkconfig.h"
#include "soc/soc_caps.h"
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#include "gfx.h"
#include "lcd_emote_assets.h"
#endif
#define LCD_DIRECT_SPI_HOST SPI2_HOST
#define LCD_DIRECT_NATIVE_WIDTH 240U
#define LCD_DIRECT_NATIVE_HEIGHT 320U
@@ -92,6 +97,18 @@
#define LCD_DIRECT_CLOCK_COLOR 0xFFFFU
#define LCD_DIRECT_CLOCK_OUTLINE_COLOR 0x0000U
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
#define LCD_DIRECT_EMOTE_FPS 30U
#define LCD_DIRECT_EMOTE_BUFFER_ROWS 4U
#define LCD_DIRECT_EMOTE_EYES_X 41
#define LCD_DIRECT_EMOTE_EYES_Y 65
#define LCD_DIRECT_EMOTE_OPEN_MS 2000U
#define LCD_DIRECT_EMOTE_STEP_MS 34U
#define LCD_DIRECT_EMOTE_MESH_COLS 16U
#define LCD_DIRECT_EMOTE_MESH_ROWS 2U
#define LCD_DIRECT_EMOTE_MESH_POINTS ((LCD_DIRECT_EMOTE_MESH_COLS + 1U) * (LCD_DIRECT_EMOTE_MESH_ROWS + 1U))
#endif
/* ST7789P3 的方向控制(MADCTL)和 RAM 数据格式控制(RAMCTRL)命令号。 */
#define LCD_CMD_MADCTL 0x36
#define LCD_CMD_RAMCTRL 0xB0
@@ -217,6 +234,22 @@ static uint16_t s_line_buffer[LCD_DIRECT_PIXELS_PER_TRANSFER];
static lcd_direct_decoded_image_t s_frame_cache[LCD_DIRECT_FRAME_CACHE_MAX];
static uint32_t s_cache_use_counter;
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
/* 一次性解压的图像随 LCD 设备常驻;眨眼只修改网格,不重复解压。 */
static gfx_image_dsc_t s_emote_base_image;
static gfx_image_dsc_t s_emote_eye_image;
static uint8_t *s_emote_base_pixels;
static uint8_t *s_emote_eye_pixels;
static bool s_emote_ready;
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 bool s_emote_active;
#endif
/* 渲染状态由 s_state_lock 保护,面板和缓存访问则由 s_lock 串行化。 */
static void lcd_direct_signal_completion(void);
static void lcd_direct_clock_text(char text[LCD_DIRECT_CLOCK_TEXT_LENGTH + 1U]);
@@ -235,12 +268,297 @@ 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);
}
return higher_priority_task_woken == pdTRUE;
}
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
static void lcd_direct_emote_flush(gfx_disp_t *disp,
int x1,
int y1,
int x2,
int y2,
const void *data)
{
portENTER_CRITICAL(&s_state_lock);
bool active = s_emote_active;
if (active) {
s_emote_flush_pending = disp;
}
portEXIT_CRITICAL(&s_state_lock);
if (!active) {
(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));
(void)gfx_disp_flush_ready(disp, false);
}
}
static esp_err_t lcd_direct_emote_decode_asset(const lcd_emote_rle_asset_t *asset,
gfx_image_dsc_t *image, uint8_t **pixels)
{
size_t count = (size_t)asset->width * asset->height;
ESP_RETURN_ON_FALSE(count > 0 && count <= SIZE_MAX / 3, ESP_ERR_INVALID_SIZE,
TAG, "invalid compressed asset dimensions");
uint8_t *decoded = heap_caps_malloc(count * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
ESP_RETURN_ON_FALSE(decoded != NULL, ESP_ERR_NO_MEM, TAG, "allocate emote asset PSRAM failed");
if (!lcd_emote_decode_rle(asset->data, asset->compressed_size, decoded, count)) {
free(decoded);
return ESP_ERR_INVALID_SIZE;
}
*image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565A8,
.header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = asset->width,
.header.h = asset->height,
.data_size = count * 3,
.data = decoded,
};
*pixels = decoded;
return ESP_OK;
}
static esp_err_t lcd_direct_emote_prepare_assets(void)
{
if (s_emote_base_pixels != NULL && s_emote_eye_pixels != NULL) {
return ESP_OK;
}
int64_t start_us = esp_timer_get_time();
esp_err_t err = lcd_direct_emote_decode_asset(&idle_base_rle_asset,
&s_emote_base_image, &s_emote_base_pixels);
if (err == ESP_OK) {
err = lcd_direct_emote_decode_asset(&idle_eyes_open_rle_asset,
&s_emote_eye_image, &s_emote_eye_pixels);
}
if (err != ESP_OK) {
free(s_emote_base_pixels);
free(s_emote_eye_pixels);
s_emote_base_pixels = NULL;
s_emote_eye_pixels = NULL;
return err;
}
ESP_LOGI(TAG, "emote assets decoded once: compressed=%uB PSRAM=%uB decode_us=%lld",
(unsigned)(idle_base_rle_asset.compressed_size + idle_eyes_open_rle_asset.compressed_size),
(unsigned)(s_emote_base_image.data_size + s_emote_eye_image.data_size),
(long long)(esp_timer_get_time() - start_us));
return ESP_OK;
}
static esp_err_t lcd_direct_emote_init(void)
{
ESP_RETURN_ON_ERROR(lcd_direct_emote_prepare_assets(), TAG, "decode emote assets failed");
const gfx_core_config_t core_config = {
.fps = LCD_DIRECT_EMOTE_FPS,
.task = GFX_EMOTE_INIT_CONFIG(),
};
s_emote_handle = gfx_emote_init(&core_config);
if (s_emote_handle == NULL) {
free(s_emote_base_pixels);
free(s_emote_eye_pixels);
s_emote_base_pixels = NULL;
s_emote_eye_pixels = NULL;
return ESP_ERR_NO_MEM;
}
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,
},
.buffers = {
.buf_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_BUFFER_ROWS,
},
};
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_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");
ESP_RETURN_ON_ERROR(gfx_img_set_src(background, (void *)&s_emote_base_image), TAG, "set emote background failed");
/* GFX 用黑色背景清除旧眼型,只绘制裁剪后的鼻子和胡须。 */
ESP_RETURN_ON_ERROR(gfx_disp_set_bg_color(s_emote_disp, (gfx_color_t) { .full = 0 }),
TAG, "set emote black background failed");
ESP_RETURN_ON_ERROR(gfx_disp_set_bg_enable(s_emote_disp, true), TAG, "enable emote background failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(background, idle_base_rle_asset.x, idle_base_rle_asset.y),
TAG, "position emote base failed");
ESP_RETURN_ON_ERROR(gfx_mesh_img_set_src(s_emote_eyes_open, (void *)&s_emote_eye_image),
TAG, "set emote eyes failed");
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_eyes_open,
LCD_DIRECT_EMOTE_EYES_X, LCD_DIRECT_EMOTE_EYES_Y),
TAG, "position emote eyes failed");
ESP_RETURN_ON_ERROR(gfx_mesh_img_set_grid(s_emote_eyes_open,
LCD_DIRECT_EMOTE_MESH_COLS, LCD_DIRECT_EMOTE_MESH_ROWS),
TAG, "configure eye mesh failed");
ESP_RETURN_ON_FALSE(gfx_mesh_img_get_point_count(s_emote_eyes_open) == LCD_DIRECT_EMOTE_MESH_POINTS,
ESP_ERR_INVALID_SIZE, TAG, "unexpected eye mesh size");
for (size_t i = 0; i < LCD_DIRECT_EMOTE_MESH_POINTS; ++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");
}
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock esp_emote_gfx failed");
ESP_RETURN_ON_ERROR(gfx_emote_unlock(s_emote_handle), TAG, "unlock esp_emote_gfx failed");
s_emote_ready = true;
ESP_LOGI(TAG,
"esp_emote_gfx idle test ready (%u fps, 2 x %u-row DMA buffers)",
(unsigned)LCD_DIRECT_EMOTE_FPS,
(unsigned)LCD_DIRECT_EMOTE_BUFFER_ROWS);
return ESP_OK;
}
static bool lcd_direct_should_stop_idle(void)
{
bool stop = false;
portENTER_CRITICAL(&s_state_lock);
stop = s_stop_requested || s_pending_valid;
portEXIT_CRITICAL(&s_state_lock);
return stop;
}
static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
{
uint32_t remaining_ms = duration_ms;
while (remaining_ms > 0U) {
if (lcd_direct_should_stop_idle()) {
return ESP_ERR_INVALID_STATE;
}
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;
}
return ESP_OK;
}
/* 同一张眼睛纹理连续收缩并弯曲;没有第二张眼型参与混合。
* closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */
static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure)
{
gfx_mesh_img_point_q8_t points[LCD_DIRECT_EMOTE_MESH_POINTS];
/* 上下眼睑对称收向原眼睛中线,包围盒位置补偿保持脸部锚点。 */
const int32_t center_y = 46 * 256;
int32_t min_y_q8 = INT32_MAX;
const int32_t scale = 256 - ((int32_t)closure * 248 / 255);
for (size_t i = 0; i < LCD_DIRECT_EMOTE_MESH_POINTS; ++i) {
points[i] = s_emote_eye_rest[i];
int32_t x = points[i].x_q8 / 256;
/* 238x93 纹理中,两只眼睛的中心分别约为 x=49 和 x=189。
* 抛物线把压扁的眼圈弯成浅浅的闭眼弧线。 */
int32_t dx = x - (x < 119 ? 49 : 189);
int32_t arch = 8 * 256 - dx * dx * (8 * 256) / (46 * 46);
if (arch < 0) {
arch = 0;
}
/* 弧线相对中线上下各约 4px,避免闭合时眼睛中心整体上移。 */
points[i].y_q8 = center_y + (points[i].y_q8 - center_y) * scale / 256
+ (4 * 256 - arch) * closure / 255;
if (points[i].y_q8 < min_y_q8) {
min_y_q8 = points[i].y_q8;
}
}
ESP_RETURN_ON_ERROR(gfx_mesh_img_set_points_q8(s_emote_eyes_open, points,
LCD_DIRECT_EMOTE_MESH_POINTS),
TAG, "deform eye mesh failed");
/* GFX 绘制原点会减掉网格 bounds_min;将该偏移加回对象位置,
* 才能保持纹理坐标相对于脸部的绝对位置,避免收缩时整体上移。 */
int32_t offset_y = min_y_q8 >= 0 ? min_y_q8 / 256
: -((-min_y_q8 + 255) / 256);
return gfx_obj_set_pos(s_emote_eyes_open, LCD_DIRECT_EMOTE_EYES_X,
LCD_DIRECT_EMOTE_EYES_Y + offset_y);
}
static esp_err_t lcd_direct_play_emote_face_until_pending(void)
{
ESP_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL,
ESP_ERR_INVALID_STATE,
TAG,
"esp_emote_gfx is not ready");
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock esp_emote_gfx failed");
portENTER_CRITICAL(&s_state_lock);
s_emote_active = true;
portEXIT_CRITICAL(&s_state_lock);
esp_err_t result = lcd_direct_emote_set_eye_closure(0U);
if (result == ESP_OK) {
gfx_disp_refresh_all(s_emote_disp);
result = gfx_refr_now(s_emote_handle);
}
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)");
/* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */
static const uint8_t blink_closure[] = {
24U, 80U, 160U, 232U, 255U, 255U,
232U, 192U, 128U, 64U, 16U, 0U,
};
while (result == ESP_OK) {
result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_OPEN_MS);
if (result != ESP_OK) {
break;
}
for (size_t i = 0; i < sizeof(blink_closure) / sizeof(blink_closure[0]) && result == ESP_OK; ++i) {
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);
}
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);
}
}
}
esp_err_t lock_err = gfx_emote_lock(s_emote_handle);
if (lock_err == ESP_OK) {
portENTER_CRITICAL(&s_state_lock);
s_emote_active = false;
portEXIT_CRITICAL(&s_state_lock);
lock_err = gfx_emote_unlock(s_emote_handle);
}
if (result == ESP_ERR_INVALID_STATE && lcd_direct_should_stop_idle()) {
result = ESP_OK;
}
return result != ESP_OK ? result : lock_err;
}
#endif
static uint16_t lcd_direct_swap_color(uint16_t color)
{
#if CONFIG_TQ_SCREEN_SWAP_COLOR_BYTES
@@ -1558,6 +1876,11 @@ static esp_err_t lcd_direct_play_step_sequence(const lcd_direct_request_t *reque
static esp_err_t lcd_direct_play_basic_face_until_pending(void)
{
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
if (s_emote_ready) {
return lcd_direct_play_emote_face_until_pending();
}
#endif
const lcd_direct_request_t request = {
.kind = PLATFORM_LCD_PRESENTATION_ASSET_SEQUENCE,
.asset_dir = "jichubiaoqing",
@@ -1638,6 +1961,14 @@ esp_err_t lcd_direct_init_device(void)
ESP_RETURN_ON_ERROR(lcd_direct_panel_init(), TAG, "esp_lcd panel init failed");
ESP_RETURN_ON_ERROR(lcd_direct_clear_panel(), TAG, "panel clear failed");
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
esp_err_t emote_err = lcd_direct_emote_init();
if (emote_err != ESP_OK) {
ESP_LOGW(TAG, "esp_emote_gfx idle test unavailable; using PNG fallback: %s",
esp_err_to_name(emote_err));
}
#endif
ESP_RETURN_ON_ERROR(ledc_set_duty(LEDC_LOW_SPEED_MODE,
LCD_DIRECT_BACKLIGHT_PWM_CHANNEL,
lcd_direct_backlight_duty(
@@ -1718,7 +2049,14 @@ void lcd_direct_start_render_loop(void)
portEXIT_CRITICAL(&s_state_lock);
ESP_LOGI(TAG, "Direct LCD render loop started");
esp_err_t preheat_err = lcd_direct_preheat_basic_face();
esp_err_t preheat_err = ESP_OK;
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
if (!s_emote_ready) {
preheat_err = lcd_direct_preheat_basic_face();
}
#else
preheat_err = lcd_direct_preheat_basic_face();
#endif
if (preheat_err != ESP_OK) {
ESP_LOGW(TAG, "Default LCD face PNG preheat failed: %s", esp_err_to_name(preheat_err));
}