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_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
View File
@@ -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"
+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
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')