perf(lcd): cache idle eye frames and anchor blink timing
This commit is contained in:
@@ -104,6 +104,12 @@
|
||||
#define LCD_DIRECT_EMOTE_DMA_ROWS 16U
|
||||
#define LCD_DIRECT_EMOTE_CLOSED_ROWS 14U
|
||||
#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_Y 65
|
||||
#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 esp_err_t s_emote_flush_error;
|
||||
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_image_dsc_t s_emote_closed_image;
|
||||
static uint8_t *s_emote_closed_pixels;
|
||||
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;
|
||||
#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
|
||||
/* 复制 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,
|
||||
int x1,
|
||||
int y1,
|
||||
@@ -311,6 +355,12 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp,
|
||||
int y2,
|
||||
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);
|
||||
bool active = s_emote_active;
|
||||
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)) {
|
||||
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));
|
||||
#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);
|
||||
esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y, x2, y + rows,
|
||||
s_emote_dma_pixels);
|
||||
@@ -344,6 +401,9 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp,
|
||||
/* 超时后不再复用可能仍被 DMA 读取的缓冲,重启前禁止此路径传输。 */
|
||||
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) {
|
||||
s_emote_flush_error = 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;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -544,6 +609,10 @@ static esp_err_t lcd_direct_emote_init(void)
|
||||
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_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;
|
||||
ESP_LOGI(TAG,
|
||||
"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;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (lcd_direct_should_stop_idle()) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
@@ -634,9 +706,19 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
|
||||
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 网格保留亚像素精度。 */
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
/* 同一任务获取/释放 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)
|
||||
{
|
||||
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) {
|
||||
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 睁开,保持单一轮廓。 */
|
||||
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) {
|
||||
break;
|
||||
}
|
||||
const int64_t blink_start_us = esp_timer_get_time();
|
||||
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
|
||||
uint64_t draw_us_total = 0;
|
||||
uint32_t draw_us_max = 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) {
|
||||
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
|
||||
const int64_t frame_start_us = esp_timer_get_time();
|
||||
#endif
|
||||
result = gfx_emote_lock(s_emote_handle);
|
||||
if (result != ESP_OK) {
|
||||
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]);
|
||||
if (result == ESP_OK) {
|
||||
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);
|
||||
if (result == ESP_OK) {
|
||||
result = unlock_err;
|
||||
}
|
||||
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);
|
||||
draw_us_total += elapsed_us;
|
||||
const uint32_t period_us = LCD_DIRECT_EMOTE_STEP_MS * 1000U;
|
||||
if (elapsed_us < period_us) {
|
||||
/* 绘制时间属于本帧预算,不再额外固定睡 34ms。 */
|
||||
result = lcd_direct_emote_wait((period_us - elapsed_us + 999U) / 1000U);
|
||||
} else {
|
||||
++late_frames;
|
||||
/* 过载仍给其他任务调度机会,并在下一步检查 pending。 */
|
||||
result = lcd_direct_emote_wait(1U);
|
||||
if (elapsed_us > draw_us_max) {
|
||||
draw_us_max = elapsed_us;
|
||||
}
|
||||
#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) {
|
||||
ESP_LOGI(TAG, "emote blink composed draw_us_avg=%llu late_frames=%u/12",
|
||||
(unsigned long long)(draw_us_total / 12U), (unsigned)late_frames);
|
||||
const uint64_t blink_us = esp_timer_get_time() - blink_start_us;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -646,8 +646,17 @@ menu "TQ Cat Configuration"
|
||||
Render the idle face as a static RGB565 background plus an
|
||||
RGB565A8 eye layer. Only the eye region is refreshed while
|
||||
blinking. Other presentations continue to use the existing
|
||||
full-screen PNG renderer. This test path currently omits the
|
||||
clock and battery overlays from the idle screen.
|
||||
full-screen PNG renderer. Clock and battery status are overlaid
|
||||
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
|
||||
bool "Use BGR color order"
|
||||
|
||||
@@ -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)
|
||||
@@ -9,6 +9,7 @@ def extract(name):
|
||||
if src[b]=='}': depth-=1
|
||||
b+=1
|
||||
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>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
@@ -16,6 +17,12 @@ prefix='''#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#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_TRANSFER_TIMEOUT_MS 100
|
||||
#define portENTER_CRITICAL(x) ((void)0)
|
||||
@@ -29,7 +36,12 @@ prefix='''#include <stdint.h>
|
||||
typedef int esp_err_t;
|
||||
typedef int gfx_disp_t;
|
||||
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 uint16_t *s_emote_capture_pixels;
|
||||
static uint16_t dma[5120];
|
||||
static uint16_t *s_emote_dma_pixels=dma;
|
||||
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) {
|
||||
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];
|
||||
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) {
|
||||
calls=ready=fail_call=timeout_call=0;origin_y=65;s_emote_active=true;
|
||||
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);
|
||||
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(memcmp(source,capture,width*93*2)==0);
|
||||
assert(calls==(93+5120/width-1)/(5120/width));
|
||||
@@ -75,6 +111,10 @@ test='''int main(void) {
|
||||
}'''
|
||||
with tempfile.TemporaryDirectory(prefix='emote-transport-') as tmp:
|
||||
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)
|
||||
subprocess.run(['clang','-std=c11','-fsanitize=address,undefined',str(c),'-o',str(exe)],check=True)
|
||||
subprocess.run([str(exe)],check=True)
|
||||
for enabled in (0,1):
|
||||
c.write_text('#define CONFIG_TQ_LCD_EMOTE_FPS_LOG '+str(enabled)+'\n'+prefix+cache_table+'\n'+extract('lcd_direct_emote_cache_index')+'\n'+
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user