feat(lcd): animate fortune results with shared GFX layers
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user