From c0770ac459534c6a0657b9f78db5a9e0f94f628a Mon Sep 17 00:00:00 2001 From: chenjintao Date: Fri, 9 Oct 2026 17:03:10 +0800 Subject: [PATCH] docs(lcd): add Chinese GFX development guide and agent rules --- AGENTS.md | 77 ++++++++- docs/GFX表情显示应用与开发指南.md | 250 ++++++++++++++++++++++++++++++ docs/lcd-idle-status.zh-CN.md | 95 ++++++++++-- 3 files changed, 407 insertions(+), 15 deletions(-) create mode 100644 docs/GFX表情显示应用与开发指南.md diff --git a/AGENTS.md b/AGENTS.md index 6ef0e76..07c77df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,7 @@ Additional enforcement rules: ## LCD Expression Management -- Use `docs/lcd-expressions.md` as the operational guide for managing, requesting, and adding LCD expressions. +- LCD/GFX 开发先阅读 `docs/GFX表情显示应用与开发指南.md`。旧 `docs/lcd-expressions.md` 的测试图案和优先级仲裁描述不能作为当前实现事实;以现行源码和新指南为准。 - New expression semantics must be declared as `domain_expression_*` contracts in `components/domain/include/domain.h` and mapped in the domain runtime adapter catalog. - Upper layers must request expressions through `domain_runtime_request_expression()`; they must not call `platform_lcd_*` playback APIs directly. - Platform LCD code must expose playback primitives only and must not reference `DOMAIN_EXPRESSION_*` semantic IDs. @@ -219,3 +219,78 @@ Use a clear conventional format: - Never hardcode credentials or API keys in source files; configure through `menuconfig`. - Do not commit local `sdkconfig` variants, serial-port-specific commands, or generated `build/` artifacts. + +## GFX 表情开发约束(给 AI 与开发者) + +本节是在原有架构规则上的补充,不替代前文。处理显示、资产、表情、缓存、 +刷新或帧率任务时,必须先完整阅读 `docs/GFX表情显示应用与开发指南.md`, +再按任务核对当前源码和配置;不得把历史日志的资源开销或 FPS 当成当前事实。 + +### 开始工作前 + +1. 确认仓库为 lucky-cat,检查分支、状态及已有差异。保留未提交代码、素材 + 和预览,不把 tq-cat 的实现或其他目录里的旧文件当成当前工程。 +2. 优先查看 `lcd_direct.c`、领域表情目录、平台公共接口、构建列表和 Kconfig。 + 图层制作还需阅读 `split_idle_expression.py`、`compress_idle_expression.py`。 +3. 当前是待机 GFX 与其他 PNG 混合后端;未接入 esp_emote_expression 状态机, + 也未整体迁移到 LVGL。不要凭依赖存在或请求优先级字段推断功能已经实现。 + +### 必须保持的正确性 + +4. 保持控制层→领域层→平台层依赖方向。业务通过领域语义请求表情,GFX 类型 + 与资产实现留在平台私有代码,不增加跨层直接写 LCD 的入口。 +5. GFX 锁必须由同一个任务获取和释放。批量对象修改、缓存捕获、性能计数 + 快照在渲染锁内完成;不能在初始化任务加锁、到渲染任务解锁。 +6. 不在中断/定时器回调中生成纹理、解码、绘制、等待传输或打印性能日志。 + 保持 SPI 完成中断轻量,渲染任务和 GFX 刷新之间避免并行写屏。 +7. DMA 暂存区必须等传输完成后才复用;整区域发送完成才通知 GFX。 + 超时后的在途缓冲不可立即覆盖或释放,不能绕过现有故障保护。 +8. 自动返回待机只在没有新 pending 且没有停止请求时入队;不得覆盖刚来的 + 表情。取消与成功完成必须区分,修改完成事件前检查所有等待者。 +9. 保留固定时间轴。每轮目标基于共同起点,不回退到画完再固定等 34ms, + 不忙等、不自动跳过眼型,不未经验证修改全局 tick、SPI 时钟或任务优先级。 + +### 资源与缓存 + +10. 生成后的 C 数组不要手改。PNG 是可编辑输入,压缩脚本生成实际编译资产; + 拆分脚本会覆盖图层,运行前检查美术是否有手工修改。 +11. 当前九个缓存是带局部静态背景的不透明 RGB565 结果,不是纯透明眼睛。 + 换背景、鼻子、胡须、配色、眼睛位置或尺寸时,必须重建缓存并核对窗口。 +12. 改时间线必须同步缓存表和回归测试;保持阶段不能产生多余刷新。 + 不支持的 closure 不得静默映射成别的眼型。 +13. 网格锚点、最小边界位置补偿、闭眼弧线与眼睛中心属于耦合几何参数。 + 不得只替换资产就声称已适配;改尺寸需同时检查资源脚本的尺寸断言。 +14. 原图为 RGB565 加独立 A8 平面,缓存为已经合成的 RGB565。验证颜色字节序、 + 透明层及交换配置,避免重复交换。新增区域裁剪必须恢复旧、新区域并集背景。 +15. 新增表情先定义静态/动态层、缓存预算和结束行为,不批量无上限预热所有 + 表情。不得无授权删除未迁移 PNG、历史图层或未跟踪文件。 +16. 缓存失败要释放部分成果并维持安全后备。当前资源随 LCD 常驻,停止循环 + 不等于完整销毁;新增热替换/重复初始化需要显式生命周期、超时和错误测试。 + +### 状态、测量与验收 + +17. 时间沿用现有同步与时区服务;电量来自领域状态,不在显示层用电压重算。 + 保留未知值和二维码/边界测试不叠加规则。当前点阵仅满足数字状态,不能 + 声称已支持任意中文文字。 +18. 性能测量沿用 `CONFIG_TQ_LCD_EMOTE_FPS_LOG`,不逐帧打印。区分步骤频率、 + 实际图像更新、DMA 块次数与面板扫描率;不将 30 FPS 配置写成实测 30 FPS。 +19. 分项都是墙钟时间,含任务抢占;平均包含保持帧,各项最大值可能不在同一帧。 + 不把合成耗时当纯 CPU,不把等待传输当纯线上耗时。 +20. 内存必须分别报告 Flash、PSRAM、内部 RAM 和 DMA。像素缓冲合计不是整个 + 组件总开销;应用镜像已包含嵌入式资源,不重复加算。留意联网/唤醒后的最低余量。 +21. 变更后至少运行三个显示回归脚本和 ESP-IDF 5.5.4 构建。条件编译要验证 + 日志开启/关闭两种情况;涉及资源时检查编码往返及视觉预览。 +22. 实机验收包括待机/表情切换、跨分钟、充电、缓存启动、唤醒与网络并行、 + 取消和超时。区分源码检查、主机测试、编译、设备日志与用户观感证据。 + 没有 TE 同步,不能承诺完全无撕裂;用户满意也不等于已量化稳定帧率。 + +回归命令(仓库根目录): + +```bash +python3 tools/test_lcd_status_hud.py +python3 tools/test_lcd_emote_transport.py +python3 tools/test_lcd_emote_timeline.py +``` + +提交只包含任务相关文件,遵守主题分支与原有提交规则;不要提交临时补丁脚本、 +本机 sdkconfig、构建目录或未授权推送。修改当前机制时同步更新中文指南和测试。 diff --git a/docs/GFX表情显示应用与开发指南.md b/docs/GFX表情显示应用与开发指南.md new file mode 100644 index 0000000..0c148d0 --- /dev/null +++ b/docs/GFX表情显示应用与开发指南.md @@ -0,0 +1,250 @@ +# GFX 表情显示应用与开发指南 + +本文面向本项目的开发者,说明当前 `esp_emote_gfx` 的具体应用及后续开发方法。正文使用中文,代码、路径和日志字段保留原名以便检索。适用基线为 2026 年 10 月 9 日的工作区实现;资源和性能数字随代码变化而变化。 + +## 一、当前方案与边界 + +本项目没有触摸和复杂界面交互,显示需求以精致表情、时间和电量为主。当前方案是轻量图层渲染,不是完整界面框架迁移。 + +- 底层为 ESP32-S3、ST7789P3、单线 SPI,当前像素时钟为 40 MHz,逻辑画布为 320×240。 +- 当前使用 `esp_emote_gfx` 3.0.5,由 `platform` 私有依赖。没有接入 `esp_emote_expression` 状态管理组件,也没有把当前表情绘制建立在 LVGL 上。依赖目录中存在其他库不等于当前显示链路使用它们。 +- 待机基础表情采用 GFX;其他表情仍走原 PNG 或步骤序列,不是所有资产都已经迁移。 +- 屏幕目前未使用 TE 同步。软件预合成和连续传输能改善撕裂观感,但不能保证面板扫描与写入完全同步。 +- 用户已确认缓存与固定时间轴版本的观感满意;最后的固定时间轴版本尚未提供新的量化日志,不能写成“稳定达到 30 FPS”。 + +已安装组件提供图片、网格图片、运动、动画、标签、二维码和按钮等接口。但本项目仅实际使用图片、网格、显示刷新及相关对象控制;通用中文文本、按钮、触摸和复杂布局不属于当前已实现能力。 + +## 二、分层与调用入口 + +```text +控制层决定何时展示表情 + → 领域层请求语义表情并查目录 + → 平台层接收通用播放请求 + → LCD 渲染任务串行播放 + → GFX 合成或 PNG 合成 + → SPI 写入屏幕 +``` + +上层通过领域层请求,不直接调用 GFX 或面板接口。例如返回待机: + +```c +const domain_expression_request_t request = { + .id = DOMAIN_EXPRESSION_IDLE, + .trigger = DOMAIN_EXPRESSION_TRIGGER_UNSPECIFIED, +}; +esp_err_t err = domain_runtime_request_expression(&request); +/* 调用方按自身策略处理 err;不要绕过领域映射直接写 LCD。 */ +``` + +新增语义表情应在 `components/domain/include/domain.h` 声明,并在领域目录中映射;平台层只接收播放原语,不应出现 `DOMAIN_EXPRESSION_*` 语义依赖。GFX 类型不得泄漏到上层公共接口。 + +当前请求采用单个待处理槽,不是完整优先级仲裁器。请求结构虽有优先级字段,也不能据此假定已经实现“低优先级自动排队”。 + +## 三、关键文件与职责 + +| 文件 | 职责 | +| --- | --- | +| `components/platform/src/lcd_direct.c` | 面板初始化、请求播放、GFX 图层、眼型生成和缓存、时间轴、状态栏、传输及性能日志 | +| `components/platform/include/platform.h` | 通用显示请求和电池覆盖层公共契约 | +| `components/platform/src/runtime/platform_lcd_runtime.c` | 平台显示入口转接 | +| `components/domain/src/runtime/domain_runtime_adapters.c` | 语义表情目录、请求映射、电量状态映射 | +| `components/platform/internal/lcd_emote_assets.h` | 私有压缩资产描述与解码声明 | +| `components/platform/src/lcd_emote_assets.c` | 游程压缩解码与输入校验 | +| `components/platform/src/generated/emote_gfx/idle_base.c` | 静态鼻子和胡须压缩数据 | +| `components/platform/src/generated/emote_gfx/idle_eyes_open.c` | 原始睁眼压缩数据 | +| `components/platform/CMakeLists.txt` | 私有组件依赖及实际参与编译的资产列表 | +| `main/Kconfig.projbuild` | GFX 待机开关与测量日志开关 | +| `tools/split_idle_expression.py` | 从旧三帧中拆出静态底图及眼睛图层 | +| `tools/compress_idle_expression.py` | 将底图和睁眼图层转换为压缩 C 资产,并做编码往返检查 | +| `tools/test_lcd_status_hud.py` | 状态栏及自动回待机回归测试 | +| `tools/test_lcd_emote_transport.py` | 离屏捕获、缓存索引、传输、闭眼弧线和统计开关回归测试 | +| `tools/test_lcd_emote_timeline.py` | 固定时间轴、时钟粒度、迟到及取消回归测试 | + +`/tmp` 下修改过程中生成的补丁脚本不是工程运行依赖,不应提交或作为正式资源流程使用。 + +## 四、当前待机表情如何运行 + +### 1. 原始资产只解码一次 + +静态底图和睁眼图层以游程压缩数据进入应用镜像,初始化时解压到 PSRAM。每个游程由四字节组成:数量、RGB565 高字节、低字节、透明度。解压后是 RGB565 色彩平面加独立透明度平面,不是交错的三字节像素。 + +压缩合计为 19,336 字节,解压合计为 97,926 字节。后续眨眼不重新解析 PNG,也不重复解压这些数据。压缩不会在每一帧增加解码成本。 + +### 2. 网格用于首次生成,不是正常播放时反复绘制 + +睁眼图层为 238×93,锚点为 `(41,65)`。网格为 16 列、2 行,共 51 个控制点。闭合程度使用 `closure` 表示,0 为睁眼,255 为闭眼。 + +原网格向中线对称收缩,并增加弧度。GFX 会根据网格边界重新计算局部原点,因此代码补偿最小纵坐标偏移,防止眼睛闭合时整体向上漂移。修改网格后不能随意删除这个补偿。 + +闭合程度达到 232 后,不再把眼圈压成极薄纹理,而是显示程序绘制的抗锯齿弧线。弧线颜色取自原睁眼资源,避免色差。 + +### 3. 离屏生成九个缓存眼型 + +`lcd_direct_emote_prepare_cache()` 在同一任务中持有 GFX 锁,分别生成: + +```text +0、16、24、64、80、128、160、192、232 +``` + +其中 232 及以上共用闭眼图像。GFX 的真实合成输出通过 `lcd_direct_emote_capture()` 捕获到 PSRAM,预热期间不发送到 LCD,也不计播放帧率。 + +缓存窗口是 `(39,63)`,尺寸为 242×97,包含眼睛四周两像素余量。缓存是已经合成的、不透明 RGB565 图像:**其中包含窗口内的静态脸部像素,不是纯透明眼睛层**。修改鼻子、胡须、背景、锚点或配色时,必须同步重新生成缓存,不能拿旧缓存任意叠加到新背景。 + +九张缓存总计:`9×242×97×2 = 422,532` 字节。正常播放使用普通图片对象 `s_emote_cached_eyes` 切换来源。重复闭眼阶段不使对象失效、不再次传输。缓存生成失败时释放已分配的缓存,保留实时网格路径。 + +### 4. 固定时间轴播放 + +当前序列为: + +```text +24 → 80 → 160 → 232 → 255 → 255 → 232 → 192 → 128 → 64 → 16 → 0 +``` + +共十二个步骤、九次不同的眼型更新。正常待机先保持睁眼约两秒,再眨眼。 + +每轮记录共同起点,第 i 步截止时间为 `起点+(i+1)×34ms`。迟到时减少下一步等待,不把误差继续加到后面的目标时间。不跳眼型,不使用忙等,不修改全局系统时钟粒度。 + +十二步目标总长为 408ms,对应约 29.41 步/秒;九次更新除以完整眨眼时长对应约 22.06 次/秒。闭眼保持也占时间,二者都不是面板扫描率。实际仍受系统 10ms 时钟粒度及后台调度影响。 + +### 5. 预合成与连续传输 + +GFX 使用一个 153,600 字节 PSRAM 合成缓冲。虽容量为整屏,当前仍是分区缓冲模式,不是 RGB 显示的硬件换帧模式。 + +普通更新区域先合成完成,再进入 `lcd_direct_emote_flush()`。它将像素复制到一个 10,240 字节内部 DMA 暂存区,每批最多 5,120 像素。每批等中断通知传输完成后才复用暂存区;整个区域发送完再通知 GFX。 + +当前缓存图像全窗口更新时,242×97 像素通常需五批传输。这不是一次原子换帧。SPI 完成中断只释放信号量,不计算眼型、不解码、不打印日志。 + +DMA 等待超时后禁止继续复用可能仍被硬件读取的缓冲,GFX 传输路径需重启恢复。不能为了“继续显示”而清除故障标志并立即覆盖缓冲。 + +## 五、时间、电量及返回待机 + +左上显示系统本地时间,尚未同步时显示 `--:--`。当前配置为中国标准时间;时区应通过现有时间服务配置,不在显示模块硬编码。 + +右上显示领域层提供的电量百分比、有效性及充电状态。未知时显示 `--%`,充电显示绿色电池及闪电缺口,低电量显示红色。显示层不得从 ADC 电压自行重算电量。 + +状态栏复用内置 5×7 点阵数字和程序绘制图标,无需额外字体包。GFX 状态栏为 320×40 RGB565 图层,只在内容变化时刷新。PNG 表情在每帧合成时更新状态;二维码和屏幕边界测试不叠加它。 + +当前不能据此声称已实现任意中文文字。后续中文提示需要选择字体、字符子集、布局及字形缓存策略;数字时间和电量不需要引入完整中文字库或大型界面框架。 + +开机空闲、显式待机请求、有限动画结束、资源缺失及非取消播放错误都应进入统一待机后端。`lcd_direct_queue_idle_if_available()` 在状态锁内检查新请求和停止状态,不得改回无条件覆盖待处理槽。 + +历史 `jichubiaoqing` 请求在 `duration_ms=0` 且 GFX 就绪时也转到新路径,包括服务器错误的有限播放。只有 GFX 禁用或初始化失败才使用旧三帧后备。其他表情无需在这次迁移中全部替换。 + +## 六、资源目录、制作和替换 + +```text +components/platform/assets/ + lcd_source/ 旧 PNG 源图,仍保留 + lcd_layers/jichubiaoqing/ 可编辑拆分图层、布局及预览 +components/platform/src/generated/emote_gfx/ + idle_base.c 实际编译的底图压缩数据 + idle_eyes_open.c 实际编译的睁眼压缩数据 +``` + +半闭眼、闭眼源图及对应历史生成文件不代表当前参与编译;以构建列表为准,不把目录里的全部文件自动加入固件,也不擅自删除旧资源。 + +在仓库根目录,确认 Python 环境有 Pillow 后,可执行: + +```bash +python3 tools/split_idle_expression.py +python3 tools/compress_idle_expression.py +``` + +第一条会从旧三帧重新生成拆分图层和预览,**会覆盖该目录中手工改过的图层**。仅替换已经拆好的图层时,通常只运行第二条。生成前先查看差异,保留美术编辑成果。 + +第二条会重新生成底图和睁眼的 C 压缩资产,并校验编码往返。当前脚本明确要求睁眼尺寸为 238×93,眼睛锚点固定为 `(41,65)`;它不是能适配所有角色的通用生成器。生成后的 C 数组不应手改。 + +替换资源时需要核对: + +1. 原画布、裁剪尺寸、透明层和锚点是否一致。 +2. 两只眼睛中心、闭合中线、弧度和闭眼宽度是否仍适用;这些目前在代码中有专用参数。 +3. 新动作是否超出缓存窗口,是否会盖住时间、电量或静态脸部。 +4. 动作所有阶段是否在缓存表中;新增阶段不能静默复用错误眼型。 +5. 编码格式、颜色高低字节、透明度平面及显示字节交换是否一致,避免重复交换导致颜色错误。 +6. PNG 资产打包与 GFX 生成资产是两条不同管线;相关步骤参考现有素材指南。不能把图层 PNG 放进目录就认为 GFX 已经接入。 + +## 七、资源开销与取舍 + +| 当前常驻对象 | 明确像素缓冲占用 | +| --- | ---: | +| 解压后的底图与睁眼图层 | 97,926 字节 PSRAM | +| 程序闭眼透明纹理 | 9,996 字节 PSRAM | +| 时间电量图层 | 25,600 字节 PSRAM | +| GFX 合成缓冲 | 153,600 字节 PSRAM | +| 九个眼型缓存 | 422,532 字节 PSRAM | +| 上述合计 | 709,654 字节 PSRAM | +| GFX DMA 暂存区 | 10,240 字节内部 DMA 内存 | + +此表不含对象管理、任务栈、SPI 描述符、PNG 缓存和系统其他模块。不能把表中合计当作整个显示组件的完整运行内存开销。 + +压缩源图像像素数据为 19,336 字节,不含描述信息和组件代码。运行时眼型缓存不写入 Flash。最新本地构建应用镜像为 3,040,480 字节,OTA 槽为 4,718,592 字节;这是当次构建快照,不是后续版本承诺。 + +实机日志曾显示联网、唤醒同时运行时,内部内存最低只剩约十 KiB,而 PSRAM 尚有数 MiB。后续优先考虑 PSRAM 和局部复用,不能因为外部内存充足就不断扩大 DMA 或内部任务栈。 + +## 八、性能日志如何读 + +通过 `menuconfig` 搜索 `TQ_LCD_EMOTE_GFX_TEST` 和 `TQ_LCD_EMOTE_FPS_LOG`。前者控制 GFX 待机路径;后者控制性能统计,默认关闭,当前本地测量配置开启。关闭统计不应改变动画节奏。 + +每轮完整眨眼输出一次 `emote_fps` 和 `emote_cost`,不在逐帧传输中打印: + +- `blink_us`:真实眨眼总耗时,不含此前睁眼停留和该条日志输出。 +- `step_fps`:步骤数除以眨眼秒数;保持步骤也计入。 +- `eye_updates`:眼型步骤中确实完成至少一个区域传输的次数;多个 DMA 块不会重复计数。 +- `update_fps`:实际更新次数除以同一眨眼时长,不是仅运动段的瞬时频率。 +- `draw_avg_us`、`draw_max_us`:步骤绘制时间;平均包含没有传输的保持步骤。 +- `late_steps`:绘制达到或超过 34ms 的步骤数,不等于屏幕丢帧数,也不统计所有调度迟到。 +- `lock`:等待 GFX 锁;`compose`:眼型修改及合成、刷新处理,扣除复制和传输;`copy`:复制到 DMA;`spi`:提交并等待传输完成。 + +各项均为墙钟时间,可能包含被其他任务抢占。各项最大值可能来自不同步骤,不能相加。不能仅凭合成时间大就断言算法慢,也不能仅凭 SPI 等待大就断言线路带宽不足。 + +缓存前低负载合成平均约 19ms,缓存后约 1.4~1.8ms;缓存后前期更新吞吐率约 18.7~19.2 次/秒,后台任务运行后多数约 17 次/秒。这些是**固定时间轴修改之前**的设备样本,不应拿来宣称当前版本最终实测帧率。 + +## 九、后续开发建议 + +### 接入更多表情 + +先逐个迁移,保留未迁移 PNG 路径。每个表情准备静态层、动态图层、锚点、时间线、缓存预算及结束行为。固定短动作适合预生成缓存;大量不同动作或连续交互需要考虑按需生成、共享缓存和生命周期,不应给每个表情无上限常驻一份整屏缓存。 + +建议把几何参数和时间线整理为平台私有描述结构,避免每个表情复制一套线程、锁和 SPI 逻辑。表情语义仍由领域目录管理,不要往平台层添加服务器、联网或业务状态判断。 + +### 后续状态控制 + +当前不是完整状态机。建议控制层决定待机、监听、响应、联网、更新等状态何时进入和退出;领域层管理语义表情及触发原因;平台层执行播放、取消、完成及安全恢复。 + +改造前先定义取消与完成的区别、临时表情结束后恢复什么、并发请求优先级及超时语义。不能把停止或被抢占也记为成功完成,不能用“统一返回待机”覆盖正在执行的新状态。若需要恢复上一业务状态,而不是一律待机,应由控制层明确决定。 + +### 缩小缓存传输区域 + +当前每个缓存眼型传输固定窗口。下一阶段可按眼型裁剪,但必须刷新旧、新区域并集并恢复背景。由于缓存包含静态像素,不能只缩小新图片后留下旧眼圈。先验证残影、边界和状态栏遮挡,再比较吞吐率。 + +### 生命周期与解耦 + +缓存和 GFX 对象当前随 LCD 实例常驻,停止渲染循环不等于完整释放 GFX 引擎、图层和缓存。后续若支持重复初始化、热换资源或停机释放,需要明确停止、等待传输、注销对象、停止引擎和释放内存的顺序,补充错误注入测试。不要宣称当前已经具备完整热替换和销毁机制。 + +源码中的锁与显示完成等待来自现有组件实现。后续新增生命周期接口仍需有明确超时,不能把内部阻塞接口包装成没有边界的公开停止接口。 + +## 十、验证与交付 + +在仓库根目录执行: + +```bash +python3 tools/test_lcd_status_hud.py +python3 tools/test_lcd_emote_transport.py +python3 tools/test_lcd_emote_timeline.py +``` + +这些脚本在电脑上从实际源码提取 C 函数,使用 Clang 与内存、未定义行为检查器。需要 Python 和 Clang;不是固件代码,不占设备 Flash 或 RAM。测试使用模拟接口,不能证明屏幕无撕裂、实时帧率或设备像素完全一致。 + +按根目录 `AGENTS.md` 激活 ESP-IDF 5.5.4,再执行 `idf.py build`。修改日志条件编译时还要检查开启与关闭两种配置,不能只验证当前开启的配置。 + +实机至少验证: + +1. 缓存成功日志、首次生成时间及回退路径。 +2. 固定时间轴版本的帧率与分项耗时,低负载和唤醒、网络同时运行各采样。 +3. 向中间闭眼,无上移、残影、明显颜色差异或缓存窗口截断。 +4. 各表情切换后正确恢复待机,新请求不被旧动画结束覆盖。 +5. 跨分钟、首次时间同步、电量变化、充电插拔和二维码页面。 +6. 语音采集、唤醒、网络无明显退化;内部、DMA 和 PSRAM 的最低余量及分配失败。 +7. 新增停止或资源释放功能时的取消、超时和反复启动测试。 + +交付时分别报告源码检查、主机测试、编译和实机证据。不能把“编译通过”写成“设备已验证”,也不能只用目标帧率或一次录像承诺持续性能。 diff --git a/docs/lcd-idle-status.zh-CN.md b/docs/lcd-idle-status.zh-CN.md index cd46e11..5755ced 100644 --- a/docs/lcd-idle-status.zh-CN.md +++ b/docs/lcd-idle-status.zh-CN.md @@ -1,7 +1,8 @@ # 待机恢复与时间 / 电量状态栏 -当前实现(2026-10-09)以本文为准,旧 lcd-expressions.md 中的测试图案、 -优先级仲裁描述不代表当前运行行为。 +完整现行说明见 [GFX 表情显示应用与开发指南](GFX表情显示应用与开发指南.md)。 +本文保留待机、状态栏和历次优化记录;旧 lcd-expressions.md 中的测试图案、 +优先级仲裁描述不代表当前运行行为。正常播放现在是缓存图像,不是逐帧网格。 ## 返回待机 @@ -46,19 +47,85 @@ AddressSanitizer / UndefinedBehaviorSanitizer 检查 808 组有效性、充电 ## 无 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 时不能把软件预合成称为硬件同步双缓冲。 + 普通眼睛更新先完成整个 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_fps,区分时序步骤与眼睛实际更新。 +- DMA 超时会锁住 GFX 传输路径,不复用可能仍被硬件读取的缓冲;需重启恢复。 +- 此方案不能保证面板扫描无撕裂。实机比较眨眼上下断层、闭眼亮点及原有 + 表情切换/状态栏行为;无 TE 时不能把软件预合成称为硬件同步双缓冲。 主机传输/闭眼回归:`python3 tools/test_lcd_emote_transport.py`,检查 320 种 区域宽度、DMA 分块/数据顺序、错误/超时处理及闭眼对称性。当前 RTOS tick 为 10ms,实际眨眼间隔仍有 tick 量化及任务调度抖动,日志不是实测 FPS。 + +## 可开关的实测帧率日志 + +menuconfig 的屏幕配置中 `Log measured GFX blink FPS` 对应 +`CONFIG_TQ_LCD_EMOTE_FPS_LOG`。默认关闭,本次本地测试配置开启。关闭后 +统计计数和旧 draw_us_avg 日志均不编译,不改变动画/等待节奏。 + +每轮完整眨眼打印一次 `emote_fps`,被抢占/取消的轮次不打印: + +- blink_us:实际整轮眨眼墙钟耗时,包含计算、SPI、等待和任务调度, + 不包含此前约 2 秒的静止睁眼时间,也不包含该条日志输出时间。 +- step_fps:12 个时序步骤 / 实测眨眼秒数,闭眼保持也算步骤。 +- eye_updates:步骤的刷新调用确实完成至少一个区域传输才计一次; + 一帧多个区域/多个 DMA 块不会重复计数,保持帧和等待期间的 HUD 更新不计。 +- update_fps:eye_updates / 相同的 blink_us,是本轮眼睛画面更新吞吐率, + 不是面板扫描率,也不是仅统计运动段的瞬时 FPS。 +- draw_avg_us / draw_max_us:步骤绘制及锁等待耗时,平均包含无需传输的保持步骤。 +- late_steps:绘制耗时达到或超过 34ms 帧预算的步骤数。 + +示例:blink_us=570000 steps=12,则 step_fps=21.05;如果 eye_updates=9, +update_fps=15.78。没有 TE,这些指标不测量屏幕实际扫描或保证无撕裂。 + +### 分项耗时 emote_cost + +同一开关下,每轮 emote_fps 后追加一条 emote_cost,各项均输出 avg_us / max_us。 +平均按 12 个步骤计算,包含不传输的闭眼保持步骤;各项最大值可能来自不同步骤, +不能相加。累计计数和快照都在 GFX 渲染锁内,不计静止/HUD 更新的耗时。 + +- lock:调用 gfx_emote_lock 到获取锁的墙钟耗时。 +- compose:获取锁后眼型变形/切换、GFX 合成和刷新调度的墙钟耗时, + 扣除 copy 与 SPI 时间;包含 GFX/回调记账开销,不等于纯 CPU 使用时间。 +- copy:PSRAM 合成区域复制到 DMA 暂存区的墙钟耗时。 +- spi:清理完成信号、提交 LCD 窗口/像素传输并等待 DMA 完成的墙钟耗时。 + 包含调度等待,不是仅由 SPI 时钟决定的线上传输时间。 + +全部计时都可能包含任务被抢占时间。若 compose 大,不能仅凭该值认定网格算法 +慢;若 spi 大,也不能仅凭该值认定 SPI 带宽不足。关闭统计仍不改变播放逻辑。 +本次不自动关闭本地唤醒或网络任务;对照测试要保持其他条件一致。 + +## 眼型 PSRAM 缓存 + +初始化时用原网格/闭眼弧线离屏合成 9 个眼型,捕获 x=39,y=63 的 242x97 +RGB565 区域(含该区域内的静态脸部像素)。总缓存 422,532 B PSRAM;不新增 +Flash 图片或 DMA 缓冲。日志 `Eye cache ready frames=9 PSRAM=422532B prepare_us=...` +报告实际预热时间,播放日志 cached=1 表示正在使用缓存。 + +之后每步切换普通 GFX image,不重新计算网格,闭眼保持不刷新。帧率/耗时 +日志继续工作。窗口含原眼睛四周 2px 余量,固定位置的鼻子/胡须保持原效果; +以后修改眼睛动作/锚点、静态脸部或亮度配色时必须重新生成缓存或调整窗口。 +新增动作阶段必须同时加入缓存表,不能把不支持的阶段静默映射到错误眼型。 + +预热阶段不传输到 LCD、不计实际更新次数。缓存分配/生成失败时释放已生成 +缓存并保留实时网格路径。实机需比较 cached=1、合成耗时、FPS、首次启动 +延时,以及其他表情/时间电量/唤醒同时工作时的表现。编译及主机窗口捕获 +测试不证明缓存与网格在实机上像素一致,也不证明已经达到 30 FPS。 + +## 固定时间轴眨眼试验 + +每轮在睁眼停留结束后记录起点,第 i 个步骤的截止时间固定为 +`blink_start_us + (i+1)*34000`,而不是当前时间再加剩余帧间隔。 +10ms tick 带来的晚唤醒会压缩下一次等待,不持续累积。已迟到时不额外 +等待、不跳过眼型;每一步仍检查停止/新请求,传输等待语义不变。 + +12 步目标总长 408ms,约 29.41 步/秒;9 次画面更新除以完整眨眼时长, +其目标吞吐率约 22.06 次/秒,包含闭眼保持,不是面板扫描率。 +实际可能受 tick 和后台调度影响,需要实机日志验证,不能保证始终达标。 +未提高全局 tick 频率、任务优先级或 SPI 时钟,也未增加缓冲。