perf(lcd): precompose emote updates and smooth closed eyes
This commit is contained in:
@@ -101,7 +101,9 @@
|
||||
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
|
||||
#define LCD_DIRECT_HUD_ROWS 40U
|
||||
#define LCD_DIRECT_EMOTE_FPS 30U
|
||||
#define LCD_DIRECT_EMOTE_BUFFER_ROWS 4U
|
||||
#define LCD_DIRECT_EMOTE_DMA_ROWS 16U
|
||||
#define LCD_DIRECT_EMOTE_CLOSED_ROWS 14U
|
||||
#define LCD_DIRECT_EMOTE_CLOSED_THRESHOLD 232U
|
||||
#define LCD_DIRECT_EMOTE_EYES_X 41
|
||||
#define LCD_DIRECT_EMOTE_EYES_Y 65
|
||||
#define LCD_DIRECT_EMOTE_OPEN_MS 2000U
|
||||
@@ -254,7 +256,13 @@ static gfx_handle_t s_emote_handle;
|
||||
static gfx_disp_t *s_emote_disp;
|
||||
static gfx_obj_t *s_emote_eyes_open;
|
||||
static gfx_mesh_img_point_q8_t s_emote_eye_rest[LCD_DIRECT_EMOTE_MESH_POINTS];
|
||||
static gfx_disp_t *s_emote_flush_pending;
|
||||
static uint16_t *s_emote_dma_pixels;
|
||||
static esp_err_t s_emote_flush_error;
|
||||
static bool s_emote_transfer_fault;
|
||||
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 bool s_emote_active;
|
||||
#endif
|
||||
|
||||
@@ -288,16 +296,7 @@ static bool IRAM_ATTR lcd_direct_on_color_trans_done(esp_lcd_panel_io_handle_t p
|
||||
(void)user_ctx;
|
||||
|
||||
BaseType_t higher_priority_task_woken = pdFALSE;
|
||||
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
|
||||
gfx_disp_t *emote_disp = NULL;
|
||||
portENTER_CRITICAL_ISR(&s_state_lock);
|
||||
emote_disp = s_emote_flush_pending;
|
||||
s_emote_flush_pending = NULL;
|
||||
portEXIT_CRITICAL_ISR(&s_state_lock);
|
||||
if (emote_disp != NULL) {
|
||||
(void)gfx_disp_flush_ready(emote_disp, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (s_transfer_done_sem != NULL) {
|
||||
(void)xSemaphoreGiveFromISR(s_transfer_done_sem, &higher_priority_task_woken);
|
||||
}
|
||||
@@ -314,26 +313,46 @@ static void lcd_direct_emote_flush(gfx_disp_t *disp,
|
||||
{
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
bool active = s_emote_active;
|
||||
if (active) {
|
||||
s_emote_flush_pending = disp;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
|
||||
if (!active) {
|
||||
if (!active || s_emote_transfer_fault) {
|
||||
(void)gfx_disp_flush_ready(disp, false);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_lcd_panel_draw_bitmap(s_panel, x1, y1, x2, y2, data);
|
||||
if (err != ESP_OK) {
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
if (s_emote_flush_pending == disp) {
|
||||
s_emote_flush_pending = NULL;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
ESP_LOGE(TAG, "esp_emote_gfx flush failed: %s", esp_err_to_name(err));
|
||||
/* GFX 已在 PSRAM 中合成完整区域。这里仅复制/传输,不穿插网格计算。
|
||||
* 单 DMA 暂存区必须等 ISR 确认完成才能复用;整区域发完再通知 GFX。 */
|
||||
const uint16_t *source = data;
|
||||
const size_t width = (size_t)(x2 - x1);
|
||||
if (width == 0 || width > LCD_DIRECT_WIDTH || y2 <= y1 || s_emote_dma_pixels == NULL) {
|
||||
s_emote_flush_error = ESP_ERR_INVALID_SIZE;
|
||||
(void)gfx_disp_flush_ready(disp, false);
|
||||
return;
|
||||
}
|
||||
const size_t capacity = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS;
|
||||
for (int y = y1; y < y2;) {
|
||||
size_t rows = capacity / width;
|
||||
if (rows > (size_t)(y2 - y)) {
|
||||
rows = (size_t)(y2 - y);
|
||||
}
|
||||
memcpy(s_emote_dma_pixels, source, width * rows * sizeof(uint16_t));
|
||||
(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);
|
||||
if (err == ESP_OK && xSemaphoreTake(s_transfer_done_sem,
|
||||
pdMS_TO_TICKS(LCD_DIRECT_TRANSFER_TIMEOUT_MS)) != pdTRUE) {
|
||||
err = ESP_ERR_TIMEOUT;
|
||||
/* 超时后不再复用可能仍被 DMA 读取的缓冲,重启前禁止此路径传输。 */
|
||||
s_emote_transfer_fault = true;
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
s_emote_flush_error = err;
|
||||
ESP_LOGE(TAG, "composed emote transfer failed: %s", esp_err_to_name(err));
|
||||
break;
|
||||
}
|
||||
source += width * rows;
|
||||
y += rows;
|
||||
}
|
||||
(void)gfx_disp_flush_ready(disp, false);
|
||||
}
|
||||
|
||||
static esp_err_t lcd_direct_emote_decode_asset(const lcd_emote_rle_asset_t *asset,
|
||||
@@ -386,6 +405,44 @@ static esp_err_t lcd_direct_emote_prepare_assets(void)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* 一条带覆盖度的抛物线代替被压成碎像素的双层眼圈。无新图片资源。 */
|
||||
static void lcd_direct_emote_draw_closed_eyes(uint8_t *dst, uint16_t width,
|
||||
uint16_t height, uint16_t color)
|
||||
{
|
||||
const size_t count = (size_t)width * height;
|
||||
memset(dst, 0, count * 3);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
int dx = x - (x < 119 ? 49 : 189);
|
||||
if (abs(dx) > 46) {
|
||||
continue;
|
||||
}
|
||||
int y_q8 = 3 * 256 + dx * dx * (8 * 256) / (46 * 46);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
int coverage = 2 * 256 - abs(y * 256 - y_q8);
|
||||
if (coverage <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (coverage > 255) {
|
||||
coverage = 255;
|
||||
}
|
||||
size_t index = (size_t)y * width + x;
|
||||
dst[index * 2] = color >> 8;
|
||||
dst[index * 2 + 1] = color & 0xFF;
|
||||
dst[count * 2 + index] = coverage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t lcd_direct_emote_refresh(void)
|
||||
{
|
||||
if (s_emote_transfer_fault) {
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
s_emote_flush_error = ESP_OK;
|
||||
esp_err_t err = gfx_refr_now(s_emote_handle);
|
||||
return err != ESP_OK ? err : s_emote_flush_error;
|
||||
}
|
||||
|
||||
static esp_err_t lcd_direct_emote_init(void)
|
||||
{
|
||||
ESP_RETURN_ON_ERROR(lcd_direct_emote_prepare_assets(), TAG, "decode emote assets failed");
|
||||
@@ -402,17 +459,20 @@ static esp_err_t lcd_direct_emote_init(void)
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
s_emote_dma_pixels = heap_caps_malloc(LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS *
|
||||
sizeof(uint16_t), MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
|
||||
ESP_RETURN_ON_FALSE(s_emote_dma_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate emote DMA failed");
|
||||
const gfx_disp_config_t display_config = {
|
||||
.h_res = LCD_DIRECT_WIDTH,
|
||||
.v_res = LCD_DIRECT_HEIGHT,
|
||||
.flush_cb = lcd_direct_emote_flush,
|
||||
.flags = {
|
||||
.swap = true,
|
||||
.buff_dma = true,
|
||||
.double_buffer = true,
|
||||
.buff_spiram = true,
|
||||
.double_buffer = false,
|
||||
},
|
||||
.buffers = {
|
||||
.buf_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_BUFFER_ROWS,
|
||||
.buf_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_HEIGHT,
|
||||
},
|
||||
};
|
||||
s_emote_disp = gfx_disp_add(s_emote_handle, &display_config);
|
||||
@@ -443,6 +503,33 @@ static esp_err_t lcd_direct_emote_init(void)
|
||||
ESP_RETURN_ON_ERROR(gfx_mesh_img_get_point_q8(s_emote_eyes_open, i, &s_emote_eye_rest[i]),
|
||||
TAG, "read eye mesh failed");
|
||||
}
|
||||
const size_t closed_count = idle_eyes_open_rle_asset.width * LCD_DIRECT_EMOTE_CLOSED_ROWS;
|
||||
s_emote_closed_pixels = heap_caps_malloc(closed_count * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
ESP_RETURN_ON_FALSE(s_emote_closed_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate closed eyes failed");
|
||||
uint16_t eye_color = 0xFFFF;
|
||||
const size_t eye_count = idle_eyes_open_rle_asset.width * idle_eyes_open_rle_asset.height;
|
||||
for (size_t i = 0; i < eye_count; ++i) {
|
||||
if (s_emote_eye_pixels[eye_count * 2 + i] >= 200) {
|
||||
eye_color = ((uint16_t)s_emote_eye_pixels[i * 2] << 8) | s_emote_eye_pixels[i * 2 + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
lcd_direct_emote_draw_closed_eyes(s_emote_closed_pixels, idle_eyes_open_rle_asset.width,
|
||||
LCD_DIRECT_EMOTE_CLOSED_ROWS, eye_color);
|
||||
s_emote_closed_image = (gfx_image_dsc_t) {
|
||||
.header.cf = GFX_COLOR_FORMAT_RGB565A8,
|
||||
.header.magic = C_ARRAY_HEADER_MAGIC,
|
||||
.header.w = idle_eyes_open_rle_asset.width,
|
||||
.header.h = LCD_DIRECT_EMOTE_CLOSED_ROWS,
|
||||
.data_size = closed_count * 3,
|
||||
.data = s_emote_closed_pixels,
|
||||
};
|
||||
s_emote_closed_eyes = gfx_img_create(s_emote_disp);
|
||||
ESP_RETURN_ON_FALSE(s_emote_closed_eyes != NULL, ESP_ERR_NO_MEM, TAG, "create closed eyes failed");
|
||||
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_closed_eyes, &s_emote_closed_image), TAG, "set closed eyes failed");
|
||||
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_closed_eyes, LCD_DIRECT_EMOTE_EYES_X,
|
||||
LCD_DIRECT_EMOTE_EYES_Y + 39), TAG, "position closed eyes failed");
|
||||
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, false), TAG, "hide closed eyes failed");
|
||||
s_emote_hud_pixels = heap_caps_calloc(LCD_DIRECT_WIDTH * LCD_DIRECT_HUD_ROWS,
|
||||
sizeof(uint16_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
ESP_RETURN_ON_FALSE(s_emote_hud_pixels != NULL, ESP_ERR_NO_MEM, TAG, "allocate HUD failed");
|
||||
@@ -459,9 +546,9 @@ static esp_err_t lcd_direct_emote_init(void)
|
||||
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_hud, &s_emote_hud_image), TAG, "set HUD failed");
|
||||
s_emote_ready = true;
|
||||
ESP_LOGI(TAG,
|
||||
"esp_emote_gfx idle test ready (%u fps, 2 x %u-row DMA buffers)",
|
||||
"esp_emote_gfx idle test ready (%u fps, PSRAM composed frame, %u-row DMA staging)",
|
||||
(unsigned)LCD_DIRECT_EMOTE_FPS,
|
||||
(unsigned)LCD_DIRECT_EMOTE_BUFFER_ROWS);
|
||||
(unsigned)LCD_DIRECT_EMOTE_DMA_ROWS);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -517,8 +604,8 @@ static esp_err_t lcd_direct_emote_update_hud(bool force, bool *changed)
|
||||
|
||||
static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
|
||||
{
|
||||
uint32_t remaining_ms = duration_ms;
|
||||
while (remaining_ms > 0U) {
|
||||
const int64_t deadline_us = esp_timer_get_time() + (int64_t)duration_ms * 1000;
|
||||
while (esp_timer_get_time() < deadline_us) {
|
||||
if (lcd_direct_should_stop_idle()) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
@@ -526,17 +613,23 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
|
||||
bool changed = false;
|
||||
esp_err_t hud_err = lcd_direct_emote_update_hud(false, &changed);
|
||||
if (hud_err == ESP_OK && changed) {
|
||||
hud_err = gfx_refr_now(s_emote_handle);
|
||||
hud_err = lcd_direct_emote_refresh();
|
||||
}
|
||||
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
|
||||
if (hud_err != ESP_OK || unlock_err != ESP_OK) {
|
||||
return hud_err != ESP_OK ? hud_err : unlock_err;
|
||||
}
|
||||
const int64_t remaining_us = deadline_us - esp_timer_get_time();
|
||||
if (remaining_us <= 0) {
|
||||
break;
|
||||
}
|
||||
const uint32_t remaining_ms = (remaining_us + 999) / 1000;
|
||||
uint32_t slice_ms = remaining_ms > LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
|
||||
? LCD_DIRECT_PRESENTATION_WAIT_POLL_MS
|
||||
: remaining_ms;
|
||||
vTaskDelay(pdMS_TO_TICKS(slice_ms));
|
||||
remaining_ms -= slice_ms;
|
||||
TickType_t ticks = pdMS_TO_TICKS(slice_ms);
|
||||
/* 当前 tick 为 10ms;短余量也至少让出一个 tick,避免忙等。 */
|
||||
vTaskDelay(ticks > 0 ? ticks : 1);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -545,6 +638,15 @@ static esp_err_t lcd_direct_emote_wait(uint32_t duration_ms)
|
||||
* closure: 0=睁眼,255=闭眼。Q8 网格保留亚像素精度。 */
|
||||
static esp_err_t lcd_direct_emote_set_eye_closure(uint8_t closure)
|
||||
{
|
||||
bool closed = closure >= LCD_DIRECT_EMOTE_CLOSED_THRESHOLD;
|
||||
if (closed != s_emote_show_closed) {
|
||||
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_eyes_open, !closed), TAG, "toggle mesh eyes failed");
|
||||
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_closed_eyes, closed), TAG, "toggle closed eyes failed");
|
||||
s_emote_show_closed = closed;
|
||||
}
|
||||
if (closed) {
|
||||
return ESP_OK;
|
||||
}
|
||||
gfx_mesh_img_point_q8_t points[LCD_DIRECT_EMOTE_MESH_POINTS];
|
||||
/* 上下眼睑对称收向原眼睛中线,包围盒位置补偿保持脸部锚点。 */
|
||||
const int32_t center_y = 46 * 256;
|
||||
@@ -594,13 +696,13 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
|
||||
}
|
||||
if (result == ESP_OK) {
|
||||
gfx_disp_refresh_all(s_emote_disp);
|
||||
result = gfx_refr_now(s_emote_handle);
|
||||
result = lcd_direct_emote_refresh();
|
||||
}
|
||||
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
|
||||
if (result == ESP_OK) {
|
||||
result = unlock_err;
|
||||
}
|
||||
ESP_LOGI(TAG, "esp_emote_gfx idle playback started (single-mesh blink)");
|
||||
ESP_LOGI(TAG, "esp_emote_gfx idle playback started (composed mesh/arc blink)");
|
||||
|
||||
/* 约 170ms 闭合、34ms 停顿、204ms 睁开,保持单一轮廓。 */
|
||||
static const uint8_t blink_closure[] = {
|
||||
@@ -616,23 +718,40 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
|
||||
if (result != ESP_OK) {
|
||||
break;
|
||||
}
|
||||
uint64_t draw_us_total = 0;
|
||||
uint32_t late_frames = 0;
|
||||
for (size_t i = 0; i < sizeof(blink_closure) / sizeof(blink_closure[0]) && result == ESP_OK; ++i) {
|
||||
const int64_t frame_start_us = esp_timer_get_time();
|
||||
result = gfx_emote_lock(s_emote_handle);
|
||||
if (result != ESP_OK) {
|
||||
break;
|
||||
}
|
||||
result = lcd_direct_emote_set_eye_closure(blink_closure[i]);
|
||||
if (result == ESP_OK) {
|
||||
result = gfx_refr_now(s_emote_handle);
|
||||
result = lcd_direct_emote_refresh();
|
||||
}
|
||||
unlock_err = gfx_emote_unlock(s_emote_handle);
|
||||
if (result == ESP_OK) {
|
||||
result = unlock_err;
|
||||
}
|
||||
if (result == ESP_OK) {
|
||||
result = lcd_direct_emote_wait(LCD_DIRECT_EMOTE_STEP_MS);
|
||||
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 (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);
|
||||
}
|
||||
++cycles;
|
||||
}
|
||||
|
||||
@@ -969,13 +1088,17 @@ static esp_err_t lcd_direct_gpio_init(void)
|
||||
static esp_err_t lcd_direct_panel_init(void)
|
||||
{
|
||||
/* 面板命令、方向和颜色格式在显示开启前一次性配置完成。 */
|
||||
size_t max_transfer_pixels = LCD_DIRECT_PIXELS_PER_TRANSFER;
|
||||
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
|
||||
max_transfer_pixels = LCD_DIRECT_WIDTH * LCD_DIRECT_EMOTE_DMA_ROWS;
|
||||
#endif
|
||||
spi_bus_config_t bus_config = {
|
||||
.mosi_io_num = CONFIG_TQ_SCREEN_SPI_MOSI_GPIO,
|
||||
.miso_io_num = CONFIG_TQ_SCREEN_SPI_MISO_GPIO,
|
||||
.sclk_io_num = CONFIG_TQ_SCREEN_SPI_CLK_GPIO,
|
||||
.quadwp_io_num = -1,
|
||||
.quadhd_io_num = -1,
|
||||
.max_transfer_sz = LCD_DIRECT_PIXELS_PER_TRANSFER * sizeof(uint16_t),
|
||||
.max_transfer_sz = max_transfer_pixels * sizeof(uint16_t),
|
||||
};
|
||||
ESP_RETURN_ON_ERROR(spi_bus_initialize(LCD_DIRECT_SPI_HOST, &bus_config, SPI_DMA_CH_AUTO),
|
||||
TAG,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
- 右上百分比和电池;未知值显示 --%,充电绿色并带闪电缺口,低电量红色。
|
||||
- SOC、有效性和充电状态来自现有 domain battery 服务,不从 ADC 重算。
|
||||
- GFX 顶部 320x40 RGB565 图层,常驻 25,600 B PSRAM;只在分钟或电池状态
|
||||
改变时使图层失效并刷新,眨眼仍只改变眼睛网格,不解码新 PNG。
|
||||
改变时使图层失效并刷新,眨眼主要改变眼睛网格,接近闭合时切换为程序绘制的抗锯齿弧线,不解码新 PNG。
|
||||
- PNG 表情复用相同点阵 / 电池绘制器,在每帧合成时更新;二维码和 LCD
|
||||
边界测试页面不显示状态栏。PNG 无限等待期间不会独立重绘状态栏。
|
||||
- 不需要字体包或新图片。旧 PNG 低电量底部告警保留;旧充电 PNG 不再自动
|
||||
@@ -42,3 +42,23 @@ AddressSanitizer / UndefinedBehaviorSanitizer 检查 808 组有效性、充电
|
||||
4. 动画结束同时到达新请求时不被自动 IDLE 覆盖。
|
||||
|
||||
编译 / 主机测试不等同于上述实机验证。
|
||||
|
||||
## 无 TE 的软件减撕裂试验
|
||||
|
||||
- GFX 使用单个 153,600 B PSRAM 合成缓冲(partition 模式,不是 RGB 全帧模式),
|
||||
+ 普通眼睛更新先完成整个 dirty 区域,再进入 flush;不会在分块传输之间计算网格。
|
||||
+- flush 使用单个 10,240 B 内部 DMA 暂存区;按最多 5,120 像素连续搬运,
|
||||
+ 每批等待 ISR 完成后才复用,整区域发完才通知 GFX。PNG 原有 8 行块不变。
|
||||
+- 相对上一版移除 2×2,560 B DMA 小缓冲,增加 5,120 B DMA 暂存净容量
|
||||
+ (不含 SPI 驱动描述符);新增合成缓冲和 9,996 B 闭眼纹理共 163,596 B PSRAM。
|
||||
+- closure >=232 时隐藏压缩眼圈并显示同色的抗锯齿闭眼弧线,避免极薄纹理抖动。
|
||||
+- 34ms 帧预算包含绘制/传输时间,超时不会再固定额外等 34ms。
|
||||
+- 每轮日志 emote blink composed draw_us_avg=... late_frames=.../12,
|
||||
+ 仅反映绘制预算,不是面板实际扫描帧率。
|
||||
+- DMA 超时会锁住 GFX 传输路径,不复用可能仍被硬件读取的缓冲;需重启恢复。
|
||||
+- 此方案不能保证面板扫描无撕裂。实机比较眨眼上下断层、闭眼亮点及原有
|
||||
+ 表情切换/状态栏行为;无 TE 时不能把软件预合成称为硬件同步双缓冲。
|
||||
|
||||
主机传输/闭眼回归:`python3 tools/test_lcd_emote_transport.py`,检查 320 种
|
||||
区域宽度、DMA 分块/数据顺序、错误/超时处理及闭眼对称性。当前 RTOS tick
|
||||
为 10ms,实际眨眼间隔仍有 tick 量化及任务调度抖动,日志不是实测 FPS。
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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 <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#define LCD_DIRECT_WIDTH 320
|
||||
#define LCD_DIRECT_EMOTE_DMA_ROWS 16
|
||||
#define LCD_DIRECT_TRANSFER_TIMEOUT_MS 100
|
||||
#define portENTER_CRITICAL(x) ((void)0)
|
||||
#define portEXIT_CRITICAL(x) ((void)0)
|
||||
#define pdMS_TO_TICKS(x) (x)
|
||||
#define ESP_LOGE(...) ((void)0)
|
||||
#define ESP_OK 0
|
||||
#define ESP_ERR_INVALID_SIZE 1
|
||||
#define ESP_ERR_TIMEOUT 2
|
||||
#define pdTRUE 1
|
||||
typedef int esp_err_t;
|
||||
typedef int gfx_disp_t;
|
||||
static bool s_emote_active, s_emote_transfer_fault;
|
||||
static int s_emote_flush_error, s_panel, s_transfer_done_sem;
|
||||
static uint16_t dma[5120];
|
||||
static uint16_t *s_emote_dma_pixels=dma;
|
||||
static uint16_t capture[320*240];
|
||||
static int calls,ready,fail_call,timeout_call,origin_y,width;
|
||||
static bool gfx_disp_flush_ready(gfx_disp_t *d,bool swap) {++ready;return true;}
|
||||
static int xSemaphoreTake(int sem,int ticks) {return ticks && calls!=timeout_call;}
|
||||
static int esp_lcd_panel_draw_bitmap(int panel,int x1,int y1,int x2,int y2,const void *data) {
|
||||
++calls; assert(ready==0); assert((x2-x1)*(y2-y1)<=5120); assert(data==dma);
|
||||
if(calls==fail_call) return 3;
|
||||
memcpy(capture+(y1-origin_y)*width,data,(x2-x1)*(y2-y1)*2);
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
test='''int main(void) {
|
||||
uint16_t source[320*240];
|
||||
for(int i=0;i<320*240;++i) source[i]=(uint16_t)i;
|
||||
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;
|
||||
lcd_direct_emote_flush(NULL,0,65,width,158,source);
|
||||
assert(ready==1 && s_emote_flush_error==0);
|
||||
assert(memcmp(source,capture,width*93*2)==0);
|
||||
assert(calls==(93+5120/width-1)/(5120/width));
|
||||
}
|
||||
width=238;calls=ready=0;timeout_call=2;s_emote_flush_error=0;
|
||||
lcd_direct_emote_flush(NULL,41,65,279,158,source);
|
||||
assert(ready==1 && s_emote_transfer_fault && s_emote_flush_error==2 && calls==2);
|
||||
lcd_direct_emote_flush(NULL,41,65,279,158,source);
|
||||
assert(calls==2 && ready==2);
|
||||
s_emote_transfer_fault=false;timeout_call=0;fail_call=1;calls=ready=0;
|
||||
lcd_direct_emote_flush(NULL,41,65,279,158,source);
|
||||
assert(calls==1 && ready==1 && s_emote_flush_error==3);
|
||||
calls=ready=0;s_emote_active=false;
|
||||
lcd_direct_emote_flush(NULL,41,65,279,158,source);assert(calls==0 && ready==1);
|
||||
uint8_t eyes[238*14*3+2];memset(eyes,123,sizeof(eyes));
|
||||
lcd_direct_emote_draw_closed_eyes(eyes+1,238,14,0xffe0);
|
||||
assert(eyes[0]==123 && eyes[sizeof(eyes)-1]==123);
|
||||
const uint8_t *alpha=eyes+1+238*14*2;
|
||||
for(int y=0;y<14;++y) for(int x=3;x<=95;++x)
|
||||
assert(alpha[y*238+x]==alpha[y*238+x+140]);
|
||||
assert(alpha[3*238+49]==255 && alpha[11*238+3]==255);
|
||||
puts("PASS: 320 transfer widths, DMA bounds/order, timeout latch, inactive/error completion and symmetric closed-eye raster");
|
||||
}'''
|
||||
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)
|
||||
Reference in New Issue
Block a user