diff --git a/components/platform/src/lcd_direct.c b/components/platform/src/lcd_direct.c index edfaa64..a62e14d 100644 --- a/components/platform/src/lcd_direct.c +++ b/components/platform/src/lcd_direct.c @@ -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; } diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 385da57..995f2a7 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -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" diff --git a/tools/test_lcd_emote_timeline.py b/tools/test_lcd_emote_timeline.py new file mode 100644 index 0000000..e180c19 --- /dev/null +++ b/tools/test_lcd_emote_timeline.py @@ -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 +#include +#include +#include +#include +#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) diff --git a/tools/test_lcd_emote_transport.py b/tools/test_lcd_emote_transport.py index 6c5fa56..a8e5a37 100644 --- a/tools/test_lcd_emote_transport.py +++ b/tools/test_lcd_emote_transport.py @@ -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 #include #include @@ -16,6 +17,12 @@ prefix='''#include #include #include #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 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')