feat(lcd): animate fortune results with shared GFX layers

This commit is contained in:
chenjintao
2026-10-10 11:36:54 +08:00
parent bf06618848
commit ee32b4d63f
26 changed files with 3063 additions and 11 deletions
+207
View File
@@ -38,6 +38,7 @@
#include "gfx.h"
#include "lcd_emote_assets.h"
#include "lcd_emote_listening.h"
#include "lcd_emote_fortune.h"
#endif
#define LCD_DIRECT_SPI_HOST SPI2_HOST
@@ -970,6 +971,7 @@ static esp_err_t lcd_direct_listening_prepare(void)
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_obj_set_visible(s_emote_background, true), TAG, "show face base failed");
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");
@@ -1166,6 +1168,203 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
}
return result != ESP_OK ? result : lock_err;
}
typedef struct {
lcd_fortune_texture_t shared[LCD_FORTUNE_SHARED_COUNT], text;
uint8_t *pixels[LCD_FORTUNE_SHARED_COUNT + 1], *frame;
gfx_image_dsc_t image, rod_image;
gfx_obj_t *object;
} lcd_direct_fortune_scene_t;
/* 极端锁/解绑故障保留已绑定资源到重启,不能释放给 GFX 的悬空指针。 */
static lcd_direct_fortune_scene_t *s_fortune_orphan;
static int lcd_direct_fortune_sequence_index(const lcd_direct_request_t *request)
{
if (request == NULL || request->step_count != 6) return -1;
for (size_t i = 0; i < 6; ++i) {
const platform_lcd_presentation_step_t *step = &request->steps[i];
if (step->asset_dir == NULL || step->frame_index != i % 3 ||
step->icon_id != PLATFORM_LCD_PRESENTATION_NO_ICON ||
step->delay_ms != (i == 5 ? 1500U : 500U)) return -1;
if (i < 3 && strcmp(step->asset_dir, "chouqian_qianzhi") != 0) return -1;
if (i >= 3 && strcmp(step->asset_dir, request->steps[3].asset_dir) != 0) return -1;
}
for (size_t i = 0; i < LCD_FORTUNE_RESULT_COUNT; ++i)
if (strcmp(request->steps[3].asset_dir, lcd_fortune_result_dirs[i]) == 0) return (int)i;
return -1; /* 不匹配的通用步骤仍使用原 PNG 播放,不偷换其他动作。 */
}
static void lcd_direct_fortune_free(lcd_direct_fortune_scene_t *scene)
{
if (scene == NULL) return;
for (size_t i = 0; i <= LCD_FORTUNE_SHARED_COUNT; ++i) free(scene->pixels[i]);
free(scene->frame);
free(scene);
}
static lcd_direct_fortune_scene_t *lcd_direct_fortune_prepare(size_t index)
{
if (index >= LCD_FORTUNE_RESULT_COUNT) return NULL;
lcd_direct_fortune_scene_t *scene = heap_caps_calloc(1, sizeof(*scene), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (scene == NULL) return NULL;
for (size_t i = 0; i <= LCD_FORTUNE_SHARED_COUNT; ++i) {
const lcd_fortune_asset_t *asset = i < LCD_FORTUNE_SHARED_COUNT ?
&lcd_fortune_shared[i] : &lcd_fortune_text[index];
size_t bytes = (size_t)asset->width * asset->height * 3;
scene->pixels[i] = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (scene->pixels[i] == NULL || !lcd_emote_fortune_decode(asset->data,
asset->compressed_size, scene->pixels[i], bytes)) {
lcd_direct_fortune_free(scene);
return NULL;
}
lcd_fortune_texture_t texture = {asset->width, asset->height, scene->pixels[i]};
if (i < LCD_FORTUNE_SHARED_COUNT) scene->shared[i] = texture;
else scene->text = texture;
}
const size_t bytes = LCD_FORTUNE_WIDTH * LCD_FORTUNE_HEIGHT * 2;
scene->frame = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (scene->frame == NULL) {lcd_direct_fortune_free(scene); return NULL;}
scene->image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565, .header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = LCD_FORTUNE_WIDTH, .header.h = LCD_FORTUNE_HEIGHT,
.data_size = bytes, .data = scene->frame,
};
const lcd_fortune_asset_t *rod = &lcd_fortune_shared[LCD_FORTUNE_ROD];
scene->rod_image = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565A8, .header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = rod->width, .header.h = rod->height,
.data_size = (size_t)rod->width * rod->height * 3,
.data = scene->pixels[LCD_FORTUNE_ROD],
};
return scene;
}
/* 同一任务持锁退出并解绑对象,然后才能释放一次性 PSRAM 图层。 */
static esp_err_t lcd_direct_fortune_detach(lcd_direct_fortune_scene_t *scene)
{
esp_err_t err = gfx_emote_lock(s_emote_handle);
if (err != ESP_OK) return err;
portENTER_CRITICAL(&s_state_lock);
s_emote_active = false;
portEXIT_CRITICAL(&s_state_lock);
err = lcd_direct_emote_select_face(false);
if (scene->object != NULL) {
esp_err_t deleted = gfx_obj_delete(scene->object);
if (deleted == ESP_OK) scene->object = NULL;
if (err == ESP_OK) err = deleted;
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
return err != ESP_OK ? err : unlock_err;
}
static esp_err_t lcd_direct_play_fortune(size_t index)
{
if (s_fortune_orphan != NULL || s_emote_transfer_fault) return ESP_ERR_INVALID_STATE;
if (lcd_direct_should_stop_idle()) return ESP_ERR_INVALID_STATE;
const int64_t prepare_start_us = esp_timer_get_time();
lcd_direct_fortune_scene_t *scene = lcd_direct_fortune_prepare(index);
if (scene == NULL) return ESP_ERR_NOT_SUPPORTED;
esp_err_t result = gfx_emote_lock(s_emote_handle);
if (result != ESP_OK) {lcd_direct_fortune_free(scene); return result;}
bool started = false;
bool rod_hidden = false;
if (lcd_direct_should_stop_idle()) result = ESP_ERR_INVALID_STATE;
if (result == ESP_OK) {
scene->object = gfx_img_create(s_emote_disp);
result = scene->object != NULL ? ESP_OK : ESP_ERR_NO_MEM;
}
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_eyes_open, false);
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_closed_eyes, false);
if (result == ESP_OK && s_emote_cached_eyes != NULL) result = gfx_obj_set_visible(s_emote_cached_eyes, false);
if (result == ESP_OK && s_listening_eyes != NULL) result = gfx_obj_set_visible(s_listening_eyes, false);
if (result == ESP_OK) result = gfx_img_set_src(s_emote_background, &scene->rod_image);
if (result == ESP_OK) result = gfx_obj_set_pos(s_emote_background, 0, 40);
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_background, true);
if (result == ESP_OK) result = gfx_obj_set_pos(scene->object, LCD_FORTUNE_X, LCD_FORTUNE_Y);
if (result == ESP_OK) result = gfx_img_set_src(scene->object, &scene->image);
if (result == ESP_OK) result = lcd_direct_emote_update_hud(true, NULL);
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) result = unlock_err;
size_t pixel_bytes = scene->image.data_size;
for (size_t i = 0; i < LCD_FORTUNE_SHARED_COUNT; ++i)
pixel_bytes += (size_t)scene->shared[i].width * scene->shared[i].height * 3;
pixel_bytes += (size_t)scene->text.width * scene->text.height * 3;
ESP_LOGI(TAG, "Fortune layers asset=%s pixels_psram=%u scene_bytes=%u prepare_us=%llu",
lcd_fortune_result_dirs[index], (unsigned)pixel_bytes, (unsigned)sizeof(*scene),
(unsigned long long)(esp_timer_get_time() - prepare_start_us));
const int64_t start_us = esp_timer_get_time();
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
uint32_t frames = 0, draw_attempts = 0, late = 0, max_draw_us = 0;
uint64_t total_draw_us = 0;
#endif
for (uint32_t ms = 0; result == ESP_OK;) {
if (lcd_direct_should_stop_idle()) {result = ESP_ERR_INVALID_STATE; break;}
const int64_t draw_start_us = esp_timer_get_time();
result = gfx_emote_lock(s_emote_handle);
if (result != ESP_OK) break;
if (!lcd_emote_fortune_build_frame(scene->frame, scene->image.data_size,
scene->shared, &scene->text, ms)) result = ESP_ERR_INVALID_SIZE;
if (result == ESP_OK) result = gfx_img_set_src(scene->object, &scene->image);
if (result == ESP_OK && ms >= 600 && !rod_hidden) {
result = gfx_obj_set_visible(s_emote_background, false);
rod_hidden = result == ESP_OK;
}
if (result == ESP_OK) result = lcd_direct_emote_update_hud(false, NULL);
if (result == ESP_OK) {
portENTER_CRITICAL(&s_state_lock);
s_emote_active = true;
portEXIT_CRITICAL(&s_state_lock);
if (!started) gfx_disp_refresh_all(s_emote_disp);
started = true;
result = lcd_direct_emote_refresh();
}
unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) result = unlock_err;
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
uint32_t draw_us = (uint32_t)(esp_timer_get_time() - draw_start_us);
total_draw_us += draw_us;
++draw_attempts;
if (draw_us > max_draw_us) max_draw_us = draw_us;
if (draw_us >= LCD_FORTUNE_STEP_MS * 1000) ++late;
if (result == ESP_OK) ++frames;
#else
(void)draw_start_us;
#endif
if (result != ESP_OK) break;
if (ms == LCD_FORTUNE_MOTION_MS) break;
uint32_t next_ms = ms + LCD_FORTUNE_STEP_MS;
if (next_ms > LCD_FORTUNE_MOTION_MS) next_ms = LCD_FORTUNE_MOTION_MS;
result = lcd_direct_emote_wait_until(start_us + (int64_t)next_ms * 1000);
ms = next_ms;
}
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (started && frames > 0) {
uint64_t motion_us = esp_timer_get_time() - start_us;
uint64_t fps100 = lcd_direct_emote_fps_x100(frames, motion_us);
ESP_LOGI(TAG, "fortune_fps result=%s frames=%u motion_us=%llu update_fps=%llu.%02llu "
"draw_avg_us=%llu draw_max_us=%u late_frames=%u status=%s",
lcd_fortune_result_dirs[index], (unsigned)frames, (unsigned long long)motion_us,
(unsigned long long)(fps100 / 100), (unsigned long long)(fps100 % 100),
(unsigned long long)(total_draw_us / draw_attempts), (unsigned)max_draw_us, (unsigned)late,
esp_err_to_name(result));
}
#endif
/* 运动仍锚定同一起点;迟到不能吞掉用户阅读签文所需的末帧停留。 */
if (result == ESP_OK) result = lcd_direct_emote_wait_until(
esp_timer_get_time() + (int64_t)LCD_FORTUNE_HOLD_MS * 1000);
esp_err_t detach_err = lcd_direct_fortune_detach(scene);
if (detach_err == ESP_OK) lcd_direct_fortune_free(scene);
else {
s_fortune_orphan = scene;
s_emote_transfer_fault = true;
ESP_LOGE(TAG, "Fortune detach failed; bound pixels retained, restart required");
}
if (result == ESP_OK) result = detach_err;
if (result == ESP_OK && !lcd_direct_should_stop_idle()) lcd_direct_signal_completion();
if (!started && detach_err == ESP_OK && result != ESP_ERR_INVALID_STATE)
return ESP_ERR_NOT_SUPPORTED;
return result;
}
#endif
static uint16_t lcd_direct_swap_color(uint16_t color)
@@ -2457,6 +2656,14 @@ static esp_err_t lcd_direct_play_step_sequence(const lcd_direct_request_t *reque
ESP_ERR_INVALID_ARG,
TAG,
"invalid step sequence");
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
int fortune = lcd_direct_fortune_sequence_index(request);
if (fortune >= 0 && s_emote_ready && LCD_DIRECT_WIDTH == 320 && LCD_DIRECT_HEIGHT == 240) {
esp_err_t err = lcd_direct_play_fortune((size_t)fortune);
if (err != ESP_ERR_NOT_SUPPORTED) return err;
ESP_LOGW(TAG, "Fortune layers unavailable; using PNG fallback");
}
#endif
for (size_t i = 0; i < request->step_count; ++i) {
(void)lcd_direct_service_preheat();