perf(lcd): cache idle eye frames and anchor blink timing

This commit is contained in:
chenjintao
2026-10-09 17:03:10 +08:00
parent b5c8aeb4fe
commit 89e8e3abc2
4 changed files with 382 additions and 19 deletions
+270 -14
View File
@@ -104,6 +104,12 @@
#define LCD_DIRECT_EMOTE_DMA_ROWS 16U #define LCD_DIRECT_EMOTE_DMA_ROWS 16U
#define LCD_DIRECT_EMOTE_CLOSED_ROWS 14U #define LCD_DIRECT_EMOTE_CLOSED_ROWS 14U
#define LCD_DIRECT_EMOTE_CLOSED_THRESHOLD 232U #define LCD_DIRECT_EMOTE_CLOSED_THRESHOLD 232U
/* 眼睛包围盒四周留 2px;缓存包含此区域内原有静态脸部像素。 */
#define LCD_DIRECT_EMOTE_CACHE_X 39
#define LCD_DIRECT_EMOTE_CACHE_Y 63
#define LCD_DIRECT_EMOTE_CACHE_WIDTH 242U
#define LCD_DIRECT_EMOTE_CACHE_HEIGHT 97U
#define LCD_DIRECT_EMOTE_CACHE_COUNT 9U
#define LCD_DIRECT_EMOTE_EYES_X 41 #define LCD_DIRECT_EMOTE_EYES_X 41
#define LCD_DIRECT_EMOTE_EYES_Y 65 #define LCD_DIRECT_EMOTE_EYES_Y 65
#define LCD_DIRECT_EMOTE_OPEN_MS 2000U #define LCD_DIRECT_EMOTE_OPEN_MS 2000U
@@ -259,10 +265,25 @@ static gfx_mesh_img_point_q8_t s_emote_eye_rest[LCD_DIRECT_EMOTE_MESH_POINTS];
static uint16_t *s_emote_dma_pixels; static uint16_t *s_emote_dma_pixels;
static esp_err_t s_emote_flush_error; static esp_err_t s_emote_flush_error;
static bool s_emote_transfer_fault; static bool s_emote_transfer_fault;
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
/* 只在 GFX 渲染锁内访问;计数区域提交,步骤内有提交才算一次画面更新。 */
static uint32_t s_emote_completed_regions;
static uint64_t s_emote_copy_us;
static uint64_t s_emote_spi_us;
#endif
static gfx_obj_t *s_emote_closed_eyes; static gfx_obj_t *s_emote_closed_eyes;
static gfx_image_dsc_t s_emote_closed_image; static gfx_image_dsc_t s_emote_closed_image;
static uint8_t *s_emote_closed_pixels; static uint8_t *s_emote_closed_pixels;
static bool s_emote_show_closed; static bool s_emote_show_closed;
static const uint8_t s_emote_cache_closures[LCD_DIRECT_EMOTE_CACHE_COUNT] = {
0, 16, 24, 64, 80, 128, 160, 192, LCD_DIRECT_EMOTE_CLOSED_THRESHOLD,
};
static gfx_image_dsc_t s_emote_cached_images[LCD_DIRECT_EMOTE_CACHE_COUNT];
static uint8_t *s_emote_cached_pixels[LCD_DIRECT_EMOTE_CACHE_COUNT];
static uint16_t *s_emote_capture_pixels;
static gfx_obj_t *s_emote_cached_eyes;
static bool s_emote_cache_ready;
static int s_emote_cache_current = -1;
static bool s_emote_active; static bool s_emote_active;
#endif #endif
@@ -304,6 +325,29 @@ static bool IRAM_ATTR lcd_direct_on_color_trans_done(esp_lcd_panel_io_handle_t p
} }
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST #if CONFIG_TQ_LCD_EMOTE_GFX_TEST
/* 复制 GFX 输出与缓存窗口的交集。兼容整个屏幕或任意分块的 flush。 */
static void lcd_direct_emote_capture(uint16_t *dst, int x1, int y1, int x2, int y2,
const uint16_t *src)
{
int left = x1 > LCD_DIRECT_EMOTE_CACHE_X ? x1 : LCD_DIRECT_EMOTE_CACHE_X;
int top = y1 > LCD_DIRECT_EMOTE_CACHE_Y ? y1 : LCD_DIRECT_EMOTE_CACHE_Y;
int right_limit = LCD_DIRECT_EMOTE_CACHE_X + LCD_DIRECT_EMOTE_CACHE_WIDTH;
int bottom_limit = LCD_DIRECT_EMOTE_CACHE_Y + LCD_DIRECT_EMOTE_CACHE_HEIGHT;
int right = x2 < right_limit ? x2 : right_limit;
int bottom = y2 < bottom_limit ? y2 : bottom_limit;
if (left >= right || top >= bottom) {
return;
}
for (int y = top; y < bottom; ++y) {
memcpy(dst + (y - LCD_DIRECT_EMOTE_CACHE_Y) * LCD_DIRECT_EMOTE_CACHE_WIDTH +
left - LCD_DIRECT_EMOTE_CACHE_X,
src + (y - y1) * (x2 - x1) + left - x1,
(right - left) * sizeof(uint16_t));
}
}
static esp_err_t lcd_direct_emote_prepare_cache(void);
static void lcd_direct_emote_flush(gfx_disp_t *disp, static void lcd_direct_emote_flush(gfx_disp_t *disp,
int x1, int x1,
int y1, int y1,
@@ -311,6 +355,12 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp,
int y2, int y2,
const void *data) const void *data)
{ {
/* 离屏预热期间只捕获 GFX 实际合成结果,不写 LCD、不计播放帧率。 */
if (s_emote_capture_pixels != NULL) {
lcd_direct_emote_capture(s_emote_capture_pixels, x1, y1, x2, y2, data);
(void)gfx_disp_flush_ready(disp, false);
return;
}
portENTER_CRITICAL(&s_state_lock); portENTER_CRITICAL(&s_state_lock);
bool active = s_emote_active; bool active = s_emote_active;
portEXIT_CRITICAL(&s_state_lock); portEXIT_CRITICAL(&s_state_lock);
@@ -334,7 +384,14 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp,
if (rows > (size_t)(y2 - y)) { if (rows > (size_t)(y2 - y)) {
rows = (size_t)(y2 - y); rows = (size_t)(y2 - y);
} }
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const int64_t copy_start_us = esp_timer_get_time();
#endif
memcpy(s_emote_dma_pixels, source, width * rows * sizeof(uint16_t)); memcpy(s_emote_dma_pixels, source, width * rows * sizeof(uint16_t));
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
s_emote_copy_us += esp_timer_get_time() - copy_start_us;
const int64_t spi_start_us = esp_timer_get_time();
#endif
(void)xSemaphoreTake(s_transfer_done_sem, 0); (void)xSemaphoreTake(s_transfer_done_sem, 0);
esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y, x2, y + rows, esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y, x2, y + rows,
s_emote_dma_pixels); s_emote_dma_pixels);
@@ -344,6 +401,9 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp,
/* 超时后不再复用可能仍被 DMA 读取的缓冲,重启前禁止此路径传输。 */ /* 超时后不再复用可能仍被 DMA 读取的缓冲,重启前禁止此路径传输。 */
s_emote_transfer_fault = true; s_emote_transfer_fault = true;
} }
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
s_emote_spi_us += esp_timer_get_time() - spi_start_us;
#endif
if (err != ESP_OK) { if (err != ESP_OK) {
s_emote_flush_error = err; s_emote_flush_error = err;
ESP_LOGE(TAG, "composed emote transfer failed: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "composed emote transfer failed: %s", esp_err_to_name(err));
@@ -352,6 +412,11 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp,
source += width * rows; source += width * rows;
y += rows; y += rows;
} }
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (s_emote_flush_error == ESP_OK) {
++s_emote_completed_regions;
}
#endif
(void)gfx_disp_flush_ready(disp, false); (void)gfx_disp_flush_ready(disp, false);
} }
@@ -544,6 +609,10 @@ static esp_err_t lcd_direct_emote_init(void)
s_emote_hud = gfx_img_create(s_emote_disp); s_emote_hud = gfx_img_create(s_emote_disp);
ESP_RETURN_ON_FALSE(s_emote_hud != NULL, ESP_ERR_NO_MEM, TAG, "create HUD failed"); ESP_RETURN_ON_FALSE(s_emote_hud != NULL, ESP_ERR_NO_MEM, TAG, "create HUD failed");
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_hud, &s_emote_hud_image), TAG, "set HUD failed"); ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_hud, &s_emote_hud_image), TAG, "set HUD failed");
esp_err_t cache_err = lcd_direct_emote_prepare_cache();
if (cache_err != ESP_OK) {
ESP_LOGW(TAG, "Eye cache unavailable, retain live mesh: %s", esp_err_to_name(cache_err));
}
s_emote_ready = true; s_emote_ready = true;
ESP_LOGI(TAG, ESP_LOGI(TAG,
"esp_emote_gfx idle test ready (%u fps, PSRAM composed frame, %u-row DMA staging)", "esp_emote_gfx idle test ready (%u fps, PSRAM composed frame, %u-row DMA staging)",
@@ -602,9 +671,12 @@ static esp_err_t lcd_direct_emote_update_hud(bool force, bool *changed)
return err; return err;
} }
static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms) static esp_err_t lcd_direct_emote_wait_until(int64_t deadline_us)
{ {
const int64_t deadline_us = esp_timer_get_time() + (int64_t)duration_ms * 1000; /* 即使已经迟到,也检查取消;追赶时不忙等、不跳过眼型。 */
if (lcd_direct_should_stop_idle()) {
return ESP_ERR_INVALID_STATE;
}
while (esp_timer_get_time() < deadline_us) { while (esp_timer_get_time() < deadline_us) {
if (lcd_direct_should_stop_idle()) { if (lcd_direct_should_stop_idle()) {
return ESP_ERR_INVALID_STATE; return ESP_ERR_INVALID_STATE;
@@ -634,9 +706,19 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
return ESP_OK; return ESP_OK;
} }
static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
{
return lcd_direct_emote_wait_until(esp_timer_get_time() + (int64_t)duration_ms * 1000);
}
static int64_t lcd_direct_emote_step_deadline(int64_t blink_start_us, size_t step)
{
return blink_start_us + (int64_t)(step + 1U) * LCD_DIRECT_EMOTE_STEP_MS * 1000;
}
/* 同一张眼睛纹理连续收缩并弯曲;没有第二张眼型参与混合。 /* 同一张眼睛纹理连续收缩并弯曲;没有第二张眼型参与混合。
* closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */ * closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */
static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure) static esp_err_t lcd_direct_emote_deform_eye_closure(uint8_t closure)
{ {
bool closed = closure >= LCD_DIRECT_EMOTE_CLOSED_THRESHOLD; bool closed = closure >= LCD_DIRECT_EMOTE_CLOSED_THRESHOLD;
if (closed != s_emote_show_closed) { if (closed != s_emote_show_closed) {
@@ -680,6 +762,117 @@ static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure)
LCD_DIRECT_EMOTE_EYES_Y + offset_y); LCD_DIRECT_EMOTE_EYES_Y + offset_y);
} }
/* 同一任务获取/释放 GFX 锁。缓存只在 LCD 初始化时生成,失败完整回退。 */
static esp_err_t lcd_direct_emote_prepare_cache(void)
{
ESP_RETURN_ON_ERROR(gfx_emote_lock(s_emote_handle), TAG, "lock eye cache failed");
const int64_t start_us = esp_timer_get_time();
const size_t bytes = LCD_DIRECT_EMOTE_CACHE_WIDTH * LCD_DIRECT_EMOTE_CACHE_HEIGHT * 2U;
esp_err_t result = ESP_OK;
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT && result == ESP_OK; ++i) {
s_emote_cached_pixels[i] = heap_caps_calloc(1, bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (s_emote_cached_pixels[i] == NULL) {
result = ESP_ERR_NO_MEM;
break;
}
s_emote_cached_images[i] = (gfx_image_dsc_t) {
.header.cf = GFX_COLOR_FORMAT_RGB565,
.header.magic = C_ARRAY_HEADER_MAGIC,
.header.w = LCD_DIRECT_EMOTE_CACHE_WIDTH,
.header.h = LCD_DIRECT_EMOTE_CACHE_HEIGHT,
.data_size = bytes,
.data = s_emote_cached_pixels[i],
};
result = lcd_direct_emote_deform_eye_closure(s_emote_cache_closures[i]);
if (result == ESP_OK) {
s_emote_capture_pixels = (uint16_t *)s_emote_cached_pixels[i];
gfx_disp_refresh_all(s_emote_disp);
result = gfx_refr_now(s_emote_handle);
s_emote_capture_pixels = NULL;
}
}
s_emote_capture_pixels = NULL;
esp_err_t restore_err = lcd_direct_emote_deform_eye_closure(0);
if (result == ESP_OK) {
result = restore_err;
}
if (result == ESP_OK) {
s_emote_cached_eyes = gfx_img_create(s_emote_disp);
result = s_emote_cached_eyes != NULL ? ESP_OK : ESP_ERR_NO_MEM;
}
if (result == ESP_OK) {
result = gfx_img_set_src(s_emote_cached_eyes, &s_emote_cached_images[0]);
}
if (result == ESP_OK) {
result = gfx_obj_set_pos(s_emote_cached_eyes, LCD_DIRECT_EMOTE_CACHE_X, LCD_DIRECT_EMOTE_CACHE_Y);
}
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_cache_current = 0;
s_emote_cache_ready = true;
ESP_LOGI(TAG, "Eye cache ready frames=%u PSRAM=%uB prepare_us=%llu (no Flash images)",
(unsigned)LCD_DIRECT_EMOTE_CACHE_COUNT,
(unsigned)(bytes * LCD_DIRECT_EMOTE_CACHE_COUNT),
(unsigned long long)(esp_timer_get_time() - start_us));
} else {
if (s_emote_cached_eyes != NULL) {
(void)gfx_obj_delete(s_emote_cached_eyes);
s_emote_cached_eyes = NULL;
}
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT; ++i) {
free(s_emote_cached_pixels[i]);
s_emote_cached_pixels[i] = NULL;
}
(void)gfx_obj_set_visible(s_emote_eyes_open, true);
(void)gfx_obj_set_visible(s_emote_closed_eyes, false);
s_emote_show_closed = false;
}
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
return result != ESP_OK ? result : unlock_err;
}
static int lcd_direct_emote_cache_index(uint8_t closure)
{
if (closure >= LCD_DIRECT_EMOTE_CLOSED_THRESHOLD) {
closure = LCD_DIRECT_EMOTE_CLOSED_THRESHOLD;
}
for (size_t i = 0; i < LCD_DIRECT_EMOTE_CACHE_COUNT; ++i) {
if (s_emote_cache_closures[i] == closure) {
return (int)i;
}
}
return -1;
}
static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure)
{
if (!s_emote_cache_ready) {
return lcd_direct_emote_deform_eye_closure(closure);
}
const int index = lcd_direct_emote_cache_index(closure);
ESP_RETURN_ON_FALSE(index >= 0, ESP_ERR_INVALID_ARG, TAG, "eye closure missing from cache");
if (index == s_emote_cache_current) {
return ESP_OK; /* 闭眼保持不失效、不传输。 */
}
esp_err_t err = gfx_img_set_src(s_emote_cached_eyes, &s_emote_cached_images[index]);
if (err == ESP_OK) {
s_emote_cache_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)
{
return elapsed_us > 0 ? (uint64_t)frames * 100000000ULL / elapsed_us : 0;
}
#endif
static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_request_t *request) static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_request_t *request)
{ {
ESP_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL, ESP_RETURN_ON_FALSE(s_emote_handle != NULL && s_emote_eyes_open != NULL,
@@ -702,7 +895,7 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
if (result == ESP_OK) { if (result == ESP_OK) {
result = unlock_err; result = unlock_err;
} }
ESP_LOGI(TAG, "esp_emote_gfx idle playback started (composed mesh/arc blink)"); ESP_LOGI(TAG, "esp_emote_gfx idle playback started (cached=%d)", s_emote_cache_ready);
/* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */ /* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */
static const uint8_t blink_closure[] = { static const uint8_t blink_closure[] = {
@@ -718,40 +911,103 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
if (result != ESP_OK) { if (result != ESP_OK) {
break; break;
} }
const int64_t blink_start_us = esp_timer_get_time();
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
uint64_t draw_us_total = 0; uint64_t draw_us_total = 0;
uint32_t draw_us_max = 0;
uint32_t late_frames = 0; uint32_t late_frames = 0;
uint32_t eye_updates = 0;
/* lock / compose / PSRAM-copy / SPI-submit-and-wait,均为墙钟时间。 */
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 < sizeof(blink_closure) / sizeof(blink_closure[0]) && result == ESP_OK; ++i) {
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const int64_t frame_start_us = esp_timer_get_time(); const int64_t frame_start_us = esp_timer_get_time();
#endif
result = gfx_emote_lock(s_emote_handle); result = gfx_emote_lock(s_emote_handle);
if (result != ESP_OK) { if (result != ESP_OK) {
break; break;
} }
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const int64_t lock_acquired_us = esp_timer_get_time();
const uint64_t copy_before_us = s_emote_copy_us;
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_eye_closure(blink_closure[i]);
if (result == ESP_OK) { if (result == ESP_OK) {
result = lcd_direct_emote_refresh(); result = lcd_direct_emote_refresh();
} }
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const uint64_t copy_us = s_emote_copy_us - copy_before_us;
const uint64_t spi_us = s_emote_spi_us - spi_before_us;
const uint64_t locked_us = esp_timer_get_time() - lock_acquired_us;
const uint64_t transfer_us = copy_us + spi_us;
const uint32_t costs[4] = {
(uint32_t)(lock_acquired_us - frame_start_us),
(uint32_t)(locked_us > transfer_us ? locked_us - transfer_us : 0),
(uint32_t)copy_us,
(uint32_t)spi_us,
};
for (size_t cost = 0; cost < 4; ++cost) {
cost_total_us[cost] += costs[cost];
if (costs[cost] > cost_max_us[cost]) {
cost_max_us[cost] = costs[cost];
}
}
if (result == ESP_OK && s_emote_completed_regions != regions_before) {
++eye_updates;
}
#endif
unlock_err = gfx_emote_unlock(s_emote_handle); unlock_err = gfx_emote_unlock(s_emote_handle);
if (result == ESP_OK) { if (result == ESP_OK) {
result = unlock_err; result = unlock_err;
} }
if (result == ESP_OK) { if (result == ESP_OK) {
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
const uint32_t elapsed_us = (uint32_t)(esp_timer_get_time() - frame_start_us); const uint32_t elapsed_us = (uint32_t)(esp_timer_get_time() - frame_start_us);
draw_us_total += elapsed_us; draw_us_total += elapsed_us;
const uint32_t period_us = LCD_DIRECT_EMOTE_STEP_MS * 1000U; if (elapsed_us > draw_us_max) {
if (elapsed_us < period_us) { draw_us_max = elapsed_us;
/* 绘制时间属于本帧预算,不再额外固定睡 34ms。 */
result = lcd_direct_emote_wait((period_us - elapsed_us + 999U) / 1000U);
} else {
++late_frames;
/* 过载仍给其他任务调度机会,并在下一步检查 pending。 */
result = lcd_direct_emote_wait(1U);
} }
#endif
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (elapsed_us >= LCD_DIRECT_EMOTE_STEP_MS * 1000U) {
++late_frames;
}
#endif
/* 所有步骤锚定同一时间轴,tick/调度超时不累积到后续截止时间。
* 绘制确实过慢时不跳帧,也不增加固定等待;下一步继续检查取消。 */
result = lcd_direct_emote_wait_until(
lcd_direct_emote_step_deadline(blink_start_us, i));
} }
} }
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
if (result == ESP_OK) { if (result == ESP_OK) {
ESP_LOGI(TAG, "emote blink composed draw_us_avg=%llu late_frames=%u/12", const uint64_t blink_us = esp_timer_get_time() - blink_start_us;
(unsigned long long)(draw_us_total / 12U), (unsigned)late_frames); const uint32_t steps = sizeof(blink_closure) / sizeof(blink_closure[0]);
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",
(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);
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",
(unsigned long long)(cost_total_us[0] / steps), (unsigned)cost_max_us[0],
(unsigned long long)(cost_total_us[1] / steps), (unsigned)cost_max_us[1],
(unsigned long long)(cost_total_us[2] / steps), (unsigned)cost_max_us[2],
(unsigned long long)(cost_total_us[3] / steps), (unsigned)cost_max_us[3]);
} }
#endif
++cycles; ++cycles;
} }
+11 -2
View File
@@ -646,8 +646,17 @@ menu "TQ Cat Configuration"
Render the idle face as a static RGB565 background plus an Render the idle face as a static RGB565 background plus an
RGB565A8 eye layer. Only the eye region is refreshed while RGB565A8 eye layer. Only the eye region is refreshed while
blinking. Other presentations continue to use the existing blinking. Other presentations continue to use the existing
full-screen PNG renderer. This test path currently omits the full-screen PNG renderer. Clock and battery status are overlaid
clock and battery overlays from the idle screen. on the idle expression.
config TQ_LCD_EMOTE_FPS_LOG
bool "Log measured GFX blink FPS"
depends on TQ_LCD_EMOTE_GFX_TEST
default n
help
Log once per completed blink: measured step FPS, actual eye
update FPS, draw time and deadline overruns. Excludes the idle
open-eye pause. This does not measure panel scan rate.
config TQ_SCREEN_COLOR_ORDER_BGR config TQ_SCREEN_COLOR_ORDER_BGR
bool "Use BGR color order" bool "Use BGR color order"
+58
View File
@@ -0,0 +1,58 @@
from pathlib import Path
import re, subprocess, tempfile
src=(Path(__file__).resolve().parents[1]/'components/platform/src/lcd_direct.c').read_text()
def extract(name):
m=re.search(r'static [^;{}]+\b'+name+r'\([^;{}]*\)\s*\{',src)
a=m.start();b=src.index('{',a)+1;depth=1
while depth:
if src[b]=='{':depth+=1
if src[b]=='}':depth-=1
b+=1
return src[a:b]
prefix='''#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <assert.h>
#include <stdio.h>
#define ESP_OK 0
#define ESP_ERR_INVALID_STATE 1
#define LCD_DIRECT_EMOTE_STEP_MS 34U
#define LCD_DIRECT_PRESENTATION_WAIT_POLL_MS 20U
#define pdMS_TO_TICKS(x) ((x)/10)
#define ESP_RETURN_ON_ERROR(expr,...) do {int e=(expr);if(e)return e;}while(0)
typedef int esp_err_t;
typedef uint32_t TickType_t;
static int s_emote_handle, delays, stop_after, locks, unlocks;
static bool stop;
static int64_t now;
static int64_t esp_timer_get_time(void){return now;}
static bool lcd_direct_should_stop_idle(void){return stop;}
static int gfx_emote_lock(int h){++locks;return 0;}
static int gfx_emote_unlock(int h){++unlocks;return 0;}
static int lcd_direct_emote_update_hud(bool force,bool *changed){*changed=false;return 0;}
static int lcd_direct_emote_refresh(void){return 0;}
static void vTaskDelay(TickType_t ticks){assert(ticks>0);now+=ticks*10000;++delays;if(stop_after && delays>=stop_after)stop=true;}
'''
test='''int main(void){
for(size_t i=0;i<12;++i){
assert(lcd_direct_emote_step_deadline(123000,i)==123000+(int64_t)(i+1)*34000);
now+=13000;
assert(lcd_direct_emote_wait_until(lcd_direct_emote_step_deadline(0,i))==0);
assert(now>=(int64_t)(i+1)*34000 && now<(int64_t)(i+1)*34000+10000);
}
int64_t anchored=now;
now=0;
for(size_t i=0;i<12;++i){now+=13000;assert(lcd_direct_emote_wait(21)==0);}
assert(anchored<418000 && now>=480000);
now=200000;int before=delays;
assert(lcd_direct_emote_wait_until(100000)==0 && now==200000 && delays==before);
stop=true;assert(lcd_direct_emote_wait_until(100000)==1);
stop=false;stop_after=delays+1;assert(lcd_direct_emote_wait_until(now+100000)==1);
assert(locks==unlocks);
printf("PASS: fixed deadlines, 10ms tick drift reduction (anchored=%lldus), late/stop handling and paired HUD locks\\n",(long long)anchored);
}'''
with tempfile.TemporaryDirectory(prefix='emote-timeline-') as tmp:
c=Path(tmp)/'test.c';exe=Path(tmp)/'test'
c.write_text(prefix+'\n'.join(extract(n) for n in ['lcd_direct_emote_wait_until','lcd_direct_emote_wait','lcd_direct_emote_step_deadline'])+'\n'+test)
subprocess.run(['clang','-std=c11','-fsanitize=address,undefined',str(c),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
+43 -3
View File
@@ -9,6 +9,7 @@ def extract(name):
if src[b]=='}': depth-=1 if src[b]=='}': depth-=1
b+=1 b+=1
return src[a:b] return src[a:b]
cache_table=re.search(r'static const uint8_t s_emote_cache_closures.*?\n};',src,re.S).group(0)
prefix='''#include <stdint.h> prefix='''#include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdbool.h> #include <stdbool.h>
@@ -16,6 +17,12 @@ prefix='''#include <stdint.h>
#include <string.h> #include <string.h>
#include <assert.h> #include <assert.h>
#define LCD_DIRECT_WIDTH 320 #define LCD_DIRECT_WIDTH 320
#define LCD_DIRECT_EMOTE_CACHE_COUNT 9
#define LCD_DIRECT_EMOTE_CLOSED_THRESHOLD 232
#define LCD_DIRECT_EMOTE_CACHE_X 39
#define LCD_DIRECT_EMOTE_CACHE_Y 63
#define LCD_DIRECT_EMOTE_CACHE_WIDTH 242
#define LCD_DIRECT_EMOTE_CACHE_HEIGHT 97
#define LCD_DIRECT_EMOTE_DMA_ROWS 16 #define LCD_DIRECT_EMOTE_DMA_ROWS 16
#define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100 #define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100
#define portENTER_CRITICAL(x) ((void)0) #define portENTER_CRITICAL(x) ((void)0)
@@ -29,7 +36,12 @@ prefix='''#include <stdint.h>
typedef int esp_err_t; typedef int esp_err_t;
typedef int gfx_disp_t; typedef int gfx_disp_t;
static bool s_emote_active, s_emote_transfer_fault; static bool s_emote_active, s_emote_transfer_fault;
static uint32_t s_emote_completed_regions;
static uint64_t s_emote_copy_us, s_emote_spi_us;
static int64_t fake_time;
static int64_t esp_timer_get_time(void) {return ++fake_time;}
static int s_emote_flush_error, s_panel, s_transfer_done_sem; static int s_emote_flush_error, s_panel, s_transfer_done_sem;
static uint16_t *s_emote_capture_pixels;
static uint16_t dma[5120]; static uint16_t dma[5120];
static uint16_t *s_emote_dma_pixels=dma; static uint16_t *s_emote_dma_pixels=dma;
static uint16_t capture[320*240]; static uint16_t capture[320*240];
@@ -44,12 +56,36 @@ static int esp_lcd_panel_draw_bitmap(int panel,int x1,int y1,int x2,int y2,const
} }
''' '''
test='''int main(void) { test='''int main(void) {
for(unsigned i=0;i<9;++i) assert(lcd_direct_emote_cache_index(s_emote_cache_closures[i])==(int)i);
assert(lcd_direct_emote_cache_index(255)==8);
assert(lcd_direct_emote_cache_index(1)==-1);
assert(lcd_direct_emote_fps_x100(12,570000)==2105);
assert(lcd_direct_emote_fps_x100(9,570000)==1578);
assert(lcd_direct_emote_fps_x100(12,0)==0);
assert(lcd_direct_emote_fps_x100(0,570000)==0);
uint16_t source[320*240]; uint16_t source[320*240];
for(int i=0;i<320*240;++i) source[i]=(uint16_t)i; for(int i=0;i<320*240;++i) source[i]=(uint16_t)i;
uint16_t cache[242*97+2]; cache[0]=cache[242*97+1]=0xabcd;
s_emote_capture_pixels=cache+1; calls=ready=0;
lcd_direct_emote_flush(NULL,0,0,320,240,source);
assert(calls==0 && ready==1);
for(int y=0;y<97;++y) for(int x=0;x<242;++x)
assert(cache[1+y*242+x]==source[(y+63)*320+x+39]);
assert(cache[0]==0xabcd && cache[242*97+1]==0xabcd);
memset(cache+1,0,242*97*2);
for(int y=0;y<240;y+=8)
lcd_direct_emote_capture(cache+1,0,y,320,y+8,source+y*320);
for(int y=0;y<97;++y) for(int x=0;x<242;++x)
assert(cache[1+y*242+x]==source[(y+63)*320+x+39]);
s_emote_capture_pixels=NULL;
for(width=1;width<=320;++width) { for(width=1;width<=320;++width) {
calls=ready=fail_call=timeout_call=0;origin_y=65;s_emote_active=true; calls=ready=fail_call=timeout_call=0;origin_y=65;s_emote_active=true;
s_emote_transfer_fault=false;s_emote_flush_error=0; s_emote_transfer_fault=false;s_emote_flush_error=0;
const uint32_t before=s_emote_completed_regions;
lcd_direct_emote_flush(NULL,0,65,width,158,source); lcd_direct_emote_flush(NULL,0,65,width,158,source);
assert(s_emote_completed_regions==before+CONFIG_TQ_LCD_EMOTE_FPS_LOG);
assert((s_emote_copy_us>0)==CONFIG_TQ_LCD_EMOTE_FPS_LOG);
assert((s_emote_spi_us>0)==CONFIG_TQ_LCD_EMOTE_FPS_LOG);
assert(ready==1 && s_emote_flush_error==0); assert(ready==1 && s_emote_flush_error==0);
assert(memcmp(source,capture,width*93*2)==0); assert(memcmp(source,capture,width*93*2)==0);
assert(calls==(93+5120/width-1)/(5120/width)); assert(calls==(93+5120/width-1)/(5120/width));
@@ -75,6 +111,10 @@ test='''int main(void) {
}''' }'''
with tempfile.TemporaryDirectory(prefix='emote-transport-') as tmp: with tempfile.TemporaryDirectory(prefix='emote-transport-') as tmp:
c=Path(tmp)/'test.c';exe=Path(tmp)/'test' c=Path(tmp)/'test.c';exe=Path(tmp)/'test'
c.write_text(prefix+extract('lcd_direct_emote_flush')+'\n'+extract('lcd_direct_emote_draw_closed_eyes')+'\n'+test) for enabled in (0,1):
subprocess.run(['clang','-std=c11','-fsanitize=address,undefined',str(c),'-o',str(exe)],check=True) c.write_text('#define CONFIG_TQ_LCD_EMOTE_FPS_LOG '+str(enabled)+'\n'+prefix+cache_table+'\n'+extract('lcd_direct_emote_cache_index')+'\n'+
subprocess.run([str(exe)],check=True) extract('lcd_direct_emote_capture')+'\n'+extract('lcd_direct_emote_flush')+'\n'+extract('lcd_direct_emote_draw_closed_eyes')+'\n'+
extract('lcd_direct_emote_fps_x100')+'\n'+test)
subprocess.run(['clang','-std=c11','-fsanitize=address,undefined',str(c),'-o',str(exe)],check=True)
subprocess.run([str(exe)],check=True)
print('PASS: cache mapping/capture without LCD transfer, FPS logging on/off and measured-rate arithmetic')