feat: arm2d-skill directory seed for CaseRun agent workspace
Add B1 directory-type seed mechanism to AGENT_WORKSPACE_SEED_FILES: - Extended agentWorkspaceFilesForRun with collectDirectorySeedFiles helper - arm2d-skill (SKILL.md + references/ + python/) auto-injected to .agents/skills/arm2d-skill/ - Enables Code Agent to follow ARM-2D constraints and call asset scripts in case Skill source: https://github.com/notLabyet/HWLabOA
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# HWLAB MVP E2E 验收测试与报告 issue 规则
|
||||
|
||||
> **门禁最小化见 AGENTS.md P0**: Critical Guard Minimalism Rule、Critical Latest-Requirement Test Rule。以下用例为当前 MVP 验收清单(一次性验证参考,非长期保留门禁)。旧用例阻碍最新交付时删除,新增用例只覆盖明确高价值风险。
|
||||
|
||||
本文定义 HWLAB MVP 的用户可读、Agent 可执行验收测试规格。它按 `cli-spec` 的 `TEST.md` 风格组织用例,但报告产物不写入 `docs/` 或 `repository report directory `;每轮验收必须创建一个带编号的 GitHub 测试报告 issue。
|
||||
|
||||
## 适用范围
|
||||
|
||||
@@ -194,9 +194,13 @@ SOURCE/static layout smoke;默认不访问公网 DEV,不部署、不重启
|
||||
M3 IO。
|
||||
|
||||
默认 `web:check` 只把当前工作台主路径、Code Agent/trace/quick-prompt、布局命中和
|
||||
安全边界作为阻断门禁。历史 auth fixture、演示 fixture 或与当前改动无关的浏览器
|
||||
安全边界作为阻断检查。历史 auth fixture、演示 fixture 或与当前改动无关的浏览器
|
||||
> **门禁最小化见 AGENTS.md P0** — 以下检查为一次性验证入口,非长期保留门禁。
|
||||
|
||||
默认 `web:check` 只把当前工作台主路径、Code Agent/trace/quick-prompt、布局命中和
|
||||
安全边界作为阻断检查。历史 auth fixture、演示 fixture 或与当前改动无关的浏览器
|
||||
journey 可以继续产出诊断报告,但不得因为 Playwright `scrollIntoView` 与固定命令栏、
|
||||
顶部状态摘要等布局细节耦合而阻塞 G14 CI/CD。需要恢复为阻断门禁时,必须先证明该
|
||||
顶部状态摘要等布局细节耦合而阻塞 G14 CI/CD。需要恢复为阻断检查时,必须先证明该
|
||||
fixture 代表真实用户入口问题,而不是旧检查假设。
|
||||
|
||||
The static smoke checks both sides of the route contract: `/` remains the
|
||||
|
||||
@@ -130,14 +130,12 @@ Forwarder 不需要人工维护长驻进程。正式固化后,它应由 Kubern
|
||||
`hwlab-cloud-api` 启动,Pod 删除时一起退出,异常退出由 kubelet 按 Pod/容器 restart policy 重启。人工只允许在
|
||||
临时 smoke Pod 中手动启动 forwarder 做分层诊断;运行态不应依赖手动 `kubectl exec` 后台进程。
|
||||
|
||||
自动化门禁只需要做合同级检查,不需要读取 Secret 或调用外部模型:
|
||||
自动化合同验证(一次性检查,非长期门禁。门禁最小化见 AGENTS.md P0):
|
||||
|
||||
- desired state 中 `codex-api` profile base URL 指向 Pod-local loopback,不指向 D601 IP 或
|
||||
`api.openai.com`。
|
||||
- forwarder 的 upstream base URL 由 env 配置,默认 host 为 `hyueapi.com`,并且 `NO_PROXY/no_proxy`
|
||||
包含 `hyueapi.com` 与 `.hyueapi.com`。
|
||||
- `hwlab-cloud-api` Pod 模板包含 forwarder sidecar 或明确等价的同 Pod 受控进程;forwarder 不暴露
|
||||
Service、NodePort、Ingress 或 hostPort。
|
||||
|
||||
- `codex-api` profile base URL 指向 Pod-local loopback,不指向 D601 IP 或 `api.openai.com`。
|
||||
- forwarder upstream base URL 默认 `hyueapi.com`,`NO_PROXY/no_proxy` 包含 `hyueapi.com` / `.hyueapi.com`。
|
||||
- `hwlab-cloud-api` Pod 包含 forwarder sidecar,forwarder 不暴露 Service/NodePort/Ingress/hostPort。
|
||||
- `deepseek` profile 仍指向 DeepSeek bridge/Moon Bridge;`codex-api` profile 不依赖 DeepSeek bridge。
|
||||
|
||||
完整 CI/CD、GitOps render 或发布流水线只能在目标 Pod 最小闭环已经通过、且当前 CI/CD 基础设施可用时继续。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# HWPOD Harness 规格
|
||||
|
||||
> **hwpod-node 运维操作见 `hwpod-ops` skill**(`~/.agents/skills/hwpod-ops/SKILL.md`),包含 G14 serve 模式、D601 Windows connect 模式启停、cloud-api 注册状态检查、源码同步、日志路径和故障排查。
|
||||
|
||||
本文是 HWLAB `v0.2` 的 HWPOD Harness 长期规格。概念体系和实施跟踪见 [pikasTech/HWLAB#897](https://github.com/pikasTech/HWLAB/issues/897)。当前实现只打通单线程核心业务闭环,不把鉴权、安全、并发、计量、复杂调度或 Evidence 体系作为前置条件。
|
||||
|
||||
本规格不保留旧设备执行路径作为迁移对照;新任务只按 HWPOD 单通道实现和验收。
|
||||
@@ -270,16 +272,7 @@ CaseRun 是强化学习 Harness 的最小执行单元,但它本身不是完整
|
||||
|
||||
最小入口:
|
||||
|
||||
```bash
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case prepare d601-f103-v2-compile --case-repo /root/hwlab-case-registry
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case build d601-f103-v2-compile --case-repo /root/hwlab-case-registry --run-dir .state/hwlab-cli/caserun/<runId>
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case collect d601-f103-v2-compile --case-repo /root/hwlab-case-registry --run-dir .state/hwlab-cli/caserun/<runId>
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case run d601-f103-v2-compile --case-repo /root/hwlab-case-registry
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case run start d601-f103-v2-compile --case-repo /root/hwlab-case-registry --run-id <runId>
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case run status <runId>
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case run result <runId>
|
||||
bun tools/hwlab-cli/bin/hwlab-cli.ts case run logs <runId> --tail 8000
|
||||
```
|
||||
用法详见 `hwlab-caserun` skill,包含同步运行、异步 start/status/result/logs、分步 prepare/build/collect 和 provider profile 选择等完整用法。
|
||||
|
||||
`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-<runId>` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 记录 agent identity、terminal result 里已有的终态字段、trace lookup hint、subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再按 case 定义执行 runner 后置 `hwpod-cli build` 和 `cmd.run` Keil 异步 job-status 查询,最后把原始证据和可读归档写入 case registry repo 的 `runs/<caseId>/<runId>/`。固定归档包括 `evidence.json`、`summary.md`、`run.json`、`result.json`、`agent-prompt.md`、`agent-diff.patch`、run-local `.hwlab/hwpod-spec.yaml`、worker stdout/stderr、`artifact-manifest.json`,以及 `agent-messages.json`、`agent-trace.md`、`agent-transcript.md` 和 `final-response.md`。`agent-trace.md` / `agent-transcript.md` 必须包含 trace lookup hint、renderer 标识和 subject diff 区块;如果归档了完整 rendered rows,其语义必须来自共享 Web/CLI trace renderer,不能由 CaseRun 自行发明。`final-response.md` 在 terminal result 存在权威 finalResponse 时保存正文,不存在时显式记录 `finalResponse=null`、terminal/error rows 和缺失原因。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- `hwlab-gateway` 只保留为底层 transport 诊断和历史硬件通道,不作为 HWPOD 产品主概念。
|
||||
- `hwlab-agent-skills` 是 skills bundle 的 health/list 可见服务,不承载每次 Code Agent 对话的运行时依赖。
|
||||
- `hwpod-node` 不部署为 G14 runtime Service;它运行在 PC host、AI 网关、调试器边缘设备或实验室控制盒上,通过 `hwpod-node-ops` 接收基础操作。
|
||||
- `hwpod-node` 的启停、注册状态检查和运维操作见 `hwpod-ops` skill。
|
||||
- `hwpod-cli`、`hwpod-ctl` 和 `hwpod-compiler-cli` 是 Code Agent workspace 内短连接工具,不进入 runtime service inventory、artifact catalog 或 GitOps desired state。
|
||||
- `hwlab-agent-mgr`、`hwlab-agent-worker`、repo-owned codex-stdio supervisor、router、tunnel、模拟器和旧硬件执行服务不在 v0.2 runtime service matrix 中,不再生成 Deployment、Job template、Service、artifact 或 GitOps desired state。
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
name: arm2d-skyer
|
||||
description: 用于 ARM-2D 嵌入式 UI 开发工作:scene/PFB/generic loader/dirty region/RGB565 资源、GIF 精灵图与动画资源链路、Keil MDK/RTE/显示适配器集成、SRAM/Flash/帧率权衡,以及在 MCU 上实现伪 shader/demo-scene 特效。
|
||||
---
|
||||
|
||||
# ARM-2D 嵌入式 UI 工作流
|
||||
|
||||
这是一个通用 ARM-2D 嵌入式 UI 技能。核心目标不是写通用教程,而是让助手按可移植、可维护、贴合硬件约束的方式处理 ARM-2D 工程。
|
||||
|
||||
## 默认背景
|
||||
|
||||
- 常用语言:C。
|
||||
- 常用图形栈:ARM-2D、PFB、Scene Player、Generic Loader。
|
||||
- 常见工程环境:Keil MDK、CMSIS-Pack/RTE、RTOS 或裸机工程。
|
||||
- 常见显示链路:LCD/OLED/RGB/MIPI/Framebuffer、SPI/QSPI/8080/LTDC 等显示接口。
|
||||
- 常见像素格式:RGB565、RGB888、ARGB8888。
|
||||
- 常见任务:动态 UI、GIF/动画素材、局部刷新、低 RAM 渲染、伪 shader 特效。
|
||||
|
||||
## 总原则
|
||||
|
||||
- 优先写能在目标 MCU 上稳定跑起来的代码,不追求理论优雅。
|
||||
- 每次涉及动画/特效/大图资源时,都要主动关注:
|
||||
- SRAM 占用
|
||||
- Flash 占用
|
||||
- 每帧 CPU 开销
|
||||
- 是否阻塞 Arm-2D/PFB 刷屏
|
||||
- 目标硬件是否有 FPU,以及当前编译配置是否启用了正确的浮点 ABI/FPU 选项
|
||||
- 对非浮点数值处理和像素热路径,不要默认套用某一个芯片的经验;应先查阅工程使用的 MCU/SoC 是否具备硬件浮点运算单元、DSP/SIMD/2D 加速器、DMA/display 加速能力,再决定定点、查表、整数近似或浮点实现。
|
||||
- Keil 编译通过不代表运行时安全;Arm-2D scratch/PFB/generic loader 仍可能申请不到内存。
|
||||
- 不要轻易建议未压缩整帧数组作为最终方案,除非 Flash 明确足够。
|
||||
- 对双核、多核、FPU、DSP/SIMD、2D DMA/GPU 等硬件能力,默认视为“需要按工程核实后再启用”;不要把某个芯片平台的优化习惯直接套到其它工程。
|
||||
- 对已有工程,先检查再改,不要盲目替换用户已有 scene/loader。
|
||||
- 当用户提出 UI 相关需求时,避免从 0 搭建自绘框架或凭空造 API;优先在 Arm-2D 库、当前工程、Pack 示例和已有 demo/scene 中检索可用 API、helper、控件/场景写法,再基于已有能力组合实现。
|
||||
- Arm-2D 库/工具链中包含用于生成字体、蒙版、图片数组、GIF/动画数组等资源的 Python 脚本;当用户提出素材接入或资源转换需求时,可以直接调用这些脚本生成输出,并把结果加入工程,但要先确认脚本参数、输出格式和目标屏幕分辨率。
|
||||
|
||||
## 标准处理流程
|
||||
|
||||
遇到 ARM-2D 工程任务时:
|
||||
|
||||
1. 先检查工程结构和目标硬件:
|
||||
- MCU/SoC 型号、内核类型、是否有 FPU/DSP/SIMD/2D 加速器
|
||||
- Keil/CMake/Makefile 等编译配置里的 FPU、float ABI、优化等级和 LTO 设置
|
||||
- 屏幕分辨率、像素格式、显示接口、DMA/display flush 路径
|
||||
- `main.c`
|
||||
- `project/mdk/template.uvprojx`
|
||||
- `project/mdk/RTE/Acceleration/arm_2d_scene_*.c`
|
||||
- 已有 `user_generic_loader_*`
|
||||
- display adapter / platform / ST7789 相关文件
|
||||
2. 判断任务类型:
|
||||
- UI 布局/控件/动效需求
|
||||
- 静态图片资源
|
||||
- GIF/动画资源
|
||||
- 动态渲染/generic loader
|
||||
- 伪 shader/特效
|
||||
- 性能/内存调优
|
||||
3. 如果任务涉及 UI、控件、scene、动效或绘制 API,先检索 Arm-2D 库/Pack 示例/当前工程已有实现:
|
||||
- 优先搜索 `arm_2d_helper.h`、`arm_2d_scene_*.c`、官方 demo、当前工程 scene 和 display adapter。
|
||||
- API 的具体使用方式优先参考 Arm-2D 官方 demo、Pack 示例和当前工程已有调用方式,不要只根据函数名猜参数、生命周期或返回值语义。
|
||||
- 优先复用 Arm-2D 已有 tile、region、opacity、mask、transform、dirty region、scene player、helper API。
|
||||
- 不要一上来从 0 写完整 UI 框架、控件系统或软件渲染层,除非确认 Arm-2D 现有能力不覆盖需求。
|
||||
4. 尽量新增独立模块,不直接破坏已有可运行代码。
|
||||
5. 如果新增 `.c/.h`,同步更新 Keil 工程文件。
|
||||
6. 能构建就构建,并报告:
|
||||
- 修改了哪些文件
|
||||
- Code / RO-data / RW-data / ZI-data 变化
|
||||
- 错误和警告
|
||||
- 运行时风险
|
||||
|
||||
## 常用参考
|
||||
|
||||
根据任务按需读取以下文件:
|
||||
|
||||
- `references/generic-loader.md`:ARM-2D generic loader 模式、ROI decode、scene 接入。
|
||||
- `references/asset-pipeline.md`:GIF、sprite、RGB565、mask、zhRGB565 等素材链路。
|
||||
- `references/immigrant.md`:Arm-2D 移植、Pack/RTE 部署、display adapter、PFB、dirty region、RTOS Helper 和编译下载运行验证。
|
||||
- `references/rp2040-performance.md`:RP2040/Tufty2040 这类无 FPU、资源受限 MCU 上的性能和内存经验;仅在目标工程确实接近该平台约束时参考,不要泛化到所有 MCU。
|
||||
- `references/keil-project.md`:Keil MDK 工程文件修改注意事项。
|
||||
|
||||
## UI 需求处理原则
|
||||
|
||||
当用户提出 UI 页面、控件、动效、转场、仪表盘、菜单、进度条、按钮、列表、弹窗、图标叠加等需求时:
|
||||
|
||||
1. 先查现有能力,不要从 0 搭建。
|
||||
2. 优先在这些位置检索可用 API 和示例:
|
||||
- 当前工程已有 `arm_2d_scene_*.c/.h`
|
||||
- 当前工程已有 `user_scene_*`、`user_*_view`、`display adapter` 文件
|
||||
- Arm-2D Pack 里的 helper、scene player、官方 demo
|
||||
- `arm_2d.h`、`arm_2d_helper.h`、`arm_2d_types.h` 等头文件
|
||||
3. API 的使用方式优先参考相关 demo 和已有工程代码:
|
||||
- 先看官方 demo / Pack 示例如何初始化、调用、等待异步完成、释放资源。
|
||||
- 再看当前工程是否已有同类 API 调用,保持风格和生命周期一致。
|
||||
- 不要只凭头文件声明或函数名猜测用法;不确定时继续查 demo、源码或已有调用点。
|
||||
4. 优先复用 Arm-2D 已有能力:
|
||||
- tile / region / child tile
|
||||
- opacity / alpha / mask
|
||||
- copy / fill / color key / mirroring / rotation / transform
|
||||
- dirty region / PFB / scene player
|
||||
- helper 提供的控件、进度条、列表、文本、仪表或 demo 写法
|
||||
5. 只有在确认现有 API 不足时,才新增轻量封装;新增封装也要贴合 Arm-2D tile/region/scene 模型,不要另起一套和 Arm-2D 割裂的 UI 框架。
|
||||
6. 给方案或代码前,尽量说明复用了哪些 Arm-2D API/示例,以及哪些部分是新增逻辑。
|
||||
|
||||
## Python 工具
|
||||
|
||||
素材处理脚本放在 `python/` 目录下,也要主动检查 Arm-2D 库 / Pack / 当前工程中是否已有同类资源生成脚本。Arm-2D 生态里常见脚本可用于生成字体、蒙版、图片数组、GIF/动画数组等资源。
|
||||
|
||||
当用户提出图片、字体、mask、GIF、动画帧、C 数组资源接入需求时:
|
||||
|
||||
1. 优先查找并使用现有 Python 脚本,不要手写一次性转换器,除非现有脚本不满足需求。
|
||||
2. 可以直接调用脚本生成输出,并将生成的 `.c/.h` 或资源文件加入工程。
|
||||
3. 生成前要核对目标屏幕分辨率、像素格式和资源显示区域,避免生成尺寸超过屏幕或和 UI 布局不匹配。
|
||||
4. 如果关键信息缺失,应主动询问用户,例如:
|
||||
- C 数组 / tile / 资源对象名称
|
||||
- 缩放尺寸或目标宽高
|
||||
- 是否保持比例、裁剪、居中或填充
|
||||
- 像素格式,例如 RGB565 / RGB888 / ARGB8888
|
||||
- 是否需要 mask / alpha / color key
|
||||
- GIF 是否需要抽帧、限帧率、缩放、转精灵图或转换为当前工程已有 loader 支持的资源格式
|
||||
5. 使用脚本前,优先查看脚本参数和输出约定,不要凭空猜命令。
|
||||
|
||||
现有素材处理脚本:
|
||||
|
||||
- `python/gif2png.py`:GIF 转精灵图。
|
||||
- `python/img2c.py`:图片转 ARM-2D tile / RGB565 / mask / zhRGB565 等。
|
||||
- `python/jinja2c.py`:模板生成 C 文件。
|
||||
- `python/ttf2c.py`:字体转 C 数据。
|
||||
- `python/__img2c_lmsk.py`:LMSK 辅助压缩模块。
|
||||
- `python/__img2c_zhRGB565.py`:zhRGB565 辅助压缩模块。
|
||||
|
||||
使用这些脚本前,优先查看脚本参数,不要凭空猜命令。
|
||||
|
||||
## ARM-2D Generic Loader 习惯
|
||||
|
||||
当需要动态内容、压缩资源解码、动画逐帧输出时,优先考虑 generic loader。
|
||||
|
||||
典型文件:
|
||||
|
||||
```text
|
||||
user_generic_loader_xxx.c
|
||||
user_generic_loader_xxx.h
|
||||
```
|
||||
|
||||
典型 API:
|
||||
|
||||
```c
|
||||
xxx_init();
|
||||
xxx_depose();
|
||||
xxx_on_load();
|
||||
xxx_on_frame_start();
|
||||
xxx_on_frame_complete();
|
||||
xxx_show();
|
||||
```
|
||||
|
||||
`fnDecode()` 必须尊重 `ptROI`,不要默认整屏连续 framebuffer。
|
||||
|
||||
详细写法见 `references/generic-loader.md`。
|
||||
|
||||
## GIF / 动画资源原则
|
||||
|
||||
不要把尚未实现的动画容器或 decoder 写成可用方案。处理动画资源时优先基于现有脚本和工程已有 loader 能力。
|
||||
|
||||
要明确区分:
|
||||
|
||||
- 未压缩 RGB565 帧序列:最容易播放,最吃 Flash。
|
||||
- 精灵图:适合工具链中间态,不一定适合最终固件。
|
||||
- zhRGB565 / QOI:压缩图片资源,需要对应 loader;若工程未启用对应 loader,先补 loader 或改用已支持格式。
|
||||
- mask/alpha:要单独确认是否存在以及如何合成。
|
||||
|
||||
如果用户明确要求节省 Flash,可以建议限帧率、缩放、裁剪、分块/差分、调色板动画、zhRGB565/QOI 等已具备或可落地的方向;不要声称尚未实现的动画容器当前可直接使用。
|
||||
|
||||
详细见 `references/asset-pipeline.md`。
|
||||
|
||||
## 性能经验
|
||||
|
||||
嵌入式 UI 热路径上要警惕:
|
||||
|
||||
- per-pixel 64-bit 除法
|
||||
- per-pixel 复杂迭代
|
||||
- 每帧全屏计算
|
||||
- 大型双缓存导致 Arm-2D 申请不到 scratch/PFB 内存
|
||||
- 双核/多核在嵌入式工程里的启动、栈、临界区和调试复杂度
|
||||
|
||||
优先方案:
|
||||
|
||||
- 低分辨率缓存 + 放大
|
||||
- palette animation
|
||||
- 查表 / 预计算 map 放 Flash
|
||||
- ROI-aware decode
|
||||
- tile/block 局部更新
|
||||
- 伪 shader,而不是真 shader
|
||||
|
||||
对于无 FPU 或浮点代价较高的 MCU,额外优先考虑定点数、查表和整数近似;对于带 FPU/DSP/SIMD/2D 加速器的 MCU,则应结合编译选项和实际 profiling 决定是否使用浮点或硬件加速。
|
||||
|
||||
特定 RP2040/Tufty2040 经验见 `references/rp2040-performance.md`,不要把该文件里的限制无条件套到其它 MCU。
|
||||
|
||||
## 红线
|
||||
|
||||
- 不要删除用户已有工程文件,除非明确要求。
|
||||
- 不要把大体积生成物塞进 skill。
|
||||
- 不要把未验证的格式 decoder 说成能用。
|
||||
- 不要编造 ARM-2D API;不确定时查工程已有代码或本地 pack 示例。
|
||||
- 拒绝直接修改 Arm-2D 官方 API、Pack 源码接口或官方私有结构体布局的请求;应改用公开 API、配置项、wrapper/adapter、用户工程本地扩展或向上游提交 patch 的方式解决。
|
||||
- 不要依赖 Arm-2D 私有结构体字段实现功能,除非官方 demo/公开头文件明确允许访问;必须访问时要先说明版本绑定和维护风险。
|
||||
@@ -0,0 +1,544 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
LMSK Encoder (Python) - based on LMSK spec + reference C encoder.
|
||||
|
||||
Features:
|
||||
1) Input: image path (PNG/BMP/JPG/...)
|
||||
2) If image has alpha: extract alpha channel -> compress to .lmsk
|
||||
3) If image has no alpha: convert to grayscale (8-bit) -> compress to .lmsk
|
||||
4) Optional output path; if omitted -> same directory, same stem + .lmsk
|
||||
5) Raw mode off by default; enable via --raw
|
||||
6) Disable gradient algorithm via --no-gradient
|
||||
7) Set gradient tolerant (0~3) via --gradient-tolerant
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
import zlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
raise SystemExit("Missing dependency: Pillow. Install with: pip install pillow")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Constants (aligned with __lmsk_common.h)
|
||||
# -----------------------------------------------------------------------------
|
||||
ARM_LMSK_VERSION_MAJOR = 1
|
||||
ARM_LMSK_VERSION_MINOR = 2
|
||||
ARM_LMSK_VERSION = ((ARM_LMSK_VERSION_MAJOR & 0xF) << 4) | (ARM_LMSK_VERSION_MINOR & 0xF)
|
||||
|
||||
TAG_U8_ALPHA = 0xFD
|
||||
TAG_U8_GRADIENT = 0xF9
|
||||
|
||||
END_MARK = 0xDEADBEEF
|
||||
|
||||
FLOOR_SIZE = 1 << 16 # tagSetBits=0 => 65536
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Helpers: header packing (16 bytes)
|
||||
# -----------------------------------------------------------------------------
|
||||
def pack_header(width: int,
|
||||
height: int,
|
||||
alpha_bits: int,
|
||||
raw: bool,
|
||||
floor_count: int,
|
||||
tagset_bits: int = 0) -> bytes:
|
||||
"""
|
||||
Pack arm_lmsk_header_t (16 bytes).
|
||||
Layout based on spec and __lmsk_common.h:
|
||||
chName[5] = "LMSK\0"
|
||||
Version = (major<<4)|minor
|
||||
iWidth = int16
|
||||
iHeight = int16
|
||||
settingBits= u3AlphaMSBCount:3 | bRaw:1 | u2TagSetBits:2 | reserved:2
|
||||
chFloorCount= uint8
|
||||
reserved32 = uint32
|
||||
"""
|
||||
if not (1 <= alpha_bits <= 8):
|
||||
raise ValueError("alpha_bits must be 1..8")
|
||||
if not (-32768 <= width <= 32767 and -32768 <= height <= 32767):
|
||||
raise ValueError("width/height must fit int16")
|
||||
|
||||
u3AlphaMSBCount = (alpha_bits - 1) & 0x7
|
||||
setting_byte = (u3AlphaMSBCount
|
||||
| ((1 if raw else 0) << 3)
|
||||
| ((tagset_bits & 0x3) << 4))
|
||||
|
||||
return (b"LMSK\0"
|
||||
+ struct.pack("<BhhBBI",
|
||||
ARM_LMSK_VERSION,
|
||||
width,
|
||||
height,
|
||||
setting_byte,
|
||||
floor_count & 0xFF,
|
||||
0))
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Delta helper (matches __arm_lmsk_get_delta)
|
||||
# -----------------------------------------------------------------------------
|
||||
def get_delta(prev: int, curr: int, alpha_bits: int) -> int:
|
||||
"""
|
||||
Compute delta in MSB domain, choosing the smaller absolute value with wrap-around,
|
||||
matching reference C encoder __arm_lmsk_get_delta().
|
||||
"""
|
||||
shift = 8 - alpha_bits
|
||||
prev_m = prev >> shift
|
||||
curr_m = curr >> shift
|
||||
delta0 = curr_m - prev_m
|
||||
mod = 1 << alpha_bits
|
||||
|
||||
if delta0 < 0:
|
||||
delta1 = (curr_m + mod) - prev_m
|
||||
return delta0 if abs(delta0) < abs(delta1) else delta1
|
||||
elif delta0 > 0:
|
||||
delta1 = (prev_m + mod) - curr_m
|
||||
return delta0 if abs(delta0) < abs(delta1) else -delta1
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Encoding result model
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class EncodeResult:
|
||||
hit: bool
|
||||
raw_size: int
|
||||
enc: bytes
|
||||
new_prev: int
|
||||
check_palette: bool
|
||||
name: str
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Palette helper (mimics reference encoder strategy with sentinel 0 after slot0)
|
||||
# -----------------------------------------------------------------------------
|
||||
def palette_find_or_insert(palette: List[int], alpha: int) -> Optional[int]:
|
||||
"""
|
||||
Reference encoder strategy:
|
||||
- search for exact alpha
|
||||
- stop early if palette[i]==0 and i>0 (sentinel = first free)
|
||||
- if not found and free exists -> insert and return index
|
||||
"""
|
||||
idx = 0
|
||||
for idx in range(32):
|
||||
if palette[idx] == alpha:
|
||||
return idx
|
||||
if palette[idx] == 0 and idx > 0:
|
||||
break
|
||||
|
||||
if idx < 32:
|
||||
palette[idx] = alpha
|
||||
return idx
|
||||
return None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Try functions: ALPHA / DELTA_LARGE / DELTA_SMALL / REPEAT / GRADIENT
|
||||
# -----------------------------------------------------------------------------
|
||||
def try_alpha(source: Sequence[int], size_left: int, prev: int,
|
||||
alpha_bits: int, palette: List[int], tolerant: int, actual_prev: int) -> EncodeResult:
|
||||
val = source[0]
|
||||
return EncodeResult(True, 1, bytes([TAG_U8_ALPHA, val]), val, True, "ALPHA")
|
||||
|
||||
|
||||
def try_delta_large(source: Sequence[int], size_left: int, prev: int,
|
||||
alpha_bits: int, palette: List[int], tolerant: int, actual_prev: int) -> EncodeResult:
|
||||
d = get_delta(prev, source[0], alpha_bits)
|
||||
if -32 <= d <= 31:
|
||||
b = ((d & 0x3F) << 2) | 0x03 # tag bits 0b11
|
||||
shift = 8 - alpha_bits
|
||||
new_prev = (source[0] >> shift) << shift
|
||||
return EncodeResult(True, 1, bytes([b]), new_prev, False, "DELTA_LARGE")
|
||||
return EncodeResult(False, 0, b"", prev, False, "DELTA_LARGE")
|
||||
|
||||
|
||||
def try_delta_small(source: Sequence[int], size_left: int, prev: int,
|
||||
alpha_bits: int, palette: List[int], tolerant: int, actual_prev: int) -> EncodeResult:
|
||||
if size_left < 2:
|
||||
return EncodeResult(False, 0, b"", prev, False, "DELTA_SMALL")
|
||||
|
||||
d0 = get_delta(prev, source[0], alpha_bits)
|
||||
if not (-4 <= d0 <= 3):
|
||||
return EncodeResult(False, 0, b"", prev, False, "DELTA_SMALL")
|
||||
|
||||
d1 = get_delta(source[0], source[1], alpha_bits)
|
||||
if not (-4 <= d1 <= 3):
|
||||
return EncodeResult(False, 0, b"", prev, False, "DELTA_SMALL")
|
||||
|
||||
# tag bits 0b10, d0 in bits[4:2], d1 in bits[7:5]
|
||||
b = 0x02 | ((d0 & 0x7) << 2) | ((d1 & 0x7) << 5)
|
||||
shift = 8 - alpha_bits
|
||||
new_prev = (source[1] >> shift) << shift
|
||||
return EncodeResult(True, 2, bytes([b]), new_prev, False, "DELTA_SMALL")
|
||||
|
||||
|
||||
def try_repeat_prev(source: Sequence[int], size_left: int, prev: int,
|
||||
alpha_bits: int, palette: List[int], tolerant: int, actual_prev: int) -> EncodeResult:
|
||||
shift = 8 - alpha_bits
|
||||
prev_m = prev >> shift
|
||||
|
||||
count = 0
|
||||
for i in range(size_left):
|
||||
if (source[i] >> shift) != prev_m:
|
||||
break
|
||||
count += 1
|
||||
|
||||
if count == 0:
|
||||
return EncodeResult(False, 0, b"", prev, False, "REPEAT")
|
||||
|
||||
if count <= 62:
|
||||
# tag bits 0b01; u6Repeat = count-1
|
||||
b = 0x01 | ((count - 1) << 2)
|
||||
return EncodeResult(True, count, bytes([b]), prev, False, "REPEAT")
|
||||
|
||||
# Reference encoder uses GRADIENT tag for extra-long repeat (>62)
|
||||
enc = struct.pack("<BBh", TAG_U8_GRADIENT, prev, count - 1)
|
||||
return EncodeResult(True, count, enc, prev, False, "REPEAT_GRADIENT")
|
||||
|
||||
|
||||
def try_gradient(source: Sequence[int], size_left: int, prev: int,
|
||||
alpha_bits: int, palette: List[int], tolerant: int, actual_prev: int) -> EncodeResult:
|
||||
"""
|
||||
Ported from __arm_lmsk_try_gradient_tag (C).
|
||||
Note: This is the "gradient detection algorithm"; can be disabled by --no-gradient.
|
||||
"""
|
||||
if size_left < 4:
|
||||
return EncodeResult(False, 0, b"", prev, False, "GRADIENT")
|
||||
|
||||
# initial
|
||||
iGradientSize = 1
|
||||
iPrevious = prev
|
||||
|
||||
iCurrent = source[0]
|
||||
idx = 1
|
||||
left = size_left - 1
|
||||
|
||||
chToAlpha = iCurrent
|
||||
iDeltaPrevious = iCurrent - iPrevious
|
||||
|
||||
iPrevious = iCurrent
|
||||
|
||||
step_len_current = 2
|
||||
step_len_prev = 0
|
||||
first_step_len = True
|
||||
|
||||
prev_step_to = 0
|
||||
prev_step_size = 0
|
||||
step_count = 0
|
||||
bNewStep = False
|
||||
|
||||
while left > 0:
|
||||
iCurrent = source[idx]
|
||||
idx += 1
|
||||
left -= 1
|
||||
|
||||
iDelta = iCurrent - iPrevious
|
||||
iDeltaChange = iDelta - iDeltaPrevious
|
||||
|
||||
if abs(iDeltaChange) > 1:
|
||||
break
|
||||
|
||||
if iDeltaChange == 0 or bNewStep:
|
||||
step_len_current += 1
|
||||
bNewStep = False
|
||||
else:
|
||||
if first_step_len:
|
||||
first_step_len = False
|
||||
else:
|
||||
if abs(step_len_current - step_len_prev) > tolerant:
|
||||
break
|
||||
|
||||
prev_step_to = iPrevious
|
||||
prev_step_size = iGradientSize
|
||||
step_count += 1
|
||||
|
||||
step_len_prev = step_len_current
|
||||
step_len_current = 1
|
||||
bNewStep = True
|
||||
|
||||
# wrong direction
|
||||
if iDelta < 0 < iDeltaPrevious:
|
||||
break
|
||||
if iDelta > 0 > iDeltaPrevious:
|
||||
break
|
||||
|
||||
iDeltaPrevious = iDelta
|
||||
chToAlpha = iCurrent
|
||||
iPrevious = iCurrent
|
||||
iGradientSize += 1
|
||||
|
||||
# resume to previous step if needed
|
||||
if step_count > 0:
|
||||
chToAlpha = prev_step_to
|
||||
iGradientSize = prev_step_size
|
||||
|
||||
if iGradientSize > 4:
|
||||
# case 1: prev equals actual previous pixel -> gradient tag directly
|
||||
if prev == actual_prev:
|
||||
steps = iGradientSize - 1 # count = pixels-1
|
||||
enc = struct.pack("<BBh", TAG_U8_GRADIENT, chToAlpha, steps)
|
||||
return EncodeResult(True, iGradientSize, enc, chToAlpha, False, "GRADIENT")
|
||||
|
||||
# case 2: prev mismatch but long enough -> insert INDEX/ALPHA + gradient
|
||||
if iGradientSize > 6:
|
||||
start_pixel = source[0] # actual first pixel of gradient segment
|
||||
|
||||
# Search/insert to palette (side effect allowed, matches C encoder)
|
||||
idx_pal = palette_find_or_insert(palette, start_pixel)
|
||||
|
||||
raw_size = iGradientSize
|
||||
grad_pixels = iGradientSize - 1
|
||||
steps = grad_pixels - 1 # = iGradientSize - 2
|
||||
grad_enc = struct.pack("<BBh", TAG_U8_GRADIENT, chToAlpha, steps)
|
||||
|
||||
if idx_pal is not None:
|
||||
index_byte = (idx_pal & 0x1F) << 2 # tag bits 00 + index
|
||||
enc = bytes([index_byte]) + grad_enc
|
||||
return EncodeResult(True, raw_size, enc, chToAlpha, False, "INDEX+GRADIENT")
|
||||
|
||||
# If palette full, fall back to ALPHA_TAG + gradient
|
||||
enc = bytes([TAG_U8_ALPHA, start_pixel]) + grad_enc
|
||||
return EncodeResult(True, raw_size, enc, chToAlpha, False, "ALPHA+GRADIENT")
|
||||
|
||||
return EncodeResult(False, 0, b"", prev, False, "GRADIENT")
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Encode one scanline (matches __arm_lmsk_encode_line)
|
||||
# -----------------------------------------------------------------------------
|
||||
def encode_line(row: bytes,
|
||||
alpha_bits: int,
|
||||
palette: List[int],
|
||||
no_gradient: bool,
|
||||
tolerant: int) -> bytes:
|
||||
width = len(row)
|
||||
out = bytearray()
|
||||
|
||||
# first pixel raw
|
||||
out.append(row[0])
|
||||
prev = row[0]
|
||||
|
||||
pos = 1
|
||||
left = width - 1
|
||||
|
||||
# order aligned with C: DELTA_LARGE, REPEAT, DELTA_SMALL, GRADIENT, ALPHA
|
||||
algs = [try_delta_large, try_repeat_prev, try_delta_small, try_gradient, try_alpha]
|
||||
|
||||
while left > 0:
|
||||
source = row[pos:]
|
||||
actual_prev = row[pos - 1] # pchSourceBase[-1] equivalent
|
||||
|
||||
best: Optional[EncodeResult] = None
|
||||
best_rate = -1.0
|
||||
|
||||
for fn in algs:
|
||||
if fn is try_gradient and no_gradient:
|
||||
continue
|
||||
|
||||
res = fn(source, left, prev, alpha_bits, palette, tolerant, actual_prev)
|
||||
if not res.hit:
|
||||
continue
|
||||
|
||||
rate = res.raw_size / len(res.enc)
|
||||
if (best is None) or (rate > best_rate) or (rate == best_rate and res.raw_size > best.raw_size):
|
||||
best = res
|
||||
best_rate = rate
|
||||
|
||||
if best is None:
|
||||
raise RuntimeError("No encoding decision hit (unexpected).")
|
||||
|
||||
# palette substitution only for ALPHA + raw_size==1 (matches C)
|
||||
if best.check_palette and best.raw_size == 1:
|
||||
idx_pal = palette_find_or_insert(palette, best.new_prev)
|
||||
if idx_pal is not None:
|
||||
index_byte = (idx_pal & 0x1F) << 2
|
||||
best = EncodeResult(True, 1, bytes([index_byte]), best.new_prev, False, "INDEX")
|
||||
|
||||
out.extend(best.enc)
|
||||
pos += best.raw_size
|
||||
left -= best.raw_size
|
||||
prev = best.new_prev
|
||||
|
||||
return bytes(out)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Whole-image encoding with row dedup + floor handling (matches lmsk_encoder.c)
|
||||
# -----------------------------------------------------------------------------
|
||||
@dataclass
|
||||
class LineOut:
|
||||
source: bytes
|
||||
encoded: bytes
|
||||
position: int
|
||||
crc32: int
|
||||
|
||||
|
||||
def encode_lmsk(mask: bytes,
|
||||
width: int,
|
||||
height: int,
|
||||
raw: bool = False,
|
||||
no_gradient: bool = False,
|
||||
tolerant: int = 1,
|
||||
alpha_bits: int = 8) -> bytes:
|
||||
if raw:
|
||||
# raw mode: header + raw pixels + end mark (no palette/tables)
|
||||
header = pack_header(width, height, alpha_bits=1, raw=True, floor_count=0)
|
||||
return header + mask + struct.pack("<I", END_MARK)
|
||||
|
||||
palette = [0] * 32
|
||||
crc_map: Dict[int, List[LineOut]] = {}
|
||||
|
||||
references = [0] * height
|
||||
data_section = bytearray()
|
||||
position = 0
|
||||
|
||||
for y in range(height):
|
||||
row = mask[y * width:(y + 1) * width]
|
||||
crc = zlib.crc32(row) & 0xFFFFFFFF
|
||||
|
||||
found: Optional[LineOut] = None
|
||||
candidates = crc_map.get(crc)
|
||||
if candidates:
|
||||
# search from tail (matches C behavior)
|
||||
for line in reversed(candidates):
|
||||
if line.source == row:
|
||||
found = line
|
||||
break
|
||||
|
||||
if found is not None:
|
||||
current_floor = position >> 16
|
||||
target_floor = found.position >> 16
|
||||
|
||||
if current_floor != target_floor:
|
||||
# clone into current floor (matches C)
|
||||
new_line = LineOut(found.source, found.encoded, position, crc)
|
||||
crc_map.setdefault(crc, []).append(new_line)
|
||||
references[y] = position
|
||||
data_section.extend(found.encoded)
|
||||
position += len(found.encoded)
|
||||
else:
|
||||
references[y] = found.position
|
||||
else:
|
||||
encoded = encode_line(row, alpha_bits, palette, no_gradient, tolerant)
|
||||
new_line = LineOut(row, encoded, position, crc)
|
||||
crc_map.setdefault(crc, []).append(new_line)
|
||||
references[y] = position
|
||||
data_section.extend(encoded)
|
||||
position += len(encoded)
|
||||
|
||||
# build floor table + line index table
|
||||
floor_level = 0
|
||||
floor_entries: List[int] = []
|
||||
index_table: List[int] = [0] * height
|
||||
|
||||
for y in range(height):
|
||||
ref = references[y]
|
||||
if ref - floor_level < 0:
|
||||
raise RuntimeError("Reference before current floor level (should not happen).")
|
||||
|
||||
if ref - floor_level >= FLOOR_SIZE:
|
||||
floor_level += FLOOR_SIZE
|
||||
floor_entries.append(y)
|
||||
|
||||
index_table[y] = ref - floor_level
|
||||
if not (0 <= index_table[y] <= 0xFFFF):
|
||||
raise RuntimeError("Line index overflow (must fit uint16).")
|
||||
|
||||
floor_count = len(floor_entries)
|
||||
if floor_count > 255:
|
||||
raise RuntimeError("Too large: floor_count > 255 (data section >= 16MB).")
|
||||
|
||||
header = pack_header(width, height, alpha_bits, raw=False, floor_count=floor_count)
|
||||
|
||||
palette_bytes = bytes(palette)
|
||||
floor_bytes = b"".join(struct.pack("<H", v) for v in floor_entries) if floor_count else b""
|
||||
index_bytes = b"".join(struct.pack("<H", v) for v in index_table)
|
||||
end_mark = struct.pack("<I", END_MARK)
|
||||
|
||||
return header + palette_bytes + floor_bytes + index_bytes + bytes(data_section) + end_mark
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Image loading: alpha extraction or grayscale conversion
|
||||
# -----------------------------------------------------------------------------
|
||||
def load_mask_from_image(path: Path) -> Tuple[bytes, int, int, bool]:
|
||||
img = Image.open(path)
|
||||
|
||||
# Detect alpha presence robustly
|
||||
has_alpha = ("A" in img.mode) or (img.mode == "P" and "transparency" in img.info)
|
||||
|
||||
if has_alpha:
|
||||
img = img.convert("RGBA")
|
||||
alpha = img.getchannel("A")
|
||||
mask = alpha.tobytes()
|
||||
else:
|
||||
img = img.convert("L")
|
||||
mask = img.tobytes()
|
||||
|
||||
return mask, img.width, img.height, has_alpha
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CLI
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Encode image alpha (or grayscale) into LMSK (*.lmsk)."
|
||||
)
|
||||
parser.add_argument("image", help="Input image path (PNG/BMP/JPG/...).")
|
||||
parser.add_argument("-o", "--output", default=None,
|
||||
help="Output .lmsk full path. If omitted, use input stem + .lmsk in same dir.")
|
||||
parser.add_argument("--raw", action="store_true",
|
||||
help="Enable RAW mode (no compression, no palette/tables). Default: off.")
|
||||
parser.add_argument("--no-gradient", action="store_true",
|
||||
help="Disable gradient detection algorithm.")
|
||||
parser.add_argument("--gradient-tolerant", type=int, default=1, choices=[0, 1, 2, 3],
|
||||
help="Gradient tolerant (0~3). Default: 1.")
|
||||
parser.add_argument("--alpha-bits", type=int, default=8, choices=[1, 2, 3, 4, 5, 6, 7, 8],
|
||||
help="Alpha Bits (1~8). Default: 8.")
|
||||
args = parser.parse_args()
|
||||
|
||||
in_path = Path(args.image)
|
||||
if not in_path.exists():
|
||||
raise SystemExit(f"Input file not found: {in_path}")
|
||||
|
||||
out_path = Path(args.output) if args.output else in_path.with_suffix(".lmsk")
|
||||
if out_path.suffix.lower() != ".lmsk":
|
||||
out_path = out_path.with_suffix(".lmsk")
|
||||
|
||||
mask, w, h, has_alpha = load_mask_from_image(in_path)
|
||||
|
||||
# For your current requirements, keep alpha_bits=8 (true lossless for 8-bit alpha/grayscale)
|
||||
alpha_bits = args.alpha_bits
|
||||
|
||||
blob = encode_lmsk(mask,
|
||||
width=w,
|
||||
height=h,
|
||||
raw=args.raw,
|
||||
no_gradient=args.no_gradient,
|
||||
tolerant=args.gradient_tolerant,
|
||||
alpha_bits=alpha_bits)
|
||||
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_bytes(blob)
|
||||
|
||||
kind = "Alpha" if has_alpha else "Grayscale"
|
||||
mode = "RAW" if args.raw else "Compressed"
|
||||
print(f"[OK] {kind} -> LMSK ({mode})")
|
||||
print(f" Input : {in_path}")
|
||||
print(f" Output: {out_path}")
|
||||
print(f" Size : {len(blob)} bytes ({w}x{h})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,746 @@
|
||||
"""zhRGB565 Encoder Library
|
||||
Combines RLE and RLE+Differential encoding implementations
|
||||
Strictly follows C implementation data types and behavior
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
from typing import Tuple, Optional
|
||||
|
||||
# Encoding thresholds
|
||||
RLE_THRESHOLD = 3 # Minimum consecutive pixels for RLE encoding
|
||||
DIFF_THRESHOLD = 7 # Minimum pixels for differential encoding
|
||||
|
||||
|
||||
def rgb565_get_r_u8(color: np.uint16) -> np.uint8:
|
||||
"""Extract RGB565 red component (5 bits) - matches C uint8_t"""
|
||||
return np.uint8((color >> 11) & 0x1F)
|
||||
|
||||
|
||||
def rgb565_get_g_u8(color: np.uint16) -> np.uint8:
|
||||
"""Extract RGB565 green component (6 bits) - matches C uint8_t"""
|
||||
return np.uint8((color >> 5) & 0x3F)
|
||||
|
||||
|
||||
def rgb565_get_b_u8(color: np.uint16) -> np.uint8:
|
||||
"""Extract RGB565 blue component (5 bits) - matches C uint8_t"""
|
||||
return np.uint8(color & 0x1F)
|
||||
|
||||
|
||||
def rgb332_val(r: np.uint8, g: np.uint8, b: np.uint8) -> np.uint8:
|
||||
"""Pack RGB components into RGB332 format - matches C uint8_t"""
|
||||
return np.uint8(((r & 0x07) << 5) | ((g & 0x07) << 2) | (b & 0x03))
|
||||
|
||||
|
||||
def pack_u8_to_u16(high: np.uint8, low: np.uint8) -> np.uint16:
|
||||
"""Pack two uint8 values into one uint16 - matches C uint16_t"""
|
||||
return np.uint16((np.uint16(high) << 8) | np.uint16(low))
|
||||
|
||||
|
||||
def can_compress_diff(diff: np.uint16) -> bool:
|
||||
"""
|
||||
Check if difference can be compressed into one byte - matches C implementation exactly
|
||||
|
||||
Condition: R component <=7, G component <=7, B component <=3
|
||||
"""
|
||||
r = rgb565_get_r_u8(diff)
|
||||
g = rgb565_get_g_u8(diff)
|
||||
b = rgb565_get_b_u8(diff)
|
||||
|
||||
return (r <= np.uint8(7)) and (g <= np.uint8(7)) and (b <= np.uint8(3))
|
||||
|
||||
|
||||
def compress_diff_to_byte(diff: np.uint16) -> np.uint8:
|
||||
"""Compress difference into one byte (RGB332 format) - matches C uint8_t"""
|
||||
r = rgb565_get_r_u8(diff)
|
||||
g = rgb565_get_g_u8(diff)
|
||||
b = rgb565_get_b_u8(diff)
|
||||
|
||||
return rgb332_val(r, g, b)
|
||||
|
||||
|
||||
def find_encode_flag(img: np.ndarray, pixel_count: np.uint64) -> Tuple[np.uint16, np.uint16, np.uint16, bool]:
|
||||
"""
|
||||
Find optimal encoding flag - strictly matches C implementation data types
|
||||
|
||||
Priority:
|
||||
1. Unused high byte values (XX00 format)
|
||||
2. High bytes where all pixels have at least 3 consecutive identical values
|
||||
3. Least used high bytes
|
||||
|
||||
Args:
|
||||
img: RGB565 image data array
|
||||
pixel_count: Number of pixels (uint64_t in C)
|
||||
|
||||
Returns:
|
||||
(encode_flag, encode_flag_cs, encode_flag_mode, is_perfect)
|
||||
encode_flag: Flag value (high byte << 8)
|
||||
encode_flag_cs: Related info (minimum consecutive count or occurrence count)
|
||||
encode_flag_mode: Mode (0=unused, 1=RLE perfect, 2=least used)
|
||||
is_perfect: Whether perfect flag was found
|
||||
"""
|
||||
if pixel_count == 0:
|
||||
return np.uint16(0xFF00), np.uint16(0), np.uint16(0), True
|
||||
|
||||
# ========== Step 1: Count basic information ==========
|
||||
# Match C implementation: uint8_t used_map[32] = {0};
|
||||
used_map = np.zeros(32, dtype=np.uint8)
|
||||
# Match C implementation: uint16_t count_map[256] = {0};
|
||||
# Use uint32_t to prevent overflow with large images
|
||||
count_map = np.zeros(256, dtype=np.uint32)
|
||||
|
||||
for i in range(int(pixel_count)):
|
||||
pixel = int(img[i])
|
||||
hi_byte = (pixel >> 8) & 0xFF
|
||||
used_map[hi_byte // 8] |= np.uint8(1 << (hi_byte % 8))
|
||||
count_map[hi_byte] += np.uint32(1) # Use uint32_t to prevent overflow
|
||||
|
||||
# ========== Step 2: Priority 1 - Unused high bytes ==========
|
||||
for hi_byte in range(256):
|
||||
if (int(used_map[hi_byte // 8]) & (1 << (hi_byte % 8))) == 0:
|
||||
return np.uint16(hi_byte << 8), np.uint16(0), np.uint16(0), True
|
||||
|
||||
# ========== Step 3: Priority 2 - Perfect RLE high bytes ==========
|
||||
for hi_byte in range(256):
|
||||
if count_map[hi_byte] == 0:
|
||||
continue
|
||||
|
||||
is_perfect = True
|
||||
min_continuous = np.uint16(0xFFFF)
|
||||
|
||||
i = np.uint64(0)
|
||||
while i < pixel_count:
|
||||
# Skip pixels that don't have this high byte
|
||||
while i < pixel_count and ((int(img[int(i)]) >> 8) & 0xFF) != hi_byte:
|
||||
i += np.uint64(1)
|
||||
|
||||
if i >= pixel_count:
|
||||
break
|
||||
|
||||
# Check continuous segments
|
||||
current_value = np.uint16(img[int(i)])
|
||||
continuous_count = np.uint16(1)
|
||||
j = i + np.uint64(1)
|
||||
|
||||
while j < pixel_count and np.uint16(img[int(j)]) == current_value:
|
||||
continuous_count += np.uint16(1)
|
||||
j += np.uint64(1)
|
||||
|
||||
# Check if condition is met (at least 3 consecutive identical)
|
||||
if continuous_count < RLE_THRESHOLD:
|
||||
is_perfect = False
|
||||
break
|
||||
|
||||
if continuous_count < min_continuous:
|
||||
min_continuous = continuous_count
|
||||
|
||||
i = j
|
||||
|
||||
if is_perfect and min_continuous != np.uint16(0xFFFF):
|
||||
# Find a specific pixel value as flag
|
||||
concrete_value = np.uint16(hi_byte << 8)
|
||||
for k in range(int(pixel_count)):
|
||||
if ((int(img[k]) >> 8) & 0xFF) == hi_byte:
|
||||
concrete_value = np.uint16(img[k])
|
||||
break
|
||||
|
||||
return np.uint16(concrete_value & 0xFF00), min_continuous, np.uint16(1), True
|
||||
|
||||
# ========== Step 4: Priority 3 - Least used high bytes ==========
|
||||
min_hi_byte = np.uint16(0)
|
||||
min_count = np.uint16(0xFFFF)
|
||||
|
||||
for hi_byte in range(256):
|
||||
if count_map[hi_byte] > 0:
|
||||
if count_map[hi_byte] < min_count:
|
||||
min_count = count_map[hi_byte]
|
||||
min_hi_byte = np.uint16(hi_byte)
|
||||
elif count_map[hi_byte] == min_count:
|
||||
if hi_byte < min_hi_byte:
|
||||
min_hi_byte = np.uint16(hi_byte)
|
||||
|
||||
if min_count == np.uint16(0xFFFF):
|
||||
return np.uint16(0xFF00), np.uint16(0), np.uint16(2), False
|
||||
|
||||
# Important: In least used mode, return XX00 format
|
||||
return np.uint16(min_hi_byte << 8), min_count, np.uint16(2), False
|
||||
|
||||
|
||||
def check_rle_length(pixels: np.ndarray, start: np.uint32, end: np.uint32) -> np.uint32:
|
||||
"""
|
||||
Check RLE length starting from position - strictly matches C implementation data types
|
||||
|
||||
Args:
|
||||
pixels: Pixel array
|
||||
start: Start position (uint32_t in C)
|
||||
end: End position (exclusive, uint32_t in C)
|
||||
|
||||
Returns:
|
||||
Number of consecutive identical pixels (uint32_t)
|
||||
"""
|
||||
if start >= end:
|
||||
return np.uint32(0)
|
||||
|
||||
first_color = np.uint16(pixels[int(start)])
|
||||
length = np.uint32(1)
|
||||
|
||||
for i in range(int(start) + 1, int(end)):
|
||||
if np.uint16(pixels[i]) == first_color:
|
||||
length += np.uint32(1)
|
||||
if length == np.uint32(0xFFFF):
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
return length
|
||||
|
||||
|
||||
def calculate_diff_length(data: np.ndarray, start: np.uint32, end: np.uint32, output: np.ndarray) -> np.uint16:
|
||||
"""
|
||||
Calculate the number of pixels that meet differential encoding criteria - strictly matches C implementation
|
||||
|
||||
Args:
|
||||
data: Pixel data array
|
||||
start: Start position (uint32_t in C)
|
||||
end: End position (exclusive, uint32_t in C)
|
||||
output: Output difference data buffer
|
||||
|
||||
Returns:
|
||||
Compressible difference pixel count (uint16_t in C)
|
||||
"""
|
||||
if end - start < DIFF_THRESHOLD:
|
||||
return np.uint16(0)
|
||||
|
||||
idx = np.uint16(0) # CRITICAL: Must be uint16_t like C
|
||||
idx_tmp = np.uint16(0)
|
||||
tmp = np.zeros(2, dtype=np.uint8)
|
||||
|
||||
# CRITICAL: C uses uint16_t for loop variable, not uint32_t
|
||||
i = np.uint16(start)
|
||||
diff_count = np.uint16(1)
|
||||
rle_cnt = np.uint16(1)
|
||||
|
||||
while i < end - np.uint16(1):
|
||||
current_pixel = np.uint16(data[int(i)])
|
||||
next_pixel = np.uint16(data[int(i) + 1])
|
||||
diff = np.uint16(current_pixel ^ next_pixel)
|
||||
|
||||
# Meet differential encoding conditions
|
||||
if can_compress_diff(diff):
|
||||
diff_count += np.uint16(1)
|
||||
|
||||
tmp[int(idx_tmp)] = compress_diff_to_byte(diff)
|
||||
idx_tmp += np.uint16(1)
|
||||
|
||||
if idx_tmp == np.uint16(2):
|
||||
if int(idx) >= len(output):
|
||||
# Prevent buffer overflow - match C behavior
|
||||
break
|
||||
output[int(idx)] = pack_u8_to_u16(tmp[0], tmp[1])
|
||||
idx += np.uint16(1)
|
||||
idx_tmp = np.uint16(0)
|
||||
if idx == np.uint16(31): # Actual encoded source data 31*2 + 1
|
||||
break
|
||||
|
||||
if diff == np.uint16(0):
|
||||
# diff=0 means 2 identical pixels
|
||||
rle_cnt += np.uint16(1)
|
||||
# Handle uint16_t overflow - if rle_cnt reaches max, treat as exceeding threshold
|
||||
if rle_cnt == np.uint16(0): # Overflow occurred
|
||||
diff_count -= np.uint16(RLE_THRESHOLD)
|
||||
break
|
||||
if rle_cnt > np.uint16(RLE_THRESHOLD): # CRITICAL: Revert back to >
|
||||
# Consecutive pixels exceed 3+1, exit for RLE processing
|
||||
diff_count -= np.uint16(RLE_THRESHOLD)
|
||||
break
|
||||
else:
|
||||
rle_cnt = np.uint16(0)
|
||||
|
||||
# CRITICAL: C uses i++ which keeps it as uint16_t
|
||||
i += np.uint16(1)
|
||||
else:
|
||||
break
|
||||
|
||||
# CRITICAL: C doesn't have this logic - only adjust for even count
|
||||
# Handle remaining differences - CRITICAL: Try removing this adjustment
|
||||
# if idx_tmp == np.uint16(1):
|
||||
# # Odd number of differences, reduce by one
|
||||
# diff_count -= np.uint16(1)
|
||||
|
||||
# Number of pixels meeting differential encoding must be odd and not zero
|
||||
if diff_count % np.uint16(2) == np.uint16(0) and diff_count != np.uint16(0):
|
||||
diff_count -= np.uint16(1)
|
||||
|
||||
if diff_count >= np.uint16(DIFF_THRESHOLD):
|
||||
return diff_count
|
||||
else:
|
||||
return np.uint16(0)
|
||||
|
||||
|
||||
def encode_rgb565_rle_only(input_data: np.ndarray, width: np.uint16, height: np.uint16) -> Tuple[Optional[np.ndarray], np.uint32, float]:
|
||||
"""
|
||||
Pure RLE encoding function - strictly matches C implementation data types
|
||||
|
||||
Args:
|
||||
input_data: Input RGB565 data, length is width*height
|
||||
width: Image width (uint16_t in C)
|
||||
height: Image height (uint16_t in C)
|
||||
|
||||
Returns:
|
||||
(output_data, output_size, compression_ratio)
|
||||
output_data: Encoded data array
|
||||
output_size: Encoded data size (uint32_t in C)
|
||||
compression_ratio: Compression ratio (percentage, smaller is better)
|
||||
"""
|
||||
pixel_count = np.uint64(width) * np.uint64(height)
|
||||
|
||||
if width == 0 or height == 0 or pixel_count == 0:
|
||||
return None, np.uint32(0), 0.0
|
||||
|
||||
# Find encoding flag - pass pixel_count as uint64_t
|
||||
encode_flag, encode_flag_cs, encode_flag_mode, flag_ok = find_encode_flag(input_data, pixel_count)
|
||||
|
||||
# Estimate maximum output size - match C calculation exactly (uint64_t)
|
||||
max_output_size = np.uint64(10) + np.uint64(height) + (pixel_count * np.uint64(2))
|
||||
output = np.zeros(int(max_output_size), dtype=np.uint32) # Use 32-bit to avoid overflow during construction
|
||||
|
||||
# Allocate row offset array (uint32_t like C)
|
||||
row_offsets = np.zeros(height + 1, dtype=np.uint32)
|
||||
|
||||
# Encoding data buffer (uint32_t for index calculations)
|
||||
encoded_data = np.zeros(int(max_output_size), dtype=np.uint32)
|
||||
encoded_index = np.uint32(0)
|
||||
|
||||
# Traverse row by row (uint16_t y in C)
|
||||
for y in range(int(height)):
|
||||
row_offsets[y] = encoded_index
|
||||
row_start = y * int(width)
|
||||
row = input_data[row_start:row_start + int(width)]
|
||||
|
||||
col = np.uint32(0)
|
||||
while col < width:
|
||||
# Check RLE length (uint32_t in C)
|
||||
rle_len = check_rle_length(row, col, width)
|
||||
|
||||
if rle_len >= RLE_THRESHOLD:
|
||||
color = np.uint16(row[int(col)])
|
||||
|
||||
if rle_len >= np.uint32(128):
|
||||
# Long encoding: flag, color, count (match C exactly)
|
||||
encoded_data[int(encoded_index)] = encode_flag
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = color
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = rle_len
|
||||
encoded_index += np.uint32(1)
|
||||
else:
|
||||
# Short encoding: flag + count, color (match C exactly)
|
||||
encoded_data[int(encoded_index)] = np.uint16(encode_flag + rle_len)
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = color
|
||||
encoded_index += np.uint32(1)
|
||||
|
||||
col += rle_len
|
||||
else:
|
||||
# Handle pixels that conflict with flag
|
||||
color_tmp = np.uint16(row[int(col)])
|
||||
|
||||
if (color_tmp & np.uint16(0xFF00)) == encode_flag:
|
||||
# Pixel conflicts with flag code, use RLE short encoding to store single pixel
|
||||
encoded_data[int(encoded_index)] = np.uint16(encode_flag + 1)
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = color_tmp
|
||||
encoded_index += np.uint32(1)
|
||||
else:
|
||||
# Store original pixel directly
|
||||
encoded_data[int(encoded_index)] = color_tmp
|
||||
encoded_index += np.uint32(1)
|
||||
|
||||
col += np.uint32(1)
|
||||
|
||||
row_offsets[height] = encoded_index
|
||||
|
||||
# Calculate upgrade table - match C logic exactly (uint16_t upgrade[500])
|
||||
upgrade = np.zeros(500, dtype=np.uint16)
|
||||
upgrade_len = np.uint16(0)
|
||||
for i in range(int(height) - 1):
|
||||
tmp0 = row_offsets[i]
|
||||
tmp1 = row_offsets[i + 1]
|
||||
if tmp0 > tmp1:
|
||||
upgrade[int(upgrade_len)] = np.uint16(i + 1)
|
||||
upgrade_len += np.uint16(1)
|
||||
|
||||
# Calculate row table start coordinate and encoding data start coordinate - match C exactly
|
||||
row_offset_addr = np.uint16(6) + upgrade_len
|
||||
encode_data_addr = row_offset_addr + np.uint16(height) + np.uint16(1)
|
||||
|
||||
# Fill header - match C structure exactly
|
||||
output[0] = width
|
||||
output[1] = height
|
||||
output[2] = encode_flag
|
||||
output[3] = upgrade_len
|
||||
output[4] = row_offset_addr
|
||||
output[5] = encode_data_addr
|
||||
|
||||
idx = np.uint32(6)
|
||||
|
||||
# Write upgrade table (uint16_t values)
|
||||
if upgrade_len > 0:
|
||||
for i in range(int(upgrade_len)):
|
||||
output[int(idx)] = upgrade[i]
|
||||
idx += np.uint32(1)
|
||||
|
||||
# Write row offset table (uint32_t values)
|
||||
for i in range(int(height) + 1):
|
||||
output[int(idx)] = row_offsets[i]
|
||||
idx += np.uint32(1)
|
||||
|
||||
# Write encoding data
|
||||
for i in range(int(encoded_index)):
|
||||
output[int(idx)] = encoded_data[i]
|
||||
idx += np.uint32(1)
|
||||
|
||||
# Calculate compression ratio - match C calculation exactly
|
||||
original_size = float(pixel_count * np.uint64(2)) # Original data size (bytes)
|
||||
compressed_size = float(idx * np.uint32(2)) # Compressed size (bytes)
|
||||
compression_ratio = (compressed_size / original_size) * 100.0
|
||||
|
||||
# Convert to uint16 array for return (like C output)
|
||||
result = output[:int(idx)].astype(np.uint16)
|
||||
|
||||
return result, idx, compression_ratio
|
||||
|
||||
|
||||
def encode_rgb565_rle_diff(input_data: np.ndarray, width: np.uint16, height: np.uint16) -> Tuple[Optional[np.ndarray], np.uint32, float]:
|
||||
"""
|
||||
RLE+Differential mixed encoding function - strictly matches C implementation data types
|
||||
|
||||
Args:
|
||||
input_data: Input RGB565 data, length is width*height
|
||||
width: Image width (uint16_t in C)
|
||||
height: Image height (uint16_t in C)
|
||||
|
||||
Returns:
|
||||
(output_data, output_size, compression_ratio)
|
||||
"""
|
||||
pixel_count = np.uint64(width) * np.uint64(height)
|
||||
|
||||
if width == np.uint16(0) or height == np.uint16(0) or pixel_count == np.uint64(0):
|
||||
return None, np.uint32(0), 0.0
|
||||
|
||||
# Find encoding flag - match C call exactly (uint64_t pixel_count)
|
||||
encode_flag, encode_flag_cs, encode_flag_mode, flag_ok = find_encode_flag(input_data, pixel_count)
|
||||
|
||||
# Estimate maximum output size - match C calculation exactly (uint64_t)
|
||||
max_output_size = np.uint64(6) + np.uint64(height) + (pixel_count * np.uint64(2))
|
||||
output = np.zeros(int(max_output_size), dtype=np.uint32) # Use 32-bit to avoid overflow
|
||||
|
||||
# Allocate row offset array - use uint32_t like C
|
||||
row_offsets = np.zeros(int(height) + 1, dtype=np.uint32)
|
||||
|
||||
# Differential encoding data buffer - match C: encoded_diff_data = (uint16_t*)malloc((size_t)65536 * 2 * sizeof(uint16_t))
|
||||
encoded_diff_data = np.zeros(65536 * 2, dtype=np.uint16)
|
||||
|
||||
# Encoding data buffer - use uint32_t for index calculations (encoded_index = uint32_t)
|
||||
encoded_data = np.zeros(int(max_output_size), dtype=np.uint32)
|
||||
encoded_index = np.uint32(0)
|
||||
|
||||
# Traverse row by row - match C logic exactly (uint16_t y)
|
||||
for y in range(int(height)):
|
||||
row_offsets[y] = encoded_index
|
||||
row_start = y * int(width)
|
||||
row = input_data[row_start:row_start + int(width)]
|
||||
|
||||
col = np.uint32(0)
|
||||
while col < width:
|
||||
# Try differential encoding first (like C) - uint32_t parameters
|
||||
diff_len = calculate_diff_length(row, col, width, encoded_diff_data)
|
||||
|
||||
if diff_len >= np.uint16(DIFF_THRESHOLD):
|
||||
base_color = np.uint16(row[int(col)])
|
||||
|
||||
if diff_len >= np.uint16(128):
|
||||
# Long encoding - match C exactly
|
||||
encoded_data[int(encoded_index)] = encode_flag
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = base_color
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = np.uint16(0x8000 + (diff_len // np.uint16(2)))
|
||||
encoded_index += np.uint32(1)
|
||||
|
||||
for i in range(int(diff_len) // 2):
|
||||
encoded_data[int(encoded_index)] = encoded_diff_data[i]
|
||||
encoded_index += np.uint32(1)
|
||||
else:
|
||||
# Short encoding - match C exactly
|
||||
encoded_data[int(encoded_index)] = np.uint16(encode_flag + np.uint16(0x80) + (diff_len // np.uint16(2)))
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = base_color
|
||||
encoded_index += np.uint32(1)
|
||||
|
||||
for i in range(int(diff_len) // 2):
|
||||
encoded_data[int(encoded_index)] = encoded_diff_data[i]
|
||||
encoded_index += np.uint32(1)
|
||||
|
||||
col += diff_len
|
||||
else:
|
||||
# Try RLE encoding (like C) - uint32_t parameters
|
||||
rle_len = check_rle_length(row, col, width)
|
||||
|
||||
if rle_len >= np.uint32(RLE_THRESHOLD):
|
||||
color = np.uint16(row[int(col)])
|
||||
|
||||
if rle_len >= np.uint32(128):
|
||||
# Long encoding - match C exactly
|
||||
encoded_data[int(encoded_index)] = encode_flag
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = color
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = rle_len
|
||||
encoded_index += np.uint32(1)
|
||||
else:
|
||||
# Short encoding - match C exactly
|
||||
encoded_data[int(encoded_index)] = np.uint16(encode_flag + rle_len)
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = color
|
||||
encoded_index += np.uint32(1)
|
||||
|
||||
col += rle_len
|
||||
else:
|
||||
# Store original pixel directly - match C logic exactly
|
||||
color_tmp = np.uint16(row[int(col)])
|
||||
|
||||
if (color_tmp & np.uint16(0xFF00)) == encode_flag:
|
||||
# Pixel conflicts with flag code - match C exactly
|
||||
encoded_data[int(encoded_index)] = np.uint16(encode_flag + np.uint16(1))
|
||||
encoded_index += np.uint32(1)
|
||||
encoded_data[int(encoded_index)] = row[int(col)] # CRITICAL: C uses row[col], not color_tmp
|
||||
encoded_index += np.uint32(1)
|
||||
else:
|
||||
# Store original pixel directly
|
||||
encoded_data[int(encoded_index)] = color_tmp
|
||||
encoded_index += np.uint32(1)
|
||||
|
||||
col += np.uint32(1)
|
||||
|
||||
row_offsets[int(height)] = encoded_index
|
||||
|
||||
# Calculate upgrade table - match C logic exactly (uint16_t upgrade[500])
|
||||
upgrade = np.zeros(500, dtype=np.uint16)
|
||||
upgrade_len = np.uint16(0)
|
||||
for i in range(int(height) - 1):
|
||||
# CRITICAL: C casts to uint16_t for comparison
|
||||
tmp0 = np.uint16(row_offsets[i])
|
||||
tmp1 = np.uint16(row_offsets[i + 1])
|
||||
if tmp0 > tmp1:
|
||||
upgrade[int(upgrade_len)] = np.uint16(i + 1)
|
||||
upgrade_len += np.uint16(1)
|
||||
|
||||
# Calculate row table start coordinate and encoding data start coordinate - match C exactly
|
||||
row_offset_addr = np.uint16(6) + upgrade_len
|
||||
encode_data_addr = row_offset_addr + np.uint16(height) + np.uint16(1)
|
||||
|
||||
# Fill header - match C structure exactly
|
||||
output[0] = width
|
||||
output[1] = height
|
||||
output[2] = encode_flag
|
||||
output[3] = upgrade_len
|
||||
output[4] = row_offset_addr
|
||||
output[5] = encode_data_addr
|
||||
|
||||
idx = np.uint32(6)
|
||||
|
||||
# Write upgrade table (uint16_t values)
|
||||
if upgrade_len > 0:
|
||||
for i in range(int(upgrade_len)):
|
||||
output[int(idx)] = upgrade[i]
|
||||
idx += np.uint32(1)
|
||||
|
||||
# Write row offset table (uint16_t values - CRITICAL: C casts to uint16_t)
|
||||
for i in range(int(height) + 1):
|
||||
output[int(idx)] = np.uint16(row_offsets[i])
|
||||
idx += np.uint32(1)
|
||||
|
||||
# Write encoding data
|
||||
for i in range(int(encoded_index)):
|
||||
output[int(idx)] = encoded_data[i]
|
||||
idx += np.uint32(1)
|
||||
|
||||
# Calculate compression ratio - match C calculation exactly
|
||||
original_size = float(pixel_count * np.uint64(2))
|
||||
compressed_size = float(idx * np.uint32(2))
|
||||
compression_ratio = (compressed_size / original_size) * 100.0
|
||||
|
||||
# Convert to uint16 array for return (like C output)
|
||||
result = output[:int(idx)].astype(np.uint16)
|
||||
|
||||
return result, idx, compression_ratio
|
||||
|
||||
|
||||
def generate_c_array(output_data: np.ndarray, output_size: np.uint32,
|
||||
width: np.uint16, height: np.uint16, compression_ratio: float,
|
||||
src_path: str = "", array_name: str = "img") -> str:
|
||||
"""
|
||||
Generate C language array format string - matches C implementation
|
||||
|
||||
Args:
|
||||
output_data: Encoded data
|
||||
output_size: Data size (uint32_t in C)
|
||||
width: Image width (uint16_t in C)
|
||||
height: Image height (uint16_t in C)
|
||||
compression_ratio: Compression ratio
|
||||
src_path: Source file path
|
||||
array_name: Array name
|
||||
|
||||
Returns:
|
||||
C language code string
|
||||
"""
|
||||
import os
|
||||
|
||||
# Get base filename
|
||||
base_name = os.path.splitext(os.path.basename(src_path))[0] if src_path else "image"
|
||||
|
||||
lines = []
|
||||
lines.append("// Compressed RGB565 data")
|
||||
lines.append(f"// Source file: {src_path}")
|
||||
lines.append(f"// Original size: {width} x {height} = {width * height} pixels")
|
||||
lines.append(f"// Compression ratio: {compression_ratio:.2f}%")
|
||||
lines.append("")
|
||||
lines.append(f"const uint16_t _{base_name}_zhRGB565_Data[{output_size}] = {{")
|
||||
lines.append("")
|
||||
|
||||
# Write header information
|
||||
lines.append(" /* width, height, encode_flag, level_up_table_len, row_offset_addr, data_addr */")
|
||||
lines.append(f" {output_data[0]}, {output_data[1]}, 0x{output_data[2]:04X}, {output_data[3]}, {output_data[4]}, {output_data[5]},")
|
||||
lines.append("")
|
||||
|
||||
idx = np.uint32(6)
|
||||
upgrade_len = np.uint16(output_data[3])
|
||||
row_offset_addr = np.uint16(output_data[4])
|
||||
|
||||
# Write upgrade table
|
||||
if upgrade_len > 0:
|
||||
lines.append(" /* level_up table */")
|
||||
line = " "
|
||||
count = 0
|
||||
for i in range(int(upgrade_len)):
|
||||
line += f"{output_data[int(idx)]}, "
|
||||
idx += np.uint32(1)
|
||||
count += 1
|
||||
if count % 16 == 0:
|
||||
lines.append(line.rstrip())
|
||||
line = " "
|
||||
if line.strip():
|
||||
lines.append(line.rstrip())
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append(" /* NO level_up table */")
|
||||
lines.append("")
|
||||
|
||||
# Write row offset table
|
||||
lines.append(f" /* Row offset table ({height} rows total) */")
|
||||
line = " "
|
||||
for i in range(int(height) + 1):
|
||||
line += f"{output_data[int(idx)]}"
|
||||
idx += np.uint32(1)
|
||||
line += ", " # Add comma after all elements (including last)
|
||||
if (i + 1) % 16 == 0:
|
||||
lines.append(line.rstrip())
|
||||
line = " "
|
||||
if line.strip():
|
||||
lines.append(line.rstrip())
|
||||
lines.append("")
|
||||
|
||||
# Write encoding data
|
||||
lines.append(" /* Encoded data */")
|
||||
|
||||
# Format output by lines - match C logic exactly
|
||||
hhcnt = np.uint32(0)
|
||||
line_base = np.uint32(0)
|
||||
next_line = np.uint32(1)
|
||||
current_line = np.uint32(0)
|
||||
|
||||
lines.append(f" /* 0 */")
|
||||
line = " "
|
||||
|
||||
while idx < output_size:
|
||||
if next_line == height:
|
||||
# Last line, output all
|
||||
while idx < output_size:
|
||||
line += f"0x{int(output_data[int(idx)]):04X}, "
|
||||
idx += np.uint32(1)
|
||||
hhcnt += np.uint32(1)
|
||||
if hhcnt % 16 == 0:
|
||||
lines.append(line.rstrip())
|
||||
line = " "
|
||||
else:
|
||||
current_line_pos = np.uint32(output_data[int(row_offset_addr) + int(current_line)]) + line_base
|
||||
next_line_pos = np.uint32(output_data[int(row_offset_addr) + int(next_line)]) + line_base
|
||||
|
||||
if current_line_pos > next_line_pos:
|
||||
line_base += np.uint32(65536)
|
||||
next_line_pos += np.uint32(65536)
|
||||
|
||||
for j in range(int(current_line_pos), int(next_line_pos)):
|
||||
if idx >= output_size:
|
||||
break
|
||||
line += f"0x{int(output_data[int(idx)]):04X}, "
|
||||
idx += np.uint32(1)
|
||||
hhcnt += np.uint32(1)
|
||||
if hhcnt % 16 == 0 and j != int(next_line_pos) - 1:
|
||||
lines.append(line.rstrip())
|
||||
line = " "
|
||||
|
||||
current_line = next_line
|
||||
next_line += np.uint32(1)
|
||||
hhcnt = np.uint32(0)
|
||||
|
||||
if idx >= output_size:
|
||||
break
|
||||
|
||||
if line.strip():
|
||||
lines.append(line.rstrip())
|
||||
lines.append(f" /* {current_line} */")
|
||||
line = " "
|
||||
|
||||
if line.strip():
|
||||
lines.append(line.rstrip())
|
||||
|
||||
lines.append("};")
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test code
|
||||
# Create a simple test image
|
||||
test_data = np.array([
|
||||
0xF800, 0xF800, 0xF800, 0x07E0, 0x07E0, 0x001F, 0x001F, 0x001F,
|
||||
0xF800, 0xF800, 0xF800, 0x07E0, 0x07E0, 0x001F, 0x001F, 0x001F,
|
||||
], dtype=np.uint16)
|
||||
|
||||
width = np.uint16(8)
|
||||
height = np.uint16(2)
|
||||
|
||||
print("Testing RLE only encoding:")
|
||||
result, size, ratio = encode_rgb565_rle_only(test_data, width, height)
|
||||
if result is not None:
|
||||
print(f"Encoding successful!")
|
||||
print(f"Output size: {size} uint16")
|
||||
print(f"Compression ratio: {ratio:.2f}%")
|
||||
print("\nGenerated C array:")
|
||||
c_code = generate_c_array(result, size, width, height, ratio, "test.bmp")
|
||||
print(c_code)
|
||||
|
||||
print("\n" + "="*50 + "\n")
|
||||
|
||||
# Test RLE+Diff encoding
|
||||
print("Testing RLE+Diff encoding:")
|
||||
result, size, ratio = encode_rgb565_rle_diff(test_data, width, height)
|
||||
if result is not None:
|
||||
print(f"Encoding successful!")
|
||||
print(f"Output size: {size} uint16")
|
||||
print(f"Compression ratio: {ratio:.2f}%")
|
||||
print("\nGenerated C array:")
|
||||
c_code = generate_c_array(result, size, width, height, ratio, "test.bmp")
|
||||
print(c_code)
|
||||
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
GIF to PNG Sprite Sheet Converter
|
||||
Flattens GIF animation frames into a single PNG sprite sheet
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Convert GIF animation to a flattened PNG sprite sheet',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python gif2png.py animation.gif -n 5
|
||||
python gif2png.py animation.gif -n 8 --dim 64 64
|
||||
python gif2png.py animation.gif -n 4 --dim 100 100 -o output.png
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'input',
|
||||
type=str,
|
||||
help='Input GIF file path'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-n', '--frames-per-row',
|
||||
type=int,
|
||||
default=5,
|
||||
metavar='N',
|
||||
help='Number of frames per row (default: 5)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--dim',
|
||||
type=int,
|
||||
nargs=2,
|
||||
metavar=('WIDTH', 'HEIGHT'),
|
||||
help='Resize each frame to specified width and height, e.g.: --dim 64 64'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-o', '--output',
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
help='Output file path (default: same name as input with .png extension)'
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def process_gif(input_path, frames_per_row, target_size=None, output_path=None):
|
||||
"""
|
||||
Process GIF file and flatten frames into a PNG sprite sheet
|
||||
|
||||
Args:
|
||||
input_path: Input GIF file path
|
||||
frames_per_row: Number of frames per row
|
||||
target_size: (width, height) tuple, or None to keep original size
|
||||
output_path: Output file path, or None to auto-generate
|
||||
"""
|
||||
# Open GIF file
|
||||
try:
|
||||
gif = Image.open(input_path)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File not found - {input_path}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Error: Cannot open file - {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Check if file is GIF format
|
||||
if gif.format != 'GIF':
|
||||
print(f"Warning: Input file format is {gif.format}, not standard GIF format")
|
||||
|
||||
# Extract all frames
|
||||
frames = []
|
||||
try:
|
||||
while True:
|
||||
# Copy current frame (seek changes current frame)
|
||||
frame = gif.copy()
|
||||
|
||||
# Convert to RGBA mode to support transparency
|
||||
if frame.mode != 'RGBA':
|
||||
frame = frame.convert('RGBA')
|
||||
|
||||
# Resize if target size specified
|
||||
if target_size:
|
||||
frame = frame.resize(target_size, Image.Resampling.LANCZOS)
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
# Move to next frame
|
||||
gif.seek(gif.tell() + 1)
|
||||
except EOFError:
|
||||
pass # Reached end of frames
|
||||
|
||||
if not frames:
|
||||
print("Error: No frames extracted")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Successfully extracted {len(frames)} frames")
|
||||
|
||||
# Get frame dimensions (all frames have same size after resize)
|
||||
frame_width, frame_height = frames[0].size
|
||||
|
||||
# Calculate grid layout
|
||||
total_frames = len(frames)
|
||||
num_cols = min(frames_per_row, total_frames)
|
||||
num_rows = (total_frames + frames_per_row - 1) // frames_per_row # Ceiling division
|
||||
|
||||
print(f"Layout: {num_rows} rows x {num_cols} columns")
|
||||
print(f"Frame size: {frame_width}x{frame_height}")
|
||||
|
||||
# Create canvas
|
||||
canvas_width = num_cols * frame_width
|
||||
canvas_height = num_rows * frame_height
|
||||
|
||||
print(f"Output image size: {canvas_width}x{canvas_height}")
|
||||
|
||||
# Create canvas with transparent background
|
||||
sprite_sheet = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0))
|
||||
|
||||
# Arrange frames row by row
|
||||
for idx, frame in enumerate(frames):
|
||||
row = idx // frames_per_row
|
||||
col = idx % frames_per_row
|
||||
|
||||
x = col * frame_width
|
||||
y = row * frame_height
|
||||
|
||||
sprite_sheet.paste(frame, (x, y), frame) # Use frame as mask to preserve transparency
|
||||
|
||||
# Determine output path
|
||||
if output_path is None:
|
||||
input_path = Path(input_path)
|
||||
output_path = input_path.with_suffix('.png')
|
||||
else:
|
||||
output_path = Path(output_path)
|
||||
|
||||
# Save PNG file
|
||||
try:
|
||||
# Optimize PNG file size
|
||||
sprite_sheet.save(
|
||||
output_path,
|
||||
'PNG',
|
||||
optimize=True,
|
||||
compress_level=9
|
||||
)
|
||||
print(f"Saved: {output_path.absolute()}")
|
||||
except Exception as e:
|
||||
print(f"Error: Failed to save file - {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
# Validate arguments
|
||||
if args.frames_per_row <= 0:
|
||||
print("Error: Frames per row must be greater than 0")
|
||||
sys.exit(1)
|
||||
|
||||
if args.dim:
|
||||
width, height = args.dim
|
||||
if width <= 0 or height <= 0:
|
||||
print("Error: Dimensions must be greater than 0")
|
||||
sys.exit(1)
|
||||
target_size = (width, height)
|
||||
else:
|
||||
target_size = None
|
||||
|
||||
# Process GIF
|
||||
process_gif(
|
||||
input_path=args.input,
|
||||
frames_per_row=args.frames_per_row,
|
||||
target_size=target_size,
|
||||
output_path=args.output
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
GIF逐帧平铺工具 - 用于动效预览/帧表制作,完全支持透明背景
|
||||
========== 所有参数都在下面配置区改,不用动下面逻辑 ==========
|
||||
"""
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import os
|
||||
|
||||
# ====================== 配置区(你直接改这里就行) ======================
|
||||
INPUT_GIF_PATH = r"C:\Users\11791\OneDrive\Desktop\images\girl.gif" # 输入GIF路径
|
||||
OUTPUT_IMAGE_PATH = r"C:\Users\11791\OneDrive\Desktop\images\girl.png" # 输出平铺图路径
|
||||
# 平铺配置
|
||||
PER_ROW = 12 # 每行放多少帧,自动计算总高度
|
||||
FRAME_SPACING = 0 # 帧之间的间距(像素)
|
||||
SCALE = 1.0 # 缩放比例,<1缩小,>1放大,1为原尺寸
|
||||
# 美化配置
|
||||
BORDER_WIDTH = 0 # 每帧的边框宽度,0为不加边框
|
||||
BORDER_COLOR = (255, 0, 0, 255) # 边框颜色,(R,G,B,A),默认红色
|
||||
SHOW_INDEX = False # 是否在每帧下方显示帧编号
|
||||
INDEX_FONT_SIZE = 12 # 编号字体大小
|
||||
INDEX_COLOR = (255, 255, 255, 255) # 编号颜色,默认白色
|
||||
TRANSPARENT_BACKGROUND = True # 输出背景是否透明,False则为黑色背景
|
||||
# =======================================================================
|
||||
|
||||
# 读取GIF所有帧
|
||||
gif = Image.open(INPUT_GIF_PATH)
|
||||
frames = []
|
||||
try:
|
||||
while True:
|
||||
# 复制当前帧,保留透明通道
|
||||
frame = gif.copy().convert("RGBA")
|
||||
# 缩放
|
||||
if SCALE != 1.0:
|
||||
new_w = int(frame.width * SCALE)
|
||||
new_h = int(frame.height * SCALE)
|
||||
frame = frame.resize((new_w, new_h), Image.Resampling.NEAREST)
|
||||
frames.append(frame)
|
||||
gif.seek(gif.tell() + 1)
|
||||
except EOFError:
|
||||
pass # 读完所有帧
|
||||
total_frames = len(frames)
|
||||
print(f"读取到GIF共{total_frames}帧,单帧尺寸:{frames[0].width}×{frames[0].height}")
|
||||
|
||||
# 计算平铺图总尺寸
|
||||
frame_w = frames[0].width
|
||||
frame_h = frames[0].height
|
||||
extra_height = INDEX_FONT_SIZE + 5 if SHOW_INDEX else 0
|
||||
rows = (total_frames + PER_ROW - 1) // PER_ROW # 向上取整计算行数
|
||||
total_width = PER_ROW * frame_w + (PER_ROW - 1) * FRAME_SPACING + BORDER_WIDTH * 2
|
||||
total_height = rows * (frame_h + extra_height) + (rows - 1) * FRAME_SPACING + BORDER_WIDTH * 2
|
||||
|
||||
# 创建画布
|
||||
bg_mode = "RGBA" if TRANSPARENT_BACKGROUND else "RGB"
|
||||
bg_color = (0,0,0,0) if TRANSPARENT_BACKGROUND else (0,0,0)
|
||||
tile_img = Image.new(bg_mode, (total_width, total_height), bg_color)
|
||||
draw = ImageDraw.Draw(tile_img)
|
||||
# 加载字体(用系统默认无衬线字体)
|
||||
try:
|
||||
font = ImageFont.truetype("arial.ttf", INDEX_FONT_SIZE)
|
||||
except:
|
||||
font = ImageFont.load_default(size=INDEX_FONT_SIZE)
|
||||
|
||||
# 逐帧贴到画布
|
||||
for idx, frame in enumerate(frames):
|
||||
row = idx // PER_ROW
|
||||
col = idx % PER_ROW
|
||||
# 计算当前帧的位置
|
||||
x = BORDER_WIDTH + col * (frame_w + FRAME_SPACING)
|
||||
y = BORDER_WIDTH + row * (frame_h + FRAME_SPACING + extra_height)
|
||||
# 贴帧
|
||||
tile_img.paste(frame, (x, y), frame)
|
||||
# 画边框
|
||||
if BORDER_WIDTH > 0:
|
||||
draw.rectangle(
|
||||
[x, y, x + frame_w, y + frame_h],
|
||||
outline=BORDER_COLOR,
|
||||
width=BORDER_WIDTH
|
||||
)
|
||||
# 写编号
|
||||
if SHOW_INDEX:
|
||||
text = f"#{idx+1}"
|
||||
# 计算文字居中位置
|
||||
text_bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_w = text_bbox[2] - text_bbox[0]
|
||||
text_x = x + (frame_w - text_w) // 2
|
||||
text_y = y + frame_h + 2
|
||||
draw.text((text_x, text_y), text, fill=INDEX_COLOR, font=font)
|
||||
print(f"已处理第{idx+1}/{total_frames}帧")
|
||||
|
||||
# 保存
|
||||
tile_img.save(OUTPUT_IMAGE_PATH)
|
||||
print(f"\n✅ 平铺图生成完成!路径:{OUTPUT_IMAGE_PATH}")
|
||||
print(f"平铺尺寸:{total_width}×{total_height},共{rows}行×{PER_ROW}列")
|
||||
@@ -0,0 +1,811 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# *************************************************************************************************
|
||||
# Arm 2D project
|
||||
# @file img2c.py
|
||||
# @brief image to C-array converter
|
||||
#
|
||||
# *************************************************************************************************
|
||||
#
|
||||
# * Copyright (C) 2010-2022 ARM Limited or its affiliates. All rights reserved.
|
||||
# *
|
||||
# * SPDX-License-Identifier: Apache-2.0
|
||||
# *
|
||||
# * Licensed under the Apache License, Version 2.0 (the License); you may
|
||||
# * not use this file except in compliance with the License.
|
||||
# * You may obtain a copy of the License at
|
||||
# *
|
||||
# * www.apache.org/licenses/LICENSE-2.0
|
||||
# *
|
||||
# * Unless required by applicable law or agreed to in writing, software
|
||||
# * distributed under the License is distributed on an AS IS BASIS, WITHOUT
|
||||
# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# * See the License for the specific language governing permissions and
|
||||
# * limitations under the License.
|
||||
# */
|
||||
|
||||
import sys
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import time;
|
||||
import argparse
|
||||
import os
|
||||
|
||||
# Add current script directory to path for imports
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
if script_dir not in sys.path:
|
||||
sys.path.insert(0, script_dir)
|
||||
sys.path.insert(0, '.') # Also add current directory
|
||||
|
||||
# Import zhRGB565 compression modules
|
||||
try:
|
||||
from __img2c_zhRGB565 import (
|
||||
encode_rgb565_rle_only,
|
||||
generate_c_array as generate_rle_c_array,
|
||||
encode_rgb565_rle_diff,
|
||||
generate_c_array as generate_diff_c_array
|
||||
)
|
||||
ZHRGB565_AVAILABLE = True
|
||||
except ImportError:
|
||||
ZHRGB565_AVAILABLE = False
|
||||
|
||||
# Import LMSK compression modules
|
||||
try:
|
||||
from __img2c_lmsk import (
|
||||
load_mask_from_image,
|
||||
encode_lmsk,
|
||||
)
|
||||
LMSK_AVAILABLE = True
|
||||
except ImportError:
|
||||
LMSK_AVAILABLE = False
|
||||
|
||||
hdr="""
|
||||
/* Generated on {0} from {1} */
|
||||
/* Re-sized : {2} */
|
||||
/* Rotated : {3} deg */
|
||||
|
||||
#include "arm_2d.h"
|
||||
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic push
|
||||
# pragma clang diagnostic ignored "-Wunknown-warning-option"
|
||||
# pragma clang diagnostic ignored "-Wreserved-identifier"
|
||||
# pragma clang diagnostic ignored "-Wmissing-variable-declarations"
|
||||
# pragma clang diagnostic ignored "-Wcast-qual"
|
||||
#elif defined(__IS_COMPILER_ARM_COMPILER_5__)
|
||||
# pragma diag_suppress=1296
|
||||
#endif
|
||||
|
||||
"""
|
||||
|
||||
tailDataGRAY8="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}GRAY8;
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}GRAY8\")
|
||||
const arm_2d_tile_t c_tile{0}GRAY8 = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_GRAY8,
|
||||
}},
|
||||
}},
|
||||
{3}c_bmp{0}GRAY8,
|
||||
}};
|
||||
|
||||
"""
|
||||
|
||||
tailDataRGB565="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}RGB565;
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}RGB565\")
|
||||
const arm_2d_tile_t c_tile{0}RGB565 = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_RGB565,
|
||||
}},
|
||||
}},
|
||||
{3}c_bmp{0}RGB565,
|
||||
}};
|
||||
|
||||
"""
|
||||
|
||||
tailDataRGB888="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}CCCN888;
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}CCCN888\")
|
||||
const arm_2d_tile_t c_tile{0}CCCN888 = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_RGB888,
|
||||
}},
|
||||
}},
|
||||
{3}c_bmp{0}CCCN888,
|
||||
}};
|
||||
|
||||
"""
|
||||
|
||||
tailDataRGBA8888="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}CCCA8888;
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}CCCA8888\")
|
||||
const arm_2d_tile_t c_tile{0}CCCA8888 = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_BGRA8888,
|
||||
}},
|
||||
}},
|
||||
{3}c_bmp{0}CCCA8888,
|
||||
}};
|
||||
|
||||
"""
|
||||
|
||||
|
||||
|
||||
tailAlpha="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}Mask;
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}Mask\")
|
||||
const arm_2d_tile_t c_tile{0}Mask = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_MASK_A8,
|
||||
}},
|
||||
}},
|
||||
.pchBuffer = (uint8_t *)c_bmp{0}Alpha,
|
||||
}};
|
||||
"""
|
||||
|
||||
tail1BitAlpha="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}A1Mask;
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}A1Mask\")
|
||||
const arm_2d_tile_t c_tile{0}A1Mask = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_MASK_A1,
|
||||
}},
|
||||
}},
|
||||
.pchBuffer = (uint8_t *)c_bmp{0}A1Alpha,
|
||||
}};
|
||||
"""
|
||||
|
||||
|
||||
tail2BitAlpha="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}A2Mask;
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}A2Mask\")
|
||||
const arm_2d_tile_t c_tile{0}A2Mask = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_MASK_A2,
|
||||
}},
|
||||
}},
|
||||
.pchBuffer = (uint8_t *)c_bmp{0}A2Alpha,
|
||||
}};
|
||||
"""
|
||||
|
||||
tail4BitAlpha="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}A4Mask;
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}A4Mask\")
|
||||
const arm_2d_tile_t c_tile{0}A4Mask = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_MASK_A4,
|
||||
}},
|
||||
}},
|
||||
.pchBuffer = (uint8_t *)c_bmp{0}A4Alpha,
|
||||
}};
|
||||
"""
|
||||
|
||||
|
||||
tailAlpha2="""
|
||||
|
||||
extern const arm_2d_tile_t c_tile{0}Mask2;
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tile{0}Mask2\")
|
||||
const arm_2d_tile_t c_tile{0}Mask2 = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_CHANNEL_8in32,
|
||||
}},
|
||||
}},
|
||||
.nAddress = ((intptr_t)c_bmp{0}CCCA8888) + 3,
|
||||
}};
|
||||
"""
|
||||
|
||||
|
||||
tail="""
|
||||
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic pop
|
||||
#elif defined(__IS_COMPILER_ARM_COMPILER_5__)
|
||||
# pragma diag_warning=1296
|
||||
#endif
|
||||
|
||||
"""
|
||||
|
||||
def main(argv):
|
||||
|
||||
parser = argparse.ArgumentParser(description='image to C array converter (v2.1.0)')
|
||||
|
||||
parser.add_argument('-i', nargs='?', type = str, required=False, help="Input file (png, bmp, etc..)")
|
||||
parser.add_argument('-o', nargs='?', type = str, required=False, help="output C file containing RGB56/RGB888/Gray8 and alpha values arrays")
|
||||
|
||||
parser.add_argument('--name', nargs='?',type = str, required=False, help="A specified array name")
|
||||
parser.add_argument('--format', nargs='?',type = str, default="all", help="RGB Format (rgb565, rgb32, gray8, mask, zhRGB565, lmsk, all)")
|
||||
parser.add_argument('--dim', nargs=2,type = int, help="Resize the image with the given width and height")
|
||||
parser.add_argument('--rot', nargs='?',type = float, default=0.0, help="Rotate the image with the given angle in degrees")
|
||||
parser.add_argument('--a1', action='store_true', help="Generate 1bit alpha-mask")
|
||||
parser.add_argument('--a2', action='store_true', help="Generate 2bit alpha-mask")
|
||||
parser.add_argument('--a4', action='store_true', help="Generate 4bit alpha-mask")
|
||||
parser.add_argument('--a8', action='store_true', help="Generate 8bit alpha-mask")
|
||||
parser.add_argument('--border', action='store_true', help="Add a 1pix border")
|
||||
parser.add_argument("--no-gradient", action="store_true", help="Disable gradient detection algorithm. (Repeat>62 still uses GRADIENT tag like C encoder.)")
|
||||
parser.add_argument("--gradient-tolerant", type=int, default=2, choices=[0, 1, 2, 3], help="Gradient tolerant (0~3). Default: 2.")
|
||||
parser.add_argument("--alpha-bits", type=int, default=6, choices=[1, 2, 3, 4, 5, 6, 7, 8], help="Alpha Bits (1~8). Default: 6.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.i == None or args.i == "" :
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
inputfile = args.i;
|
||||
basename = os.path.basename(inputfile).split('.')[0];
|
||||
|
||||
|
||||
outputfile = args.o
|
||||
if outputfile == None or outputfile == "":
|
||||
outputfile = basename + ".c"
|
||||
|
||||
arr_name = args.name
|
||||
if arr_name == None or arr_name == "":
|
||||
arr_name = basename
|
||||
|
||||
if args.format != 'rgb565' and \
|
||||
args.format != 'rgb32' and \
|
||||
args.format != 'gray8' and \
|
||||
args.format != 'mask' and \
|
||||
args.format != 'zhRGB565' and \
|
||||
args.format != 'lmsk' and \
|
||||
args.format != 'all':
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
|
||||
if args.a1:
|
||||
args.format = ''
|
||||
|
||||
if args.a2:
|
||||
args.format = ''
|
||||
|
||||
if args.a4:
|
||||
args.format = ''
|
||||
|
||||
if args.a8:
|
||||
args.format = ''
|
||||
|
||||
try:
|
||||
image = Image.open(inputfile)
|
||||
except FileNotFoundError:
|
||||
print("Cannot open image file %s" % (inputfile))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# rotation
|
||||
if args.rot != 0.0:
|
||||
image = image.rotate(args.rot)
|
||||
|
||||
|
||||
# resizing
|
||||
resized = False
|
||||
if args.dim != None:
|
||||
image = image.resize((args.dim[0], args.dim[1]))
|
||||
resized = True
|
||||
|
||||
|
||||
mode = image.mode
|
||||
|
||||
# add 1 pixel border
|
||||
if args.border:
|
||||
data = np.asarray(image)
|
||||
pad_val = 0
|
||||
if data.ndim == 2:
|
||||
padded = np.pad(data, 1, mode='constant', constant_values=pad_val)
|
||||
else:
|
||||
padded = np.pad(data, ((1, 1), (1, 1), (0, 0)),
|
||||
mode='constant', constant_values=pad_val)
|
||||
image = Image.fromarray(padded, mode=mode)
|
||||
|
||||
# Modes supported by Pillow
|
||||
|
||||
# 1 (1-bit pixels, black and white, stored with one pixel per byte), the value is in 0-1.
|
||||
# L (8-bit pixels, black and white), the value is in 0-255.
|
||||
# P (8-bit pixels, mapped to any other mode using a color palette), the value is in 0-255.
|
||||
# RGB (3×8-bit pixels, true color), the value is in [0-255, 0-255, 0-255].
|
||||
# RGBA (4×8-bit pixels, true color with transparency mask), the value is in [0-255, 0-255, 0-255, 0-255]
|
||||
# CMYK (4×8-bit pixels, color separation)
|
||||
# YCbCr (3×8-bit pixels, color video format)
|
||||
# LAB (3×8-bit pixels, the L*a*b color space)
|
||||
# HSV (3×8-bit pixels, Hue, Saturation, Value color space)
|
||||
# I (32-bit signed integer pixels)
|
||||
# F (32-bit floating point pixels)
|
||||
# LA (L with alpha)
|
||||
# PA (P with alpha)
|
||||
# RGBX (true color with padding)
|
||||
# RGBa (true color with premultiplied alpha)
|
||||
# La (L with premultiplied alpha)
|
||||
# I;16 (16-bit unsigned integer pixels)
|
||||
# I;16L (16-bit little endian unsigned integer pixels)
|
||||
# I;16B (16-bit big endian unsigned integer pixels)
|
||||
# I;16N (16-bit native endian unsigned integer pixels)
|
||||
# BGR;15 (15-bit reversed true colour)
|
||||
# BGR;16 (16-bit reversed true colour)
|
||||
# BGR;24 (24-bit reversed true colour)
|
||||
# BGR;32 (32-bit reversed true colour)
|
||||
|
||||
# handle {P, LA, RGB, RGBA} for now
|
||||
origin_image = image
|
||||
|
||||
if mode == 'P' or mode == 'LA':
|
||||
image = image.convert('RGBA')
|
||||
mode = 'RGBA'
|
||||
if mode == 'L':
|
||||
image = image.convert('RGB')
|
||||
mode = 'RGB'
|
||||
(row, col) = image.size
|
||||
data = np.asarray(image)
|
||||
|
||||
# C Array format width
|
||||
WIDTH_ALPHA = 16
|
||||
WIDTH_GRAY8 = 32
|
||||
WIDTH_RGB565 = 16
|
||||
WIDTH_RGB32 = 16
|
||||
|
||||
with open(outputfile,"w") as o:
|
||||
|
||||
# insert header
|
||||
print(hdr.format(time.asctime( time.localtime(time.time())), argv[0], resized, args.rot), file=o)
|
||||
|
||||
if mode == "RGBA":
|
||||
if args.format == 'all' or args.format == 'mask':
|
||||
print('ARM_ALIGN(4) ARM_SECTION(\"arm2d.asset.c_bmp%sAlpha\")' % (arr_name), file=o)
|
||||
# alpha channel array available
|
||||
print('static const uint8_t c_bmp%sAlpha[%d*%d] = {' % (arr_name, row, col),file=o)
|
||||
cnt = 0
|
||||
for eachRow in data:
|
||||
lineWidth=0
|
||||
print("/* -%d- */" % (cnt), file=o)
|
||||
for eachPix in eachRow:
|
||||
alpha = eachPix[3]
|
||||
if lineWidth % WIDTH_ALPHA == (WIDTH_ALPHA - 1):
|
||||
print("0x%02x," %(alpha) ,file=o)
|
||||
else:
|
||||
print("0x%02x" %(alpha), end =", ",file=o)
|
||||
lineWidth+=1
|
||||
cnt+=1
|
||||
print('',file=o)
|
||||
print('};\r\n', file=o)
|
||||
|
||||
# 1-bit Alpha channel
|
||||
if args.a1 or args.format == 'all' or args.format == 'mask':
|
||||
|
||||
def RevBitPairPerByte(byteArr):
|
||||
return ((byteArr & 0x01) << 7) | ((byteArr & 0x80) >> 7) | ((byteArr & 0x40) >> 5) | ((byteArr & 0x02) << 5) | \
|
||||
((byteArr & 0x04) << 3) | ((byteArr & 0x20) >> 3) | ((byteArr & 0x10) >> 1) | ((byteArr & 0x08) << 1)
|
||||
|
||||
print('ARM_ALIGN(4) ARM_SECTION("arm2d.asset.c_bmp%sA1Alpha")' % (arr_name), file=o)
|
||||
print('static const uint8_t c_bmp%sA1Alpha[%d*%d] = {' % (arr_name, (row+7)//8, col), file=o)
|
||||
cnt = 0
|
||||
alpha = data[...,3].astype(np.uint8)
|
||||
for eachRow in alpha:
|
||||
lineWidth=0
|
||||
print("/* -%d- */" % (cnt), file=o)
|
||||
|
||||
bitsArr = np.unpackbits(eachRow.astype(np.uint8))
|
||||
|
||||
# generate indexes for MSB bit every byte
|
||||
idx = np.arange(0, np.size(bitsArr), 8)
|
||||
idx = np.reshape(idx, (1,-1))
|
||||
|
||||
# extraction + endianness conversion
|
||||
msbBits = bitsArr[idx] & 0x80 >> 7
|
||||
packedBytes = RevBitPairPerByte(np.packbits(msbBits))
|
||||
|
||||
for elt in packedBytes:
|
||||
if lineWidth % WIDTH_ALPHA == (WIDTH_ALPHA-1):
|
||||
print("0x%02x," %(elt) ,file=o)
|
||||
else:
|
||||
print("0x%02x" %(elt), end =", ",file=o)
|
||||
lineWidth+=1
|
||||
cnt+=1
|
||||
print('',file=o)
|
||||
print('};\n', file=o)
|
||||
|
||||
# 2-bit Alpha channel
|
||||
if args.a2 or args.format == 'all' or args.format == 'mask':
|
||||
|
||||
def RevBitPairPerByte(byteArr):
|
||||
return ((byteArr & 0x03) << 6) | ((byteArr & 0xc0) >> 6) | ((byteArr & 0x30) >> 2 ) | ((byteArr & 0x0c) << 2)
|
||||
|
||||
|
||||
print('ARM_ALIGN(4) ARM_SECTION(\"arm2d.asset.c_bmp%sA2Alpha\")' % (arr_name), file=o)
|
||||
print('static const uint8_t c_bmp%sA2Alpha[%d*%d] = {' % (arr_name, (row+3)/4, col),file=o)
|
||||
cnt = 0
|
||||
alpha = data[...,3].astype(np.uint8)
|
||||
for eachRow in alpha:
|
||||
lineWidth=0
|
||||
print("/* -%d- */" % (cnt), file=o)
|
||||
|
||||
bitsArr = np.unpackbits(eachRow.astype(np.uint8))
|
||||
|
||||
# generate indexes for MSB bit pair every byte
|
||||
idx = np.arange(0, np.size(bitsArr), 8)
|
||||
idx=np.reshape(np.column_stack((idx+0, idx+1)), (1,-1))
|
||||
|
||||
# extraction + endianness conversion
|
||||
packedBytes = RevBitPairPerByte(np.packbits(bitsArr[idx]))
|
||||
|
||||
for elt in packedBytes:
|
||||
if lineWidth % WIDTH_ALPHA == (WIDTH_ALPHA-1):
|
||||
print("0x%02x," %(elt) ,file=o)
|
||||
else:
|
||||
print("0x%02x" %(elt), end =", ",file=o)
|
||||
lineWidth+=1
|
||||
cnt+=1
|
||||
print('',file=o)
|
||||
print('};\r\n', file=o)
|
||||
|
||||
# 4-bit Alpha channel
|
||||
if args.a4 or args.format == 'all' or args.format == 'mask':
|
||||
|
||||
def RevBitQuadPerByte(byteArr):
|
||||
return ((byteArr & 0x0f) << 4) | ((byteArr & 0xf0) >> 4)
|
||||
|
||||
|
||||
print('ARM_ALIGN(4) ARM_SECTION(\"arm2d.asset.c_bmp%sA4Alpha\")' % (arr_name), file=o)
|
||||
print('static const uint8_t c_bmp%sA4Alpha[%d*%d] = {' % (arr_name, (row+1)/2, col),file=o)
|
||||
cnt = 0
|
||||
alpha = data[...,3].astype(np.uint8)
|
||||
for eachRow in alpha:
|
||||
lineWidth=0
|
||||
print("/* -%d- */" % (cnt), file=o)
|
||||
|
||||
bitsArr = np.unpackbits(eachRow.astype(np.uint8))
|
||||
|
||||
# generate indexes for MSB bit quadruplet every byte
|
||||
idx = np.arange(0, np.size(bitsArr), 8)
|
||||
idx=np.reshape(np.column_stack(
|
||||
(np.column_stack((idx+0, idx+1)), np.column_stack((idx+2, idx+3)))),
|
||||
(1,-1)),
|
||||
|
||||
# extraction + endianness conversion
|
||||
packedBytes = RevBitQuadPerByte(np.packbits(bitsArr[idx]))
|
||||
|
||||
for elt in packedBytes:
|
||||
if lineWidth % WIDTH_ALPHA == (WIDTH_ALPHA - 1):
|
||||
print("0x%02x," %(elt) ,file=o)
|
||||
else:
|
||||
print("0x%02x" %(elt), end =", ",file=o)
|
||||
lineWidth+=1
|
||||
cnt+=1
|
||||
print('',file=o)
|
||||
print('};\r\n', file=o)
|
||||
|
||||
|
||||
# Gray8 channel array
|
||||
if args.format == 'gray8' or args.format == 'all':
|
||||
|
||||
R = (data[...,0]).astype(np.uint16)
|
||||
G = (data[...,1]).astype(np.uint16)
|
||||
B = (data[...,2]).astype(np.uint16)
|
||||
# merge
|
||||
RGB = np.rint((R + G + B)/3).astype(np.uint8)
|
||||
|
||||
print('',file=o)
|
||||
print('ARM_SECTION(\"arm2d.asset.c_bmp%sGRAY8\")' % (arr_name), file=o)
|
||||
print('const uint8_t c_bmp%sGRAY8[%d*%d] = {' % (arr_name, row, col), file=o)
|
||||
cnt = 0
|
||||
for eachRow in RGB:
|
||||
lineWidth=0
|
||||
print("/* -%d- */" % (cnt), file=o)
|
||||
for eachPix in eachRow:
|
||||
if lineWidth % WIDTH_GRAY8 == (WIDTH_GRAY8 - 1):
|
||||
print("0x%02x," %(eachPix) ,file=o)
|
||||
else:
|
||||
print("0x%02x" %(eachPix), end =", ", file=o)
|
||||
lineWidth+=1
|
||||
print('',file=o)
|
||||
cnt+=1
|
||||
print('};', file=o)
|
||||
buffStr='pchBuffer'
|
||||
typStr='uint8_t'
|
||||
|
||||
# RGB565 channel array
|
||||
if args.format == 'rgb565' or args.format == 'all':
|
||||
R = (data[...,0]>>3).astype(np.uint16) << 11
|
||||
G = (data[...,1]>>2).astype(np.uint16) << 5
|
||||
B = (data[...,2]>>3).astype(np.uint16)
|
||||
# merge
|
||||
RGB = R | G | B
|
||||
|
||||
print('',file=o)
|
||||
print('ARM_SECTION(\"arm2d.asset.c_bmp%sRGB565\")' % (arr_name), file=o)
|
||||
print('const uint16_t c_bmp%sRGB565[%d*%d] = {' % (arr_name, row, col), file=o)
|
||||
cnt = 0
|
||||
for eachRow in RGB:
|
||||
lineWidth=0
|
||||
print("/* -%d- */" % (cnt), file=o)
|
||||
for eachPix in eachRow:
|
||||
if lineWidth % WIDTH_RGB565 == (WIDTH_RGB565 - 1):
|
||||
print("0x%04x," %(eachPix) ,file=o)
|
||||
else:
|
||||
print("0x%04x" %(eachPix), end =", ", file=o)
|
||||
lineWidth+=1
|
||||
print('',file=o)
|
||||
cnt+=1
|
||||
print('};', file=o)
|
||||
buffStr='phwBuffer'
|
||||
typStr='uint16_t'
|
||||
|
||||
# Lossless Compressed Mask (lmsk)
|
||||
if (args.format == 'lmsk') or (args.format == 'all'):
|
||||
if not LMSK_AVAILABLE:
|
||||
print("Warning: LMSK compression library not available, skipping LMSK format", file=sys.stderr)
|
||||
else:
|
||||
# Detect alpha presence robustly
|
||||
has_alpha = ("A" in origin_image.mode) or (origin_image.mode == "P" and "transparency" in origin_image.info)
|
||||
|
||||
if has_alpha:
|
||||
img = origin_image.convert("RGBA")
|
||||
alpha = img.getchannel("A")
|
||||
mask_raw = alpha.tobytes()
|
||||
else:
|
||||
img = origin_image.convert("L")
|
||||
mask_raw = img.tobytes()
|
||||
|
||||
mask_compressed = encode_lmsk(mask_raw,
|
||||
width=img.width,
|
||||
height=img.height,
|
||||
raw=False,
|
||||
no_gradient=args.no_gradient,
|
||||
tolerant=args.gradient_tolerant,
|
||||
alpha_bits=args.alpha_bits)
|
||||
|
||||
print('',file=o)
|
||||
print('extern const uint8_t c_lmsk%s[%d];\n' % (arr_name, len(mask_compressed)), file=o)
|
||||
print('ARM_ALIGN(4) ARM_SECTION(\"arm2d.asset.c_lmsk%s\")' % (arr_name), file=o)
|
||||
print('const uint8_t c_lmsk%s[%d] = {' % (arr_name, len(mask_compressed)), file=o)
|
||||
|
||||
for i in range(0, len(mask_compressed), 16):
|
||||
chunk = mask_compressed[i:i+16]
|
||||
|
||||
hex_strings = [f"0x{b:02x}" for b in chunk]
|
||||
|
||||
line = ", ".join(hex_strings)
|
||||
|
||||
o.write(" ")
|
||||
o.write(line)
|
||||
o.write(",\n")
|
||||
|
||||
print('};', file=o)
|
||||
|
||||
# zhRGB565 compressed format
|
||||
if (args.format == 'zhRGB565') or (args.format == 'all'):
|
||||
if not ZHRGB565_AVAILABLE:
|
||||
print("Warning: zhRGB565 compression library not available, skipping zhRGB565 format", file=sys.stderr)
|
||||
else:
|
||||
# Convert to RGB565 first
|
||||
R = (data[...,0]>>3).astype(np.uint16) << 11
|
||||
G = (data[...,1]>>2).astype(np.uint16) << 5
|
||||
B = (data[...,2]>>3).astype(np.uint16)
|
||||
RGB = R | G | B
|
||||
|
||||
# Use RLE+DIFF compression for better gradient compression
|
||||
compressed_data, compressed_size, compression_ratio = encode_rgb565_rle_diff(RGB.flatten(), row, col)
|
||||
compression_method = "RLE+DIFF"
|
||||
|
||||
if compressed_data is not None:
|
||||
print('',file=o)
|
||||
print('/* ============================================ */', file=o)
|
||||
print('/* zhRGB565 compressed data (%s) */' % compression_method, file=o)
|
||||
print('/* Original size: %d bytes */' % (row * col * 2), file=o)
|
||||
print('/* Compressed size: %d bytes */' % (compressed_size * 2), file=o)
|
||||
print('/* Compression ratio: %.2f%% */' % compression_ratio, file=o)
|
||||
print('/* ============================================ */', file=o)
|
||||
print('',file=o)
|
||||
|
||||
# Generate compressed C array
|
||||
c_code = generate_rle_c_array(compressed_data, compressed_size, row, col, compression_ratio, inputfile, arr_name)
|
||||
|
||||
# Write the compressed data (extract just the array part)
|
||||
lines = c_code.split('\n')
|
||||
in_array = False
|
||||
for line in lines:
|
||||
if 'const uint16_t' in line and '[' in line:
|
||||
in_array = True
|
||||
# Add ARM_SECTION directive and extern declaration for zhRGB565
|
||||
# Extract the original array name and convert to ARM format
|
||||
import re
|
||||
match = re.search(r'const uint16_t (\w+)\[(.*?)\]', line)
|
||||
|
||||
if match:
|
||||
original_name = match.group(1)
|
||||
# Convert to ARM format: c_zhRGB565_ + name
|
||||
arm_name = f'c_zhrgb{arr_name}'
|
||||
array_size = match.group(2) if match.group(2) else ''
|
||||
# Generate extern declaration
|
||||
extern_decl = f"extern const uint16_t {arm_name}[{array_size}];"
|
||||
print(extern_decl, file=o)
|
||||
print('ARM_SECTION("arm2d.asset.c_zhrgb%s")' % (arr_name), file=o)
|
||||
# Generate the actual array definition with modified name
|
||||
modified_line = line.replace(original_name, arm_name)
|
||||
print(modified_line, file=o)
|
||||
|
||||
continue
|
||||
elif line.strip() == '};':
|
||||
if in_array:
|
||||
print(line, file=o)
|
||||
break
|
||||
elif in_array:
|
||||
print(line, file=o)
|
||||
else:
|
||||
print("Warning: RLE compression failed for zhRGB565 format", file=sys.stderr)
|
||||
|
||||
|
||||
|
||||
if args.format == 'rgb32' or args.format == 'all':
|
||||
R = data[...,0].astype(np.uint32) << 16
|
||||
G = data[...,1].astype(np.uint32) << 8
|
||||
B = data[...,2].astype(np.uint32)
|
||||
if mode == "RGBA":
|
||||
A = data[...,3].astype(np.uint32) << 24
|
||||
else:
|
||||
# alpha chanel forced to 0xFF
|
||||
A = 0xff << 24
|
||||
# merge
|
||||
RGB = R | G | B | A
|
||||
|
||||
print('',file=o)
|
||||
|
||||
if mode == "RGBA":
|
||||
print('ARM_SECTION(\"arm2d.asset.c_bmp%sCCCA8888\")' % (arr_name), file=o)
|
||||
print('const uint32_t c_bmp%sCCCA8888[%d*%d] = {' % (arr_name, row, col), file=o)
|
||||
else:
|
||||
print('ARM_SECTION(\"arm2d.asset.c_bmp%sCCCN888\")' % (arr_name), file=o)
|
||||
print('const uint32_t c_bmp%sCCCN888[%d*%d]= {' % (arr_name, row, col), file=o)
|
||||
|
||||
cnt = 0
|
||||
for eachRow in RGB:
|
||||
lineWidth=0
|
||||
print("/* -%d- */" % (cnt), file=o)
|
||||
for eachPix in eachRow:
|
||||
if lineWidth % WIDTH_RGB32 == (WIDTH_RGB32 - 1):
|
||||
print("0x%08x," %(eachPix) ,file=o)
|
||||
else:
|
||||
print("0x%08x" %(eachPix), end =", ", file=o)
|
||||
lineWidth+=1
|
||||
print('',file=o)
|
||||
cnt+=1
|
||||
print('};', file=o)
|
||||
buffStr='pwBuffer'
|
||||
typStr='uint32_t'
|
||||
|
||||
# insert tail
|
||||
if args.format == 'gray8' or args.format == 'all':
|
||||
buffStr='pchBuffer'
|
||||
typStr='uint8_t'
|
||||
print(tailDataGRAY8.format(arr_name, str(row), str(col), "."+buffStr+" = ("+typStr+"*)"), file=o)
|
||||
|
||||
if args.format == 'rgb565' or args.format == 'all':
|
||||
buffStr='phwBuffer'
|
||||
typStr='uint16_t'
|
||||
print(tailDataRGB565.format(arr_name, str(row), str(col), "."+buffStr+" = ("+typStr+"*)"), file=o)
|
||||
|
||||
|
||||
|
||||
if args.format == 'rgb32' or args.format == 'all':
|
||||
buffStr='pwBuffer'
|
||||
typStr='uint32_t'
|
||||
if mode == "RGBA":
|
||||
print(tailDataRGBA8888.format(arr_name, str(row), str(col), "."+buffStr+" = ("+typStr+"*)"), file=o)
|
||||
print(tailAlpha2.format(arr_name, str(row), str(col)), file=o)
|
||||
else :
|
||||
print(tailDataRGB888.format(arr_name, str(row), str(col), "."+buffStr+" = ("+typStr+"*)"), file=o)
|
||||
|
||||
|
||||
if mode == "RGBA":
|
||||
if args.format == 'all' or args.format == 'mask':
|
||||
print(tailAlpha.format(arr_name, str(row), str(col)), file=o)
|
||||
|
||||
if args.a1 or args.format == 'all' or args.format == 'mask':
|
||||
print(tail1BitAlpha.format(arr_name, str(row), str(col)), file=o)
|
||||
|
||||
if args.a2 or args.format == 'all' or args.format == 'mask':
|
||||
print(tail2BitAlpha.format(arr_name, str(row), str(col)), file=o)
|
||||
|
||||
if args.a4 or args.format == 'all' or args.format == 'mask':
|
||||
print(tail4BitAlpha.format(arr_name, str(row), str(col)), file=o)
|
||||
|
||||
|
||||
print(tail.format(arr_name, str(row), str(col)), file=o)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# *************************************************************************************************
|
||||
# Arm 2D project
|
||||
# @file jinja2c.py
|
||||
# @brief Jinja Launcher for arm-2d code templates
|
||||
#
|
||||
# *************************************************************************************************
|
||||
#
|
||||
# * Copyright (C) 2010-2024 ARM Limited or its affiliates. All rights reserved.
|
||||
# *
|
||||
# * SPDX-License-Identifier: Apache-2.0
|
||||
# *
|
||||
# * Licensed under the Apache License, Version 2.0 (the License); you may
|
||||
# * not use this file except in compliance with the License.
|
||||
# * You may obtain a copy of the License at
|
||||
# *
|
||||
# * www.apache.org/licenses/LICENSE-2.0
|
||||
# *
|
||||
# * Unless required by applicable law or agreed to in writing, software
|
||||
# * distributed under the License is distributed on an AS IS BASIS, WITHOUT
|
||||
# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# * See the License for the specific language governing permissions and
|
||||
# * limitations under the License.
|
||||
# */
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
def main(argv):
|
||||
|
||||
# Parse the input
|
||||
parser = argparse.ArgumentParser(description='Jinja Launcher for arm-2d code templates (v1.1.0)')
|
||||
|
||||
parser.add_argument("-i", "--input", type = str, help="Path of the input jinja2 template file", required=False)
|
||||
parser.add_argument("-var", "--variable", type = str, help="an optional path of the JSON file containing jinja2 template variables", required=False)
|
||||
parser.add_argument("-f", "--folder", type = str, help="an optional path for specify the working folder", required=False)
|
||||
parser.add_argument("-out", "--output_folder", type = str, help="an optional path for oputput folder", required=False)
|
||||
parser.add_argument("-o", "--output", type = str, help="an optional path of the output file, if you ignore this argument and the input template ends with '.jinja', this tool will remove it and use the rest part as the output file name.", required=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.input == None or args.input == "" :
|
||||
parser.print_help()
|
||||
exit(1)
|
||||
|
||||
|
||||
if args.folder == None or args.folder == "" :
|
||||
args.folder = "../Library/jinja"
|
||||
|
||||
if not os.path.isdir(args.folder):
|
||||
print(f"Error: The folder {args.folder} does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
output_file = args.output
|
||||
if output_file is None:
|
||||
if args.input.endswith('.jinja'):
|
||||
output_file = args.input[:-6] # remove ".jinja" extension
|
||||
|
||||
if args.output_folder == None or args.output_folder == "" :
|
||||
if output_file.endswith('.c') :
|
||||
args.output_folder = "../Source"
|
||||
if output_file.endswith('.h') :
|
||||
args.output_folder = "../Include"
|
||||
|
||||
output_file = os.path.join(args.folder, args.output_folder, output_file)
|
||||
else:
|
||||
output_file = None
|
||||
|
||||
if not os.path.isdir(os.path.join(args.folder, args.output_folder)):
|
||||
print(f"Error: The folder {os.path.join(args.folder, args.output_folder)} does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
#Template environment
|
||||
env = Environment(loader=FileSystemLoader(args.folder))
|
||||
|
||||
# Load the template file
|
||||
if args.input and not os.path.isfile(os.path.join(args.folder, args.input)):
|
||||
print(f"Error: The template file {os.path.join(args.folder, args.input)} does not exist.")
|
||||
sys.exit(1)
|
||||
|
||||
template = env.get_template(args.input)
|
||||
|
||||
# Load variables
|
||||
|
||||
variables = {}
|
||||
if args.variable is not None:
|
||||
if not os.path.isfile(os.path.join(args.folder, args.variable)):
|
||||
print(f"Error: The jason file {os.path.join(args.folder, args.variable)} does not exist.")
|
||||
sys.exit(1)
|
||||
with open(os.path.join(args.folder, args.variable), 'r') as f:
|
||||
variables = json.load(f)
|
||||
|
||||
# generate output
|
||||
output = template.render(variables)
|
||||
|
||||
if output_file is not None:
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(output)
|
||||
else:
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv[1:])
|
||||
@@ -0,0 +1,585 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# *************************************************************************************************
|
||||
# Arm 2D project
|
||||
# @file ttf2c.py
|
||||
# @brief TrueTypeFont to C-array converter
|
||||
#
|
||||
# *************************************************************************************************
|
||||
#
|
||||
# * Copyright (C) 2025 ARM Limited or its affiliates. All rights reserved.
|
||||
# *
|
||||
# * SPDX-License-Identifier: Apache-2.0
|
||||
# *
|
||||
# * Licensed under the Apache License, Version 2.0 (the License); you may
|
||||
# * not use this file except in compliance with the License.
|
||||
# * You may obtain a copy of the License at
|
||||
# *
|
||||
# * www.apache.org/licenses/LICENSE-2.0
|
||||
# *
|
||||
# * Unless required by applicable law or agreed to in writing, software
|
||||
# * distributed under the License is distributed on an AS IS BASIS, WITHOUT
|
||||
# * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# * See the License for the specific language governing permissions and
|
||||
# * limitations under the License.
|
||||
# */
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import freetype
|
||||
import numpy as np
|
||||
import math
|
||||
import binascii
|
||||
import tempfile
|
||||
|
||||
c_head_string="""
|
||||
|
||||
/* generated with ttf2c.py (v2.4.1) */
|
||||
|
||||
#include "arm_2d_helper.h"
|
||||
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic push
|
||||
# pragma clang diagnostic ignored "-Wunknown-warning-option"
|
||||
# pragma clang diagnostic ignored "-Wreserved-identifier"
|
||||
# pragma clang diagnostic ignored "-Wmissing-variable-declarations"
|
||||
# pragma clang diagnostic ignored "-Wcast-qual"
|
||||
# pragma clang diagnostic ignored "-Wmissing-declarations"
|
||||
# pragma clang diagnostic ignored "-Wgnu-variable-sized-type-not-at-end"
|
||||
# pragma clang diagnostic ignored "-Wimplicit-int-conversion"
|
||||
# pragma clang diagnostic ignored "-Wdeclaration-after-statement"
|
||||
# pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments"
|
||||
# pragma clang diagnostic ignored "-Wincompatible-pointer-types-discards-qualifiers"
|
||||
# pragma clang diagnostic ignored "-Wpadded"
|
||||
# pragma clang diagnostic ignored "-Wunused-parameter"
|
||||
# pragma clang diagnostic ignored "-Wunused-variable"
|
||||
# pragma clang diagnostic ignored "-Wsign-conversion"
|
||||
#elif defined(__IS_COMPILER_GCC__)
|
||||
# pragma GCC diagnostic ignored "-Wdiscarded-qualifiers"
|
||||
# pragma GCC diagnostic ignored "-Wunused-variable"
|
||||
#elif defined(__IS_COMPILER_ARM_COMPILER_5__)
|
||||
# pragma diag_suppress=1296,1,64,177
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
uint16_t hwIndex;
|
||||
arm_2d_size_t tCharSize;
|
||||
int16_t iAdvance;
|
||||
int16_t iBearingX;
|
||||
int16_t iBearingY;
|
||||
uint8_t chCodeLength;
|
||||
uint8_t chUTF8[4];
|
||||
} const __ttf_char_descriptor_t;
|
||||
"""
|
||||
|
||||
|
||||
c_body_string="""
|
||||
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tileUTF8{0}A{5}Mask\")
|
||||
static const arm_2d_tile_t c_tileUTF8{0}A{5}Mask = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {4},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_{5}BIT,
|
||||
}},
|
||||
}},
|
||||
.pchBuffer = (uint8_t *)c_bmpUTF8{0}A{5}Font,
|
||||
}};
|
||||
|
||||
#define __UTF8_FONT_SIZE_{5}__
|
||||
|
||||
|
||||
|
||||
static
|
||||
IMPL_FONT_GET_CHAR_DESCRIPTOR(__utf8_a{5}_font_get_char_descriptor)
|
||||
{{
|
||||
assert(NULL != ptFont);
|
||||
assert(NULL != ptDescriptor);
|
||||
assert(NULL != pchCharCode);
|
||||
|
||||
arm_2d_user_font_t *ptThis = (arm_2d_user_font_t *)ptFont;
|
||||
ARM_2D_UNUSED(ptThis);
|
||||
|
||||
memset(ptDescriptor, 0, sizeof(arm_2d_char_descriptor_t));
|
||||
|
||||
ptDescriptor->tileChar.ptParent = (arm_2d_tile_t *)&ptFont->tileFont;
|
||||
ptDescriptor->tileChar.tInfo.bDerivedResource = true;
|
||||
|
||||
/* use the white space as the default char */
|
||||
__ttf_char_descriptor_t *ptUTF8Char =
|
||||
(__ttf_char_descriptor_t *)&c_tUTF8{0}LookUpTableA{5}[dimof(c_tUTF8{0}LookUpTableA{5})-1];
|
||||
|
||||
arm_foreach(__ttf_char_descriptor_t, c_tUTF8{0}LookUpTableA{5}, ptChar) {{
|
||||
if (0 == strncmp( (char *)pchCharCode,
|
||||
(char *)ptChar->chUTF8,
|
||||
ptChar->chCodeLength)) {{
|
||||
/* found the UTF8 char */
|
||||
ptUTF8Char = ptChar;
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
|
||||
ptDescriptor->chCodeLength = ptUTF8Char->chCodeLength;
|
||||
ptDescriptor->tileChar.tRegion.tSize = ptUTF8Char->tCharSize;
|
||||
ptDescriptor->tileChar.tRegion.tLocation.iY = (int16_t)ptUTF8Char->hwIndex;
|
||||
|
||||
ptDescriptor->iAdvance = ptUTF8Char->iAdvance;
|
||||
ptDescriptor->iBearingX= ptUTF8Char->iBearingX;
|
||||
ptDescriptor->iBearingY= ptUTF8Char->iBearingY;
|
||||
|
||||
return ptDescriptor;
|
||||
}}
|
||||
|
||||
ARM_SECTION(\"arm2d.asset.FONT.ARM_2D_FONT_{0}_A{5}\")
|
||||
const
|
||||
struct {{
|
||||
implement(arm_2d_user_font_t);
|
||||
arm_2d_char_idx_t tUTF8Table;
|
||||
}} ARM_2D_FONT_{0}_A{5} = {{
|
||||
|
||||
.use_as__arm_2d_user_font_t = {{
|
||||
.use_as__arm_2d_font_t = {{
|
||||
.tileFont = impl_child_tile(
|
||||
c_tileUTF8{0}A{5}Mask,
|
||||
0, /* x offset */
|
||||
0, /* y offset */
|
||||
{1}, /* width */
|
||||
{4} /* height */
|
||||
),
|
||||
.tCharSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
.nCount = {3}, //!< Character count
|
||||
.fnGetCharDescriptor = &__utf8_a{5}_font_get_char_descriptor,
|
||||
.fnDrawChar = &__arm_2d_lcd_text_default_a{5}_font_draw_char,
|
||||
}},
|
||||
.hwCount = 1,
|
||||
.hwDefaultCharIndex = 1, /* tBlank */
|
||||
}},
|
||||
|
||||
.tUTF8Table = {{
|
||||
.hwCount = {3},
|
||||
.hwOffset = 0,
|
||||
}},
|
||||
}};
|
||||
|
||||
#undef __UTF8_FONT_SIZE_{5}__
|
||||
"""
|
||||
|
||||
|
||||
c_body_string_ascii="""
|
||||
|
||||
|
||||
ARM_SECTION(\"arm2d.tile.c_tileUTF8{0}A{5}Mask\")
|
||||
static const arm_2d_tile_t c_tileUTF8{0}A{5}Mask = {{
|
||||
.tRegion = {{
|
||||
.tSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {4},
|
||||
}},
|
||||
}},
|
||||
.tInfo = {{
|
||||
.bIsRoot = true,
|
||||
.bHasEnforcedColour = true,
|
||||
.tColourInfo = {{
|
||||
.chScheme = ARM_2D_COLOUR_{5}BIT,
|
||||
}},
|
||||
}},
|
||||
.pchBuffer = (uint8_t *)c_bmpUTF8{0}A{5}Font,
|
||||
}};
|
||||
|
||||
#define __UTF8_FONT_SIZE_{5}__
|
||||
|
||||
|
||||
|
||||
static
|
||||
IMPL_FONT_GET_CHAR_DESCRIPTOR(__utf8_a{5}_font_get_char_descriptor)
|
||||
{{
|
||||
assert(NULL != ptFont);
|
||||
assert(NULL != ptDescriptor);
|
||||
assert(NULL != pchCharCode);
|
||||
|
||||
arm_2d_user_font_t *ptThis = (arm_2d_user_font_t *)ptFont;
|
||||
ARM_2D_UNUSED(ptThis);
|
||||
|
||||
memset(ptDescriptor, 0, sizeof(arm_2d_char_descriptor_t));
|
||||
|
||||
ptDescriptor->tileChar.ptParent = (arm_2d_tile_t *)&ptFont->tileFont;
|
||||
ptDescriptor->tileChar.tInfo.bDerivedResource = true;
|
||||
|
||||
/* use the white space as the default char */
|
||||
__ttf_char_descriptor_t *ptUTF8Char = NULL;
|
||||
|
||||
if (pchCharCode[0] > 0x20 && pchCharCode[0] <= 0x7e) {{
|
||||
ptUTF8Char = (__ttf_char_descriptor_t *)
|
||||
&c_tUTF8{0}LookUpTableA{5}[
|
||||
pchCharCode[0] - c_tUTF8{0}LookUpTableA{5}[0].chUTF8[0]];
|
||||
}} else {{
|
||||
/* use the white space as the default char */
|
||||
ptUTF8Char = (__ttf_char_descriptor_t *)
|
||||
&c_tUTF8{0}LookUpTableA{5}[dimof(c_tUTF8{0}LookUpTableA{5})-1];
|
||||
}}
|
||||
|
||||
ptDescriptor->chCodeLength = ptUTF8Char->chCodeLength;
|
||||
ptDescriptor->tileChar.tRegion.tSize = ptUTF8Char->tCharSize;
|
||||
ptDescriptor->tileChar.tRegion.tLocation.iY = (int16_t)ptUTF8Char->hwIndex;
|
||||
|
||||
ptDescriptor->iAdvance = ptUTF8Char->iAdvance;
|
||||
ptDescriptor->iBearingX= ptUTF8Char->iBearingX;
|
||||
ptDescriptor->iBearingY= ptUTF8Char->iBearingY;
|
||||
|
||||
return ptDescriptor;
|
||||
}}
|
||||
|
||||
ARM_SECTION(\"arm2d.asset.FONT.ARM_2D_FONT_{0}_A{5}\")
|
||||
const
|
||||
struct {{
|
||||
implement(arm_2d_user_font_t);
|
||||
arm_2d_char_idx_t tUTF8Table;
|
||||
}} ARM_2D_FONT_{0}_A{5} = {{
|
||||
|
||||
.use_as__arm_2d_user_font_t = {{
|
||||
.use_as__arm_2d_font_t = {{
|
||||
.tileFont = impl_child_tile(
|
||||
c_tileUTF8{0}A{5}Mask,
|
||||
0, /* x offset */
|
||||
0, /* y offset */
|
||||
{1}, /* width */
|
||||
{4} /* height */
|
||||
),
|
||||
.tCharSize = {{
|
||||
.iWidth = {1},
|
||||
.iHeight = {2},
|
||||
}},
|
||||
.nCount = {3}, //!< Character count
|
||||
.fnGetCharDescriptor = &__utf8_a{5}_font_get_char_descriptor,
|
||||
.fnDrawChar = &__arm_2d_lcd_text_default_a{5}_font_draw_char,
|
||||
}},
|
||||
.hwCount = 1,
|
||||
.hwDefaultCharIndex = 1, /* tBlank */
|
||||
}},
|
||||
|
||||
.tUTF8Table = {{
|
||||
.hwCount = {3},
|
||||
.hwOffset = 0,
|
||||
}},
|
||||
}};
|
||||
|
||||
#undef __UTF8_FONT_SIZE_{5}__
|
||||
"""
|
||||
|
||||
|
||||
c_tail_string="""
|
||||
|
||||
#if defined(__clang__)
|
||||
# pragma clang diagnostic pop
|
||||
#elif defined(__IS_COMPILER_ARM_COMPILER_5__)
|
||||
# pragma diag_warning=1296
|
||||
#endif
|
||||
"""
|
||||
|
||||
def generate_glyphs_data(input_file, text, pixel_size, font_bit_size, font_index):
|
||||
face = freetype.Face(input_file, index=font_index)
|
||||
face.set_pixel_sizes(0, pixel_size)
|
||||
|
||||
glyphs_data = []
|
||||
current_index = 0
|
||||
|
||||
width_max = 0
|
||||
height_max = 0
|
||||
|
||||
for char in set(text):
|
||||
# Get the glyph bitmap
|
||||
face.load_char(char)
|
||||
bitmap = face.glyph.bitmap
|
||||
utf8_encoding = char.encode('utf-8')
|
||||
|
||||
if len(bitmap.buffer) == 0:
|
||||
continue
|
||||
if list(utf8_encoding) == [0xef, 0xbb, 0xbf]:
|
||||
continue
|
||||
|
||||
width_max = max(bitmap.width, width_max)
|
||||
height_max = max(bitmap.rows, height_max)
|
||||
|
||||
width_max += 2
|
||||
height_max += 2
|
||||
|
||||
for char in sorted(set(text)):
|
||||
face.load_char(char)
|
||||
bitmap = face.glyph.bitmap
|
||||
utf8_encoding = char.encode('utf-8')
|
||||
|
||||
if len(bitmap.buffer) == 0:
|
||||
continue
|
||||
if list(utf8_encoding) == [0xef, 0xbb, 0xbf]:
|
||||
continue
|
||||
|
||||
# update advance, bearing x and bearing y as we added the 1px boarder around each glyph.
|
||||
advance_width = math.ceil(face.glyph.advance.x / 64.0) + 1
|
||||
bearing_x = face.glyph.bitmap_left + 1
|
||||
bearing_y = face.glyph.bitmap_top + 1
|
||||
width = bitmap.width
|
||||
height = bitmap.rows
|
||||
pitch = bitmap.pitch
|
||||
|
||||
if bitmap.pixel_mode == freetype.FT_PIXEL_MODE_MONO:
|
||||
buffer = np.frombuffer(bytes(bitmap.buffer), dtype=np.uint8)
|
||||
bits_per_row = pitch * 8
|
||||
bitmap_array = np.unpackbits(buffer).reshape((height, bits_per_row))[:, :width]
|
||||
bitmap_array = bitmap_array.astype(np.uint8) * 255
|
||||
|
||||
elif bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY2:
|
||||
buffer = np.frombuffer(bytes(bitmap.buffer), dtype=np.uint8)
|
||||
bits_per_row = pitch * 4
|
||||
bitmap_array = np.unpackbits(buffer).reshape((height, bits_per_row))[:, :width * 2]
|
||||
bitmap_array = (bitmap_array.reshape((height, width * 2)) * 85).astype(np.uint8)
|
||||
|
||||
elif bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY4:
|
||||
buffer = np.frombuffer(bytes(bitmap.buffer), dtype=np.uint8)
|
||||
bits_per_row = pitch * 2
|
||||
bitmap_array = np.unpackbits(buffer).reshape((height, bits_per_row))[:, :width * 4]
|
||||
bitmap_array = (bitmap_array.reshape((height, width * 2)) * 17).astype(np.uint8)
|
||||
|
||||
elif bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY:
|
||||
bitmap_array = np.array(bitmap.buffer, dtype=np.uint8).reshape((height, width))
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported pixel mode: {bitmap.pixel_mode}")
|
||||
continue
|
||||
|
||||
if width < width_max:
|
||||
if (width_max - width) > 1:
|
||||
padding = ((0, 0), (1, width_max - width - 1))
|
||||
else:
|
||||
padding = ((0, 0), (0, width_max - width))
|
||||
bitmap_array = np.pad(bitmap_array, padding, 'constant')
|
||||
|
||||
padding = ((1, 1), (0, 0))
|
||||
bitmap_array = np.pad(bitmap_array, padding, 'constant')
|
||||
|
||||
height += 2
|
||||
|
||||
char_index_advance = len(bitmap_array.flatten());
|
||||
|
||||
if font_bit_size == 4:
|
||||
def RevBitQuadPerByte(byteArr):
|
||||
return ((byteArr & 0x0f) << 4) | ((byteArr & 0xf0) >> 4)
|
||||
|
||||
# temporary array with 2x reduced width & pad
|
||||
(r, c) = np.shape(bitmap_array)
|
||||
tmp = np.empty((0, int((c+1)/2)), dtype=np.uint8)
|
||||
|
||||
for cur in bitmap_array:
|
||||
bitsArr = np.unpackbits(cur.astype(np.uint8))
|
||||
# generate indexes for MSB bit quadruplet every byte
|
||||
idx = np.arange(0, np.size(bitsArr), 8)
|
||||
idx = np.reshape(np.column_stack(
|
||||
(np.column_stack((idx+0, idx+1)), np.column_stack((idx+2, idx+3)))),
|
||||
(1,-1)),
|
||||
|
||||
# extraction + endianness conversion appended in temp array
|
||||
# packbits is taking care of padding
|
||||
tmp = np.vstack([tmp, RevBitQuadPerByte(np.packbits(bitsArr[idx]))])
|
||||
|
||||
bitmap_array = tmp
|
||||
|
||||
elif font_bit_size == 2:
|
||||
def RevBitPairPerByte(byteArr):
|
||||
return ((byteArr & 0x03) << 6) | ((byteArr & 0xc0) >> 6) | ((byteArr & 0x30) >> 2 ) | ((byteArr & 0x0c) << 2)
|
||||
|
||||
# temporary array with 4x reduced width & pad
|
||||
(r, c) = np.shape(bitmap_array)
|
||||
tmp = np.empty((0, int((c+3)/4)), dtype=np.uint8)
|
||||
|
||||
for cur in bitmap_array:
|
||||
bitsArr = np.unpackbits(cur.astype(np.uint8))
|
||||
|
||||
# generate indexes for MSB bit pair every byte
|
||||
idx = np.arange(0, np.size(bitsArr), 8)
|
||||
idx = np.reshape(np.column_stack((idx+0, idx+1)), (1,-1))
|
||||
|
||||
# extraction + endianness conversion appended in temp array
|
||||
tmp = np.vstack([tmp, RevBitPairPerByte(np.packbits(bitsArr[idx]))])
|
||||
|
||||
bitmap_array = tmp
|
||||
|
||||
elif font_bit_size == 1:
|
||||
def RevBitPerByte(byteArr):
|
||||
return ((byteArr & 0x01) << 7) | ((byteArr & 0x80) >> 7) | \
|
||||
((byteArr & 0x02) << 5) | ((byteArr & 0x40) >> 5) | \
|
||||
((byteArr & 0x04) << 3) | ((byteArr & 0x20) >> 3) | \
|
||||
((byteArr & 0x08) << 1) | ((byteArr & 0x10) >> 1)
|
||||
|
||||
# temporary array with 8x reduced width & pad
|
||||
(r, c) = np.shape(bitmap_array)
|
||||
tmp = np.empty((0, int((c+7)/8)), dtype=np.uint8)
|
||||
|
||||
for cur in bitmap_array:
|
||||
bitsArr = np.unpackbits(cur.astype(np.uint8))
|
||||
|
||||
# generate indexes for MSB bit every byte
|
||||
idx = np.arange(0, np.size(bitsArr), 8)
|
||||
|
||||
# extraction + endianness conversion
|
||||
tmp = np.vstack([tmp, RevBitPerByte(np.packbits(bitsArr[idx]))])
|
||||
|
||||
bitmap_array = tmp
|
||||
|
||||
char_mask_array = bitmap_array.flatten()
|
||||
|
||||
glyphs_data.append((char, char_mask_array, width + 2, height, current_index, advance_width, bearing_x, bearing_y, utf8_encoding))
|
||||
|
||||
current_index += char_index_advance
|
||||
|
||||
return glyphs_data, width_max, height_max
|
||||
|
||||
|
||||
def utf8_to_c_array(utf8_bytes):
|
||||
return '{' + ', '.join([f'0x{byte:02x}' for byte in utf8_bytes]) + '}'
|
||||
|
||||
def write_c_code(glyphs_data, output_file, name, char_max_width, char_max_height, font_bit_size, ascii_mode):
|
||||
|
||||
with open(output_file, "a") as f:
|
||||
|
||||
print("ARM_SECTION(\"arm2d.asset.FONT.c_bmpUTF8{0}A{1}Font\")\nconst uint8_t c_bmpUTF8{0}A{1}Font[] = {{\n"
|
||||
.format(name, font_bit_size),
|
||||
file=f)
|
||||
|
||||
for char, data, width, height, index, advance_width, bearing_x, bearing_y, utf8_encoding in glyphs_data:
|
||||
utf8_c_array = utf8_to_c_array(utf8_encoding)
|
||||
f.write(f"\n // Glyph for character {utf8_c_array}\n")
|
||||
hex_str = binascii.hexlify(data).decode()
|
||||
|
||||
for i in range(0, len(hex_str), char_max_width*2):
|
||||
line = hex_str[i:i+char_max_width*2]
|
||||
spaced_line = ' '.join(f"0x{line[j:j+2]}," for j in range(0, len(line), 2))
|
||||
f.write(" ");
|
||||
f.write(spaced_line)
|
||||
f.write("\n");
|
||||
|
||||
f.write(" // a white space\n ");
|
||||
f.write("0x00, " * (char_max_width * char_max_height))
|
||||
f.write("\n};\n\n")
|
||||
|
||||
print("ARM_SECTION(\"arm2d.asset.FONT.c_tUTF8{0}LookUpTableA{1}\")\n const __ttf_char_descriptor_t c_tUTF8{0}LookUpTableA{1}[{2}] = {{\n"
|
||||
.format(name, font_bit_size, len(glyphs_data) + 1), file=f)
|
||||
|
||||
last_index = 0;
|
||||
last_advance = 0;
|
||||
last_height = 0;
|
||||
for char, data, width, height, index, advance_width, bearing_x, bearing_y, utf8_encoding in glyphs_data:
|
||||
utf8_c_array = utf8_to_c_array(utf8_encoding)
|
||||
last_index = index
|
||||
last_advance = advance_width
|
||||
last_height = height;
|
||||
f.write(f" {{ {round(index / char_max_width)}, {{ {width}, {height}, }}, {advance_width}, {bearing_x}, {bearing_y}, {len(utf8_encoding)}, {utf8_c_array} }},\n")
|
||||
|
||||
last_index += char_max_width * last_height
|
||||
f.write(f" {{ {round(last_index / char_max_width)}, {{ {char_max_width}, {char_max_height}, }}, {round(char_max_width / 2)}, {0}, {char_max_height}, 1, {{0x20}} }},\n")
|
||||
|
||||
last_index += char_max_width * char_max_height
|
||||
|
||||
f.write("};\n")
|
||||
|
||||
if ascii_mode == True:
|
||||
print(c_body_string_ascii.format( name,
|
||||
char_max_width,
|
||||
char_max_height,
|
||||
len(glyphs_data),
|
||||
round(last_index / char_max_width)+1,
|
||||
font_bit_size), file=f)
|
||||
else:
|
||||
print(c_body_string.format( name,
|
||||
char_max_width,
|
||||
char_max_height,
|
||||
len(glyphs_data),
|
||||
round(last_index / char_max_width)+1,
|
||||
font_bit_size), file=f)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='TrueTypeFont to C array converter (v2.4.1)')
|
||||
parser.add_argument("-i", "--input", type=str, help="Path to the TTF file", required=True)
|
||||
parser.add_argument("--index", type=int, help="The Font Index in a TTC file", required=False, default=0)
|
||||
parser.add_argument("-t", "--text", type=str, help="Path to the text file", required=False)
|
||||
parser.add_argument("-n", "--name", type=str, help="The customized UTF8 font name", required=False, default="UTF8")
|
||||
parser.add_argument("-o", "--output", type=str, help="Path to the output C file", required=True)
|
||||
parser.add_argument("-p", "--pixelsize",type=int, help="Font size in pixels", required=False, default=32)
|
||||
parser.add_argument("-s", "--fontbitsize",type=int, help="font bit size (1,2,4,8)", required=False, default=0)
|
||||
|
||||
if len(sys.argv)==1:
|
||||
parser.print_help(sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.fontbitsize not in [1, 2, 4, 8, 0]:
|
||||
print(f'Invalid alpha size={args.fontbitsize}')
|
||||
sys.exit(1)
|
||||
|
||||
ascii_mode = False
|
||||
|
||||
if args.text == None or args.text == "" :
|
||||
ascii_mode = True
|
||||
fd, args.text = tempfile.mkstemp()
|
||||
with os.fdopen(fd, 'w') as temptextfile:
|
||||
print("!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", file=temptextfile, end='')
|
||||
|
||||
with open(args.output, "w") as outputfile:
|
||||
print(c_head_string, file=outputfile)
|
||||
|
||||
if args.fontbitsize in [1, 2, 4, 8]:
|
||||
with open(args.text, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, args.fontbitsize, args.index)
|
||||
write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, args.fontbitsize, ascii_mode)
|
||||
|
||||
else:
|
||||
with open(args.text, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 1, args.index)
|
||||
write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 1, ascii_mode)
|
||||
|
||||
|
||||
with open(args.text, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 2, args.index)
|
||||
write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 2, ascii_mode)
|
||||
|
||||
|
||||
with open(args.text, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 4, args.index)
|
||||
write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 4, ascii_mode)
|
||||
|
||||
|
||||
with open(args.text, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
glyphs_data, char_max_width, char_max_height = generate_glyphs_data(args.input, text, args.pixelsize, 8, args.index)
|
||||
write_c_code(glyphs_data, args.output, args.name, char_max_width, char_max_height, 8, ascii_mode)
|
||||
|
||||
with open(args.output, "a") as outputfile:
|
||||
print(c_tail_string, file=outputfile)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,153 @@
|
||||
"""
|
||||
视频抠图转序列帧工具 - 专为ARM2D动效/B站素材设计
|
||||
========== 所有参数都在下面配置区改,不用动下面逻辑 ==========
|
||||
"""
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore") # 屏蔽无关性能警告
|
||||
import cv2
|
||||
from rembg import remove
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
import os
|
||||
|
||||
# ====================== 配置区(你直接改这里就行) ======================
|
||||
INPUT_VIDEO_PATH = r"C:\Users\11791\OneDrive\Desktop\images\girl.mp4" # 输入视频路径
|
||||
OUTPUT_DIR = r"C:\Users\11791\OneDrive\Desktop\images\output_frames" # 输出帧保存路径
|
||||
# 帧配置
|
||||
TARGET_FRAME_COUNT = 100 # 要输出的总帧数,均匀从视频里抽取
|
||||
TARGET_SIZE = (240, 240) # 输出图片尺寸(宽,高),和你LCD一致就行
|
||||
AUTO_CROP_CENTER = True # 自动居中裁剪,避免拉伸变形,建议开
|
||||
# 画质配置
|
||||
CONTRAST = 1.0 # 对比度倍数,>1提高,视频暗就往大调
|
||||
BRIGHTNESS = 10 # 亮度增加值,0~255,视频暗往大调
|
||||
ENABLE_ALPHA_MATTING = False # 开边缘优化,抠出来无锯齿
|
||||
ONLY_OUTPUT_MASK = False # 只输出黑白Alpha遮罩,做局部刷新时开
|
||||
ENABLE_REMOVE_BG = True # 是否开启抠图,关闭后直接抽帧转GIF(速度提升10倍以上)
|
||||
USE_GPU = False # 要是CUDA装好了就改成True,速度快10倍
|
||||
# GIF输出配置
|
||||
ENABLE_GIF_OUTPUT = True # 是否输出合并后的GIF
|
||||
OUTPUT_GIF_PATH = r"C:\Users\11791\OneDrive\Desktop\images\output.gif" # GIF输出路径
|
||||
GIF_FPS = 10 # GIF的播放帧率,10帧每秒就很流畅
|
||||
GIF_LOOP_FOREVER = True # GIF是否无限循环
|
||||
KEEP_FRAMES_AFTER_GIF = True # 生成GIF后是否保留单独的序列帧
|
||||
# =======================================================================
|
||||
|
||||
# 自动创建输出目录
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
# 配置校验
|
||||
if not ENABLE_REMOVE_BG and ONLY_OUTPUT_MASK:
|
||||
print("⚠️ 提示:不抠图模式下ONLY_OUTPUT_MASK选项无效,已自动关闭")
|
||||
ONLY_OUTPUT_MASK = False
|
||||
# 选择运行后端
|
||||
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] if USE_GPU else ['CPUExecutionProvider']
|
||||
|
||||
# 读取视频信息
|
||||
cap = cv2.VideoCapture(INPUT_VIDEO_PATH, cv2.CAP_FFMPEG)
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
# 计算要抽取的帧索引(均匀分布在整个视频)
|
||||
extract_indices = [int(i * total_frames / TARGET_FRAME_COUNT) for i in range(TARGET_FRAME_COUNT)]
|
||||
|
||||
print(f"=== 开始处理 ===")
|
||||
print(f"输入视频总帧数:{total_frames}")
|
||||
print(f"将抽取{len(extract_indices)}帧,输出尺寸:{TARGET_SIZE[0]}×{TARGET_SIZE[1]}")
|
||||
print(f"输出路径:{OUTPUT_DIR}\n")
|
||||
|
||||
for idx, frame_idx in enumerate(extract_indices):
|
||||
# 跳到指定帧
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print(f"跳过第{idx+1}帧:读取失败")
|
||||
continue
|
||||
|
||||
# 1. 调整对比度亮度
|
||||
frame = cv2.convertScaleAbs(frame, alpha=CONTRAST, beta=BRIGHTNESS)
|
||||
|
||||
# 2. 裁剪+缩放
|
||||
if AUTO_CROP_CENTER:
|
||||
h, w = frame.shape[:2]
|
||||
crop_size = min(w, h)
|
||||
x_start = (w - crop_size) // 2
|
||||
y_start = (h - crop_size) // 2
|
||||
frame = frame[y_start:y_start+crop_size, x_start:x_start+crop_size]
|
||||
frame = cv2.resize(frame, TARGET_SIZE)
|
||||
|
||||
# 3. 抠图/直接使用原图
|
||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
img_pil = Image.fromarray(frame_rgb)
|
||||
if ENABLE_REMOVE_BG:
|
||||
img_rembg = remove(
|
||||
img_pil,
|
||||
providers=providers,
|
||||
alpha_matting=ENABLE_ALPHA_MATTING,
|
||||
alpha_matting_discard_threshold=1e-5, # 优化矩阵计算,消除警告
|
||||
alpha_matting_shift=1e-2,
|
||||
only_mask=ONLY_OUTPUT_MASK
|
||||
)
|
||||
else:
|
||||
# 不抠图模式,直接保留原图,添加全不透明alpha通道兼容后续逻辑
|
||||
img_rembg = img_pil.convert("RGBA")
|
||||
|
||||
# 4. 保存
|
||||
if ONLY_OUTPUT_MASK:
|
||||
# 遮罩直接存灰度图
|
||||
img_out = Image.fromarray(np.array(img_rembg) * 255).convert("L")
|
||||
else:
|
||||
# 存带透明通道的RGBA图
|
||||
img_out = Image.fromarray(np.array(img_rembg))
|
||||
|
||||
output_path = os.path.join(OUTPUT_DIR, f"frame_{idx+1:02d}.png")
|
||||
img_out.save(output_path)
|
||||
print(f"已完成第{idx+1}/{TARGET_FRAME_COUNT}帧:{os.path.basename(output_path)}")
|
||||
|
||||
cap.release()
|
||||
frame_count = len(os.listdir(OUTPUT_DIR))
|
||||
print(f"\n=== 帧处理完成,共生成{frame_count}帧 ===")
|
||||
|
||||
# ========== 生成GIF ==========
|
||||
if ENABLE_GIF_OUTPUT and frame_count > 0:
|
||||
print("正在生成GIF...")
|
||||
# 读取所有帧
|
||||
frames = []
|
||||
for i in range(1, frame_count+1):
|
||||
frame_path = os.path.join(OUTPUT_DIR, f"frame_{i:02d}.png")
|
||||
if os.path.exists(frame_path):
|
||||
img = Image.open(frame_path)
|
||||
# 处理透明通道,避免GIF透明异常
|
||||
if img.mode == "RGBA":
|
||||
# 转索引色,保留透明
|
||||
alpha = img.getchannel("A")
|
||||
img = img.convert("P", palette=Image.ADAPTIVE, colors=256)
|
||||
mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0)
|
||||
img.paste(255, mask=mask)
|
||||
img.info["transparency"] = 255
|
||||
frames.append(img)
|
||||
|
||||
if len(frames) > 0:
|
||||
# 计算每帧延迟时间(毫秒)
|
||||
frame_delay = int(1000 / GIF_FPS)
|
||||
loop = 0 if GIF_LOOP_FOREVER else 1
|
||||
# 保存GIF
|
||||
frames[0].save(
|
||||
OUTPUT_GIF_PATH,
|
||||
save_all=True,
|
||||
append_images=frames[1:],
|
||||
duration=frame_delay,
|
||||
loop=loop,
|
||||
disposal=2, # 每帧播放完清除,避免透明残留
|
||||
optimize=False
|
||||
)
|
||||
print(f"GIF生成成功:{OUTPUT_GIF_PATH}")
|
||||
|
||||
# 删除单独帧(如果配置了)
|
||||
if not KEEP_FRAMES_AFTER_GIF:
|
||||
for f in os.listdir(OUTPUT_DIR):
|
||||
os.remove(os.path.join(OUTPUT_DIR, f))
|
||||
os.rmdir(OUTPUT_DIR)
|
||||
print("已清理临时序列帧文件")
|
||||
|
||||
print(f"\n=== 全部处理完成 ===")
|
||||
if KEEP_FRAMES_AFTER_GIF or not ENABLE_GIF_OUTPUT:
|
||||
print(f"序列帧保存在:{OUTPUT_DIR}")
|
||||
if ENABLE_GIF_OUTPUT:
|
||||
print(f"GIF保存在:{OUTPUT_GIF_PATH}")
|
||||
@@ -0,0 +1,135 @@
|
||||
# 素材与动画资源链路
|
||||
|
||||
## 目标
|
||||
|
||||
资源处理目标不是“能显示就行”,而是要在目标 MCU/SoC 的实际资源约束下平衡:
|
||||
|
||||
- Flash 占用
|
||||
- SRAM 占用
|
||||
- 解码速度
|
||||
- 透明/遮罩效果
|
||||
- 局部刷新
|
||||
- 工具链可维护性
|
||||
|
||||
## 常见格式
|
||||
|
||||
### 未压缩 RGB565 帧序列
|
||||
|
||||
优点:
|
||||
|
||||
- MCU 端播放最简单。
|
||||
- 不需要 decoder。
|
||||
- 可以直接构造 `arm_2d_tile_t`。
|
||||
|
||||
缺点:
|
||||
|
||||
- 极吃 Flash。
|
||||
|
||||
尺寸估算:
|
||||
|
||||
```text
|
||||
width * height * 2 * frame_count
|
||||
```
|
||||
|
||||
例如 240x240x80:
|
||||
|
||||
```text
|
||||
240 * 240 * 2 * 80 = 9.216 MB
|
||||
```
|
||||
|
||||
除非 Flash 足够,否则不要作为最终方案。
|
||||
|
||||
### 精灵图 Sprite Sheet
|
||||
|
||||
优点:
|
||||
|
||||
- PC 侧容易预览。
|
||||
- 工具链中间态方便。
|
||||
|
||||
缺点:
|
||||
|
||||
- 进入固件后通常仍然会变成未压缩像素或某种压缩格式。
|
||||
- 不等于省 Flash。
|
||||
|
||||
### Mask / Alpha
|
||||
|
||||
如果动画需要透明背景,要明确:
|
||||
|
||||
- 使用 1-bit mask?
|
||||
- 使用 A2/A4/A8 alpha?
|
||||
- 是否需要边缘抗锯齿?
|
||||
- 是否能接受硬边透明?
|
||||
|
||||
1-bit mask 省空间,但边缘可能有锯齿。
|
||||
|
||||
### zhRGB565 / QOI
|
||||
|
||||
适合压缩图片资源。
|
||||
|
||||
需要确认工程里是否启用了对应 Arm-2D loader/component。当前 skill 内已有 zhRGB565 相关辅助脚本,但是否能在目标工程中直接使用,仍要以工程端 loader/component 为准。
|
||||
|
||||
## 决策建议
|
||||
|
||||
### 用户只想验证画面
|
||||
|
||||
可以临时用:
|
||||
|
||||
- RGB565 frame array
|
||||
- sprite sheet 转数组
|
||||
|
||||
### 用户明确要省 Flash
|
||||
|
||||
优先考虑:
|
||||
|
||||
- 降尺寸 / 限帧率 / 减帧数
|
||||
- QOI
|
||||
- zhRGB565
|
||||
- tile/block 局部更新
|
||||
- palette animation
|
||||
- 背景固定时只存前景变化区域
|
||||
|
||||
### 用户要透明 GIF
|
||||
|
||||
先确认:
|
||||
|
||||
- 背景是否固定?
|
||||
- 透明边缘是否需要抗锯齿?
|
||||
- 是否可以用 1-bit mask?
|
||||
- 目标尺寸和帧数是多少?
|
||||
|
||||
### 用户要实时动态效果
|
||||
|
||||
优先考虑:
|
||||
|
||||
- 低分辨率缓存 + 放大
|
||||
- palette animation
|
||||
- 预计算 map 放 Flash
|
||||
|
||||
|
||||
## Python 工具
|
||||
|
||||
当前 skill 内置脚本目录:
|
||||
|
||||
```text
|
||||
python/
|
||||
```
|
||||
|
||||
主要脚本:
|
||||
|
||||
- `gif2png.py`:GIF 转精灵图。
|
||||
- `img2c.py`:图片转 C/ARM-2D tile 数据。
|
||||
- `ttf2c.py`:字体转换。
|
||||
- `jinja2c.py`:模板生成 C。
|
||||
- `__img2c_lmsk.py`:LMSK 辅助模块。
|
||||
- `__img2c_zhRGB565.py`:zhRGB565 辅助模块。
|
||||
|
||||
|
||||
使用前先运行 help 或直接读脚本参数。
|
||||
|
||||
## 当前不支持项
|
||||
|
||||
- 未提供格式文档、PC 编码/解码器或 MCU generic loader 的动画容器,不要写成可直接使用的方案。
|
||||
|
||||
## 待完善
|
||||
|
||||
- Keil 工程自动添加资源脚本。
|
||||
@@ -0,0 +1,189 @@
|
||||
# ARM-2D Generic Loader 参考
|
||||
|
||||
## 适用场景
|
||||
|
||||
使用 generic loader 的情况:
|
||||
|
||||
- 动态渲染,软件生成式内容,例如粒子模拟。
|
||||
- 需要按 ROI 逐块生成或解码资源内容。
|
||||
- 资源太大,不适合整帧解码到 SRAM。
|
||||
- 希望配合 PFB / ROI / dirty region 做局部刷新。
|
||||
|
||||
## 基本结构
|
||||
|
||||
建议新建一对文件:
|
||||
|
||||
```text
|
||||
project/mdk/<feature>/user_generic_loader_<feature>.c
|
||||
project/mdk/<feature>/user_generic_loader_<feature>.h
|
||||
```
|
||||
|
||||
头文件通常包含:
|
||||
|
||||
```c
|
||||
typedef struct xxx_cfg_t {
|
||||
arm_2d_size_t tSize;
|
||||
uint16_t bUseHeapForVRES : 1;
|
||||
|
||||
struct {
|
||||
const arm_loader_io_t *ptIO;
|
||||
uintptr_t pTarget;
|
||||
} ImageIO;
|
||||
|
||||
arm_2d_scene_t *ptScene;
|
||||
} xxx_cfg_t;
|
||||
|
||||
typedef struct xxx_t xxx_t;
|
||||
|
||||
struct xxx_t {
|
||||
union {
|
||||
arm_2d_tile_t tTile;
|
||||
inherit(arm_generic_loader_t);
|
||||
};
|
||||
|
||||
ARM_PRIVATE(
|
||||
xxx_cfg_t tCFG;
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
C 文件通常开头:
|
||||
|
||||
```c
|
||||
#define __GENERIC_LOADER_INHERIT__
|
||||
#define __xxx_IMPLEMENT__
|
||||
|
||||
#include "user_generic_loader_xxx.h"
|
||||
```
|
||||
|
||||
## init 模式
|
||||
|
||||
```c
|
||||
arm_generic_loader_cfg_t tCFG = {
|
||||
.bUseHeapForVRES = this.tCFG.bUseHeapForVRES,
|
||||
.tColourInfo.chScheme = ARM_2D_COLOUR,
|
||||
.bBlendWithBG = false,
|
||||
.ImageIO = {
|
||||
.ptIO = this.tCFG.ImageIO.ptIO,
|
||||
.pTarget = this.tCFG.ImageIO.pTarget,
|
||||
},
|
||||
.UserDecoder = {
|
||||
.fnDecoderInit = &__xxx_decoder_init,
|
||||
.fnDecode = &__xxx_draw,
|
||||
},
|
||||
.ptScene = this.tCFG.ptScene,
|
||||
};
|
||||
|
||||
arm_generic_loader_init(&this.use_as__arm_generic_loader_t, &tCFG);
|
||||
```
|
||||
|
||||
初始化后要设置 tile 尺寸:
|
||||
|
||||
```c
|
||||
this.tTile.tRegion.tSize = this.tCFG.tSize;
|
||||
```
|
||||
|
||||
## show 模式
|
||||
|
||||
```c
|
||||
void xxx_show(xxx_t *ptThis,
|
||||
const arm_2d_tile_t *ptTile,
|
||||
const arm_2d_region_t *ptRegion,
|
||||
bool bIsNewFrame)
|
||||
{
|
||||
ARM_2D_UNUSED(bIsNewFrame);
|
||||
|
||||
if (-1 == (intptr_t)ptTile) {
|
||||
ptTile = arm_2d_get_default_frame_buffer();
|
||||
}
|
||||
|
||||
arm_2d_tile_copy_only(&this.tTile, ptTile, ptRegion);
|
||||
}
|
||||
```
|
||||
|
||||
## fnDecode 约定
|
||||
|
||||
函数签名:
|
||||
|
||||
```c
|
||||
static arm_2d_err_t __xxx_draw(
|
||||
arm_generic_loader_t *ptObj,
|
||||
arm_2d_region_t *ptROI,
|
||||
uint8_t *pchBuffer,
|
||||
uint32_t iTargetStrideInByte,
|
||||
uint_fast8_t chBitsPerPixel)
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 必须只写 `ptROI` 对应区域。
|
||||
- 不能假设目标 buffer 是全屏连续 framebuffer。
|
||||
- 每行写完后用 `iTargetStrideInByte` 跳到下一行。
|
||||
- RGB565 时通常这样写:
|
||||
|
||||
```c
|
||||
for (int_fast16_t y = yStart; y < yLimit; y++) {
|
||||
uint16_t *phwPixel = (uint16_t *)pchBuffer;
|
||||
|
||||
for (int_fast16_t x = xStart; x < xLimit; x++) {
|
||||
*phwPixel++ = colour;
|
||||
}
|
||||
|
||||
pchBuffer += iTargetStrideInByte;
|
||||
}
|
||||
```
|
||||
|
||||
## scene 接入
|
||||
|
||||
在 `arm_2d_scene_0.h` 中 include 对应 loader 头文件。
|
||||
|
||||
在 `arm_2d_scene_0.c` 中:
|
||||
|
||||
```c
|
||||
xxx_t Xxx;
|
||||
```
|
||||
|
||||
load:
|
||||
|
||||
```c
|
||||
xxx_on_load(&Xxx);
|
||||
```
|
||||
|
||||
frame start:
|
||||
|
||||
```c
|
||||
xxx_on_frame_start(&Xxx);
|
||||
```
|
||||
|
||||
depose:
|
||||
|
||||
```c
|
||||
xxx_depose(&Xxx);
|
||||
```
|
||||
|
||||
PFB draw handler:
|
||||
|
||||
```c
|
||||
arm_2d_align_centre(__top_canvas, 240, 240) {
|
||||
xxx_show(&Xxx, ptTile, &__centre_region, bIsNewFrame);
|
||||
}
|
||||
```
|
||||
|
||||
init:
|
||||
|
||||
```c
|
||||
xxx_cfg_t tCFG = {
|
||||
.tSize = {
|
||||
.iWidth = 240,
|
||||
.iHeight = 240,
|
||||
},
|
||||
.ptScene = &this.use_as__arm_2d_scene_t,
|
||||
};
|
||||
xxx_init(&Xxx, &tCFG);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 如果 decoder 很重,不要在每个 ROI 里重复计算整帧。
|
||||
- 可以使用低分辨率缓存,然后 ROI 采样放大。
|
||||
- 如果使用压缩资源,尽量做 ROI 解码或 tile/block 解码。
|
||||
@@ -0,0 +1,677 @@
|
||||
---
|
||||
name: arm2d-mdk-rte
|
||||
description: Use when deploying, configuring, or debugging Arm-2D in embedded projects, especially Keil/MDK projects that use Arm CMSIS-Pack and RTE. Covers Arm-2D Pack/RTE integration, display adapter setup, PFB configuration, dirty-region refresh, synchronous/asynchronous flushing, DMA/display porting, demo/scene integration, and build-download-runtime verification. 用于在嵌入式工程中部署、配置、调试 Arm-2D,尤其适合通过 Keil/MDK、CMSIS-Pack、RTE 接入 Arm-2D 的项目,覆盖 Pack/RTE 部署、显示适配器、PFB、脏矩形、同步/异步刷新、DMA/显示端口、demo/scene 接入和编译下载运行验证。
|
||||
---
|
||||
|
||||
# Arm-2D Deployment With MDK/RTE
|
||||
|
||||
本 skill 用于把 Arm-2D 以 Pack/RTE 的方式部署到嵌入式工程中,并完成显示适配、刷新路径、demo 接入和性能验证。内容保持工程无关,不绑定具体芯片、屏幕、下载器、目录或项目参数。
|
||||
|
||||
## 中文版
|
||||
|
||||
### 适用场景
|
||||
|
||||
使用本 skill 处理这些任务:
|
||||
|
||||
- 在 Keil/MDK 工程里通过 CMSIS-Pack/RTE 添加 Arm-2D。
|
||||
- 配置 `arm_2d_cfg.h` 和 display adapter。
|
||||
- 为 LCD/OLED/RGB/MIPI/Framebuffer 等显示设备接入 Arm-2D。
|
||||
- 配置 PFB、dirty region、同步/异步 flushing。
|
||||
- 将 Arm-2D benchmark、官方 demo 或自定义 scene 接入应用。
|
||||
- 分析 Arm-2D 帧率、刷新耗时、DMA/display port 性能瓶颈。
|
||||
|
||||
### 部署原则
|
||||
|
||||
优先使用 Pack/RTE 集成 Arm-2D,而不是手动复制整套 Arm-2D 源码。
|
||||
|
||||
- 保持 Arm-2D 组件可在 MDK 的 Manage Run-Time Environment 里勾选和升级。
|
||||
- 不直接修改 Pack 安装目录里的文件。
|
||||
- 工程定制代码放在工程本地,例如 `RTE/`、`board/`、`bsp/`、`drivers/`、`applications/`。
|
||||
- 把显示硬件相关逻辑隔离到 display port/display adapter,应用层只调用 Arm-2D scene/demo 接口。
|
||||
|
||||
### Pack/RTE 接入步骤
|
||||
|
||||
1. 安装 Arm-2D Pack。
|
||||
2. 如果是重新部署或切换 Arm-2D Pack/RTE 版本,先清空工程里的 `RTE/Acceleration` 目录,再让 MDK/RTE 重新生成。
|
||||
3. 在 MDK 的 RTE 配置界面勾选 Arm-2D 相关组件。移植 Arm-2D 工程时,除非用户明确要求最小化组件,否则 Arm-2D 组件可以全部加入;Arm-2D/编译器/链接器会在编译和链接阶段自行裁剪未使用代码,不要为了“看起来干净”过早少选组件导致后续缺头文件、缺 helper 或缺 scene/demo 支持。
|
||||
4. 如果用户工程明显使用了操作系统/RTOS(例如存在 RT-Thread、FreeRTOS、CMSIS-RTOS、线程、信号量、互斥锁、定时器任务等痕迹),在 RTE 中勾选 Arm-2D 的 RTOS Helper。
|
||||
5. 确认工程里生成或引用了 Arm-2D 配置文件。
|
||||
6. 确认 include path 包含 RTE 生成目录、Arm-2D 配置目录、显示适配层目录和应用目录。
|
||||
7. 在工程本地维护 Arm-2D 配置,不修改 Pack 原始文件。
|
||||
|
||||
按照 Arm-2D “傻瓜部署教程”移植默认 Display Adapter 场景时,优先在 RTE 中选择这些组件:
|
||||
|
||||
- CMSIS: `CORE`
|
||||
- CMSIS: `DSP`,优先使用 `Source` variant
|
||||
- Utilities / `perf_counter`: `Core`,优先使用 `Source` variant;这会通过 Pack/RTE 加入 `perf_counter.c`、默认 port 和 SysTick wrapper
|
||||
- Acceleration / Arm-2D: `Core`
|
||||
- Acceleration / Arm-2D: `Alpha-Blending`
|
||||
- Acceleration / Arm-2D: `Transform`
|
||||
- Acceleration / Arm-2D Helper: `PFB`
|
||||
- Acceleration / Arm-2D Helper: `Display Adapter`
|
||||
- 点击 Resolve 后补齐依赖,通常包括 Arm-2D Extras 的 `LCD ASCII Printf` 和 `Controls`,以及 Controls 依赖的 `Filter`
|
||||
|
||||
如果 Keil/RTE 已经把这些组件源文件加入构建,不要再在普通工程分组里手动重复加入同一批 Pack 源文件;重复加入会导致 `arm_2d_helper_init`、`arm_cos_f32`、字体/资源对象等符号 multiply defined。普通工程分组只保留本地平台适配代码,例如 `Disp0_DrawBitmap()`、时间戳函数和 cache clean 逻辑。
|
||||
|
||||
强烈建议按 perf_counter Pack 方式给 Arm-2D 提供高精度参考时钟,不要长期使用 `HAL_GetTick()`/`1000Hz` 作为 helper 时间源。默认 Display Adapter 场景、动画和 benchmark 都依赖 `arm_2d_helper_get_system_timestamp()` 与 `arm_2d_helper_get_reference_clock_frequency()`;若只返回毫秒 tick,UI 可能看起来不动或性能数据失真。
|
||||
|
||||
perf_counter RTE 部署要点:
|
||||
|
||||
1. 在 RTE 中勾选 `GorgonMeducer::Utilities:perf_counter:Core&Source`。不要优先选择 `SourceNoWrapper`,也不要修改 perf_counter Pack 安装目录里的源码。
|
||||
2. 点击 Resolve,让 RTE 自动加入 `perf_counter.c`、`perfc_port_default.c` 和对应的 SysTick wrapper 汇编文件。
|
||||
3. Source variant 默认启用 SysTick wrapper,并在 `Pre_Include_Global.h` 中生成 `__PERF_COUNTER_CFG_USE_SYSTICK_WRAPPER__`。
|
||||
4. `RTE_Components.h` 中应生成 `__PERF_COUNTER__`。
|
||||
5. Arm Compiler 5/6 下不要在 `SysTick_Handler()` 中手工调用 `perfc_port_insert_to_system_timer_insert_ovf_handler()`;wrapper 会调用原有 SysTick handler。
|
||||
6. HAL 已经占用 SysTick 的裸机工程里,在 `SystemClock_Config()` 完成后、Arm-2D 初始化前调用 `perfc_init(true)`。
|
||||
7. 新版本 Arm-2D CMSIS-Pack 的 `arm_2d_helper.c` 已经提供 weak 默认实现:检测到 `__PERF_COUNTER__` 后自动调用 `get_system_ticks()`,并在 timestamp 中叠加 `s_tHelper.lTimestampInit`;参考时钟也会优先通过 perf_counter port 获取。此时不要在本地 port 中重复强定义 `arm_2d_helper_get_system_timestamp()` 或 `arm_2d_helper_get_reference_clock_frequency()`,否则会覆盖 Pack 原生逻辑。
|
||||
8. 只有在旧版本 Arm-2D Pack 没有上述自动适配、或非 Arm 架构要求用户实现时,才在工程本地提供 helper 时间源:
|
||||
|
||||
```c
|
||||
#include "perf_counter.h"
|
||||
|
||||
int64_t arm_2d_helper_get_system_timestamp(void)
|
||||
{
|
||||
return get_system_ticks();
|
||||
}
|
||||
|
||||
uint32_t arm_2d_helper_get_reference_clock_frequency(void)
|
||||
{
|
||||
return perfc_get_systimer_frequency();
|
||||
}
|
||||
```
|
||||
|
||||
Keil/MDK AC6 汇编器配置必须同步检查:
|
||||
|
||||
- 若工程使用 Arm Compiler 6 / ARMCLANG,perf_counter Pack 可能选入 `systick_wrapper_gnu.s`。
|
||||
- `systick_wrapper_gnu.s` 必须由 armclang assembler 处理;如果被旧式 Arm assembler 处理,会出现 `Unexpected characters at end of line`、`Area directive missing` 等错误。
|
||||
- 在 Keil 工程 XML 中,`<ClangAsOpt>` 应选择 armclang assembler 对应值;本类工程中实测 `<ClangAsOpt>1</ClangAsOpt>` 可以让 `systick_wrapper_gnu.s` 正常编译。
|
||||
- 处理这类错误时优先修正工程的汇编器选择,不要修改 perf_counter Pack 源码,也不要为了绕过错误改用手工 SysTick hook。
|
||||
|
||||
perf_counter 部署验收清单:
|
||||
|
||||
- 构建日志中出现 `compiling perf_counter.c` 和 `compiling perfc_port_default.c`。
|
||||
- 使用 Source variant 时,构建日志中应出现对应 wrapper,例如 `assembling systick_wrapper_gnu.s`,且最终链接成功。
|
||||
- MAP 文件中 `SysTick_Handler` 应来自 wrapper 对象,并引用原有 `$Super$$SysTick_Handler`;wrapper 同时应引用 `perfc_port_insert_to_system_timer_insert_ovf_handler`。
|
||||
- MAP 文件中应能看到 `perfc_init`、`get_system_ticks` 和 `PERFC`。
|
||||
- 应用代码中不再用 `HAL_GetTick()`/`1000U` 作为 `arm_2d_helper_get_system_timestamp()` / `arm_2d_helper_get_reference_clock_frequency()` 的实现。
|
||||
- 若 Arm-2D Pack 自带 perf_counter-aware weak helper,不应在本地 port 中强覆盖这两个函数;MAP 中应能看到这两个函数来自 Arm-2D helper 对象,而不是本地显示 port。
|
||||
|
||||
同一台机器可能同时存在多个 Arm Pack Root,例如 `C:\Users\...\AppData\Local\Arm\Packs` 和 `D:\Users\...\AppData\Local\Arm\Packs`。以当前 Keil/uVision 构建日志和 RTE 配置实际选择的 Pack Root 为准,不要在 include path 或普通分组里手动引用另一个 Pack Root 下的 Arm-2D/CMSIS-DSP 源码或头文件,否则容易出现版本混用、重复符号或配置界面与编译结果不一致。
|
||||
|
||||
清空 `RTE/Acceleration` 的目的是避免旧版本 RTE 生成文件、旧配置头文件、旧组件引用和新 Pack 混在一起。清空前如果里面有手写的工程配置,先迁移或备份到工程自己的 board/application 配置位置。
|
||||
|
||||
常见工程文件:
|
||||
|
||||
```text
|
||||
RTE/Acceleration/arm_2d_cfg.h
|
||||
RTE/Acceleration/arm_2d_disp_adapter_0.h
|
||||
board/arm2d/
|
||||
applications/
|
||||
```
|
||||
|
||||
Arm-2D 基础适配参数主要集中在:
|
||||
|
||||
- `arm_2d_disp_adapter_0.h`:display adapter、屏幕尺寸、PFB/display 刷新相关配置通常在这里。
|
||||
- `arm_2d_cfg.h`:Arm-2D 全局配置、helper、dirty region、PFB/内存策略等通常在这里。
|
||||
|
||||
移植时必须重点核对 PFB 大小与屏幕分辨率是否匹配。PFB block 过小会导致切块和 flush 次数过多,调度开销变大;PFB block 过大则会占用过多 SRAM,并可能削弱 dirty region 的灵活性。不要照搬原工程的 PFB 参数,必须结合目标屏幕分辨率、像素格式、SRAM 容量、DMA/display 接口效率重新确认。
|
||||
|
||||
实际路径可以不同,关键是区分:
|
||||
|
||||
- Pack 提供通用 Arm-2D 源码和组件。
|
||||
- RTE/工程本地文件提供配置。
|
||||
- board/driver 层提供屏幕 flush、DMA、cache、同步回调。
|
||||
- application 层提供 scene/demo。
|
||||
|
||||
### 初始化流程
|
||||
|
||||
典型启动顺序:
|
||||
|
||||
```c
|
||||
arm_2d_init();
|
||||
disp_adapter0_init();
|
||||
```
|
||||
|
||||
之后在主循环或 RTOS 线程中持续驱动 display adapter:
|
||||
|
||||
```c
|
||||
while (1) {
|
||||
(void)__disp_adapter0_task();
|
||||
}
|
||||
```
|
||||
|
||||
如果使用 RTOS,benchmark 或渲染线程里不要随手加入额外 delay/yield,除非它是调度策略的一部分。测性能时应先让渲染循环保持清晰、稳定。
|
||||
|
||||
### Display Adapter 配置
|
||||
|
||||
display adapter 至少需要明确:
|
||||
|
||||
- 屏幕宽高。
|
||||
- 像素格式,例如 RGB565、RGB888、ARGB8888。
|
||||
- PFB block 宽高。
|
||||
- PFB 数量或 heap size。
|
||||
- dirty region 是否启用。
|
||||
- flush 是同步还是异步。
|
||||
- 是否需要 RGB565 高低字节交换。
|
||||
- cache clean/invalidate 策略。
|
||||
|
||||
配置时先保证正确显示,再优化性能。
|
||||
|
||||
### PFB 配置
|
||||
|
||||
PFB 是 Arm-2D 的分块渲染缓冲。调 PFB 时关注这些点:
|
||||
|
||||
- PFB 太小:切块数量多,调度和 flush 开销变大。
|
||||
- PFB 太大:内存占用大,dirty region 裁剪可能不够灵活。
|
||||
- PFB 尺寸应结合屏幕分辨率、缓存、DMA 对齐、总线效率和 scene 类型实测。
|
||||
- 如果配置项支持动态 PFB,需要确认手动设置是否真的生效。
|
||||
- PFB 对齐参数可能改善 DMA/display 写入效率,也可能扩大实际渲染区域,需要实测。
|
||||
|
||||
不要只看理论,PFB 最终以硬件运行数据为准。
|
||||
|
||||
### Dirty Region
|
||||
|
||||
dirty region 适合 UI、仪表、动画元素较少或局部变化的 scene。
|
||||
|
||||
- 开启后,Arm-2D 可以只刷新变化区域,降低 CPU 渲染和 display flush 工作量。
|
||||
- 对全屏 shader、全屏视频、整屏滚动等场景,dirty region 优势可能较小。
|
||||
- 不要假设关闭 dirty region 一定更快;应通过帧率、flush 次数和实际刷新面积验证。
|
||||
|
||||
验证 dirty region 时要同时看:
|
||||
|
||||
- FPS/frame time。
|
||||
- 每帧 flush 次数。
|
||||
- 实际 flush 像素数量或区域大小。
|
||||
- 是否出现区域不刷新、残影或误判静止。
|
||||
|
||||
### 同步与异步刷新
|
||||
|
||||
同步 flush:
|
||||
|
||||
- 实现简单,行为确定。
|
||||
- 适合单 PFB 或 flush 很快的场景。
|
||||
- 如果 display flush 时间很短,同步方式可能更高效。
|
||||
|
||||
异步 flush:
|
||||
|
||||
- 目标是让 CPU 准备下一块 PFB,同时 DMA/display 外设刷新上一块 PFB。
|
||||
- 通常需要至少两个 PFB buffer。
|
||||
- 必须正确处理 flush complete、buffer release、cache 和并发状态。
|
||||
- 如果只有一个 PFB,异步刷新通常无法真正并行,反而可能增加回调/中断/状态机开销。
|
||||
|
||||
是否开启异步刷新,应通过硬件数据判断,而不是按名字判断。
|
||||
|
||||
### 显示 Flush Port
|
||||
|
||||
display flush port 是 Arm-2D 和硬件屏幕之间的关键层。
|
||||
|
||||
常见实现:
|
||||
|
||||
- CPU copy 到 framebuffer。
|
||||
- DMA2D/2D accelerator copy 到 framebuffer。
|
||||
- SPI/QSPI/MIPI/8080 等接口发送区域数据。
|
||||
- LTDC/RGB 屏幕使用 framebuffer,flush 只更新 framebuffer 对应区域。
|
||||
|
||||
实现 flush port 时要确认:
|
||||
|
||||
- 源 PFB 地址、目标 framebuffer 地址和 stride 正确。
|
||||
- 像素格式匹配。
|
||||
- RGB565 高低字节顺序正确。
|
||||
- cache clean/invalidate 正确。
|
||||
- DMA completion callback 或同步等待逻辑可靠。
|
||||
- 错误标志可以被统计或打印。
|
||||
|
||||
如果有 2D DMA/GPU/accelerator,应优先让硬件做大块搬运或格式转换,CPU 负责 scene 逻辑和必要的软件渲染。
|
||||
|
||||
### Demo 和 Scene 接入
|
||||
|
||||
Arm-2D demo 应放在应用层,和显示端口解耦。
|
||||
|
||||
常见文件组织:
|
||||
|
||||
```text
|
||||
applications/arm2d_demo.c
|
||||
applications/arm2d_demo.h
|
||||
applications/arm2d_scene_xxx.c
|
||||
applications/arm2d_scene_xxx.h
|
||||
```
|
||||
|
||||
scene 代码通常包含:
|
||||
|
||||
```c
|
||||
#include "arm_2d.h"
|
||||
#include "arm_2d_helper.h"
|
||||
#include "arm_2d_disp_adapter_0.h"
|
||||
```
|
||||
|
||||
Arm-2D scene 渲染回调常见形式:
|
||||
|
||||
```c
|
||||
arm_fsm_rt_t fnScene(void *pTarget,
|
||||
const arm_2d_tile_t *ptTile,
|
||||
bool bIsNewFrame);
|
||||
```
|
||||
|
||||
自定义 scene 要注意:
|
||||
|
||||
- 用 Arm-2D 的 tile/region API 获取当前 PFB 的有效区域。
|
||||
- 不要假设每次都渲染整屏。
|
||||
- 需要全屏动画时,显式启用 full-frame refresh 或每帧标记 dirty。
|
||||
- benchmark、官方 demo、自定义 demo 应能互相替换,不要把显示驱动写死在 demo 里。
|
||||
|
||||
### Arm-2D API 理解与避坑
|
||||
|
||||
不要凭直觉猜 Arm-2D API。先确认当前 Pack 版本里的头文件和 demo,再写适配代码。
|
||||
|
||||
核心概念:
|
||||
|
||||
- `arm_2d_tile_t` 表示一块二维像素区域,可以是 framebuffer、PFB、子图、图片资源或任意渲染目标。
|
||||
- `arm_2d_region_t` 表示 tile 内的矩形区域,包含 location 和 size。
|
||||
- `arm_2d_location_t` 表示二维坐标。
|
||||
- `arm_fsm_rt_t` 是 Arm-2D 常见的异步/状态机返回值,不能简单当作 `void` 或 `bool`。
|
||||
- `arm_2d_scene_t` 是 scene player 使用的场景基类,用户 scene 通常通过 `implement(arm_2d_scene_t);` 嵌入它。
|
||||
|
||||
scene 回调常见签名是:
|
||||
|
||||
```c
|
||||
arm_fsm_rt_t fnScene(void *pTarget,
|
||||
const arm_2d_tile_t *ptTile,
|
||||
bool bIsNewFrame);
|
||||
```
|
||||
|
||||
注意第一个参数通常是 `void *pTarget`,不是 `arm_2d_scene_t *`。如果需要访问自定义 scene 对象,要按当前 demo/框架写法从 target 或容器关系中取得,不要硬猜。
|
||||
|
||||
从 tile 取得当前 PFB 有效区域和像素地址时,优先使用 Arm-2D 提供的 helper/API,不要自己用裸指针猜偏移:
|
||||
|
||||
```c
|
||||
arm_2d_location_t tOffset;
|
||||
arm_2d_region_t tValidRegion;
|
||||
uint16_t *phwBuffer;
|
||||
|
||||
arm_2d_get_address_and_region_from_tile(ptTile,
|
||||
&tValidRegion,
|
||||
&tOffset,
|
||||
uint16_t,
|
||||
phwBuffer);
|
||||
```
|
||||
|
||||
写像素时要分清:
|
||||
|
||||
- 当前 PFB 的有效区域 `tValidRegion`。
|
||||
- 当前 PFB 在完整屏幕里的偏移 `tOffset`。
|
||||
- 目标 tile 的 stride。
|
||||
- 像素格式和像素类型,例如 `uint16_t` 对 RGB565。
|
||||
|
||||
常见错误:
|
||||
|
||||
- 误以为每次 scene 回调拿到的都是整屏 framebuffer。
|
||||
- 忽略 PFB offset,导致每个 block 都从 `(0,0)` 的逻辑坐标开始画。
|
||||
- 用 region 宽度当 stride,但实际 stride 应按目标 tile/framebuffer 的行宽计算。
|
||||
- 没有处理 `bIsNewFrame`,导致动画时间、状态更新或 dirty 标记异常。
|
||||
- 没有 full-frame refresh,过程动画或 shader 只画一帧后不再刷新。
|
||||
- 把 Arm-2D 的异步返回值当同步函数处理。
|
||||
|
||||
对全屏动画、shader、实时仪表这类每帧变化的内容,应显式启用 full-frame refresh 或确保每帧都有 dirty region:
|
||||
|
||||
```c
|
||||
arm_2d_helper_pfb_full_frame_refresh_mode(&DISP_ADAPTER.use_as__arm_2d_helper_pfb_t,
|
||||
true);
|
||||
```
|
||||
|
||||
其中 `DISP_ADAPTER` 应替换为当前工程实际的 display adapter 对象。
|
||||
|
||||
对 scene/player/demo 的建议:
|
||||
|
||||
- 先参考 Pack 自带 demo 的 scene 初始化和 append 流程。
|
||||
- 不要绕过 scene player 直接乱调内部成员,除非当前版本文档或 demo 明确这样做。
|
||||
- 每次换 Arm-2D Pack 版本,都重新核对函数签名和结构体字段。
|
||||
- 自定义 demo 要和 display flush port 解耦,避免 demo 直接依赖某个屏幕驱动。
|
||||
|
||||
### 性能验证
|
||||
|
||||
性能优化前先建立可观测数据。
|
||||
|
||||
建议统计:
|
||||
|
||||
- FPS。
|
||||
- frame time。
|
||||
- flush time。
|
||||
- flush count。
|
||||
- flush pixel count 或区域大小。
|
||||
- CPU 占用。
|
||||
- DMA/display 错误数。
|
||||
- 是否发生 cache、bus、alignment 相关异常。
|
||||
|
||||
判断瓶颈:
|
||||
|
||||
- flush time 很低但 frame time 很高:通常是 CPU 渲染瓶颈。
|
||||
- flush time 很高:检查 display 总线、DMA、framebuffer、cache、区域大小。
|
||||
- FPS 高但 flush count 不增长:可能是假数据或 scene 没有继续刷新。
|
||||
- 画面残影或局部不更新:检查 dirty region、tile region、full-frame refresh 逻辑。
|
||||
|
||||
### 编译下载验证
|
||||
|
||||
通用流程:
|
||||
|
||||
1. 编译工程。
|
||||
2. 下载到目标板。
|
||||
3. 确认程序复位运行。
|
||||
4. 读取或打印 Arm-2D 运行统计。
|
||||
5. 对比不同 PFB、dirty、async、DMA/cache 配置。
|
||||
6. 每次启用 LTO 或改变优化等级后,重新确认符号地址和运行状态。
|
||||
|
||||
在 MDK 命令行环境中可以使用 UV4 自动编译下载;在有调试器/探针工具时,可以读取 RAM 统计变量辅助判断。
|
||||
|
||||
### 常见问题
|
||||
|
||||
- 重新部署 Arm-2D Pack/RTE 前没有清空 `RTE/Acceleration`,导致旧版本生成文件和新组件混用。
|
||||
- Pack/RTE 组件添加了,但 include path 没有包含工程本地配置目录。
|
||||
- 修改了 PFB 配置,但动态 PFB 或其他宏覆盖了手动设置。
|
||||
- RGB565 颜色异常,可能是 BGR/RGB 顺序或高低字节问题。
|
||||
- DCache 开启后没有 clean/invalidate,导致 DMA/display 读到旧数据。
|
||||
- 单 PFB 下打开 async flush,以为会自动并行,实际没有收益。
|
||||
- dirty region 误判导致 scene 跑一帧后停住。
|
||||
- benchmark 分数和真实应用 FPS 不一致,因为 scene 负载完全不同。
|
||||
- LTO 后符号地址变化,调试工具读取了旧地址。
|
||||
|
||||
---
|
||||
|
||||
## English Version
|
||||
|
||||
### When To Use
|
||||
|
||||
Use this skill for tasks such as:
|
||||
|
||||
- Adding Arm-2D to a Keil/MDK project through CMSIS-Pack/RTE.
|
||||
- Configuring `arm_2d_cfg.h` and a display adapter.
|
||||
- Connecting Arm-2D to LCD/OLED/RGB/MIPI/framebuffer displays.
|
||||
- Tuning PFB, dirty regions, synchronous flushing, and asynchronous flushing.
|
||||
- Integrating Arm-2D benchmarks, official demos, or custom scenes.
|
||||
- Debugging Arm-2D frame rate, flush time, DMA/display port behavior, and rendering bottlenecks.
|
||||
|
||||
### Deployment Principles
|
||||
|
||||
Prefer Pack/RTE integration instead of manually copying the whole Arm-2D source tree into the project.
|
||||
|
||||
- Keep Arm-2D selectable and upgradable from MDK's Manage Run-Time Environment.
|
||||
- Do not edit files inside the installed Pack directory.
|
||||
- Keep project-specific code in local project folders such as `RTE/`, `board/`, `bsp/`, `drivers/`, or `applications/`.
|
||||
- Isolate display-hardware logic in the display port/display adapter. Application code should use Arm-2D scene/demo APIs.
|
||||
|
||||
### Pack/RTE Integration Steps
|
||||
|
||||
1. Install the Arm-2D Pack.
|
||||
2. If redeploying Arm-2D or switching Pack/RTE versions, clear the project's `RTE/Acceleration` directory first, then let MDK/RTE regenerate it.
|
||||
3. Select the required Arm-2D components in MDK's RTE configuration.
|
||||
4. Confirm the project has Arm-2D configuration files.
|
||||
5. Confirm include paths cover the generated RTE folder, Arm-2D configuration folder, display adapter/port folder, and application folder.
|
||||
6. Keep Arm-2D configuration project-local. Do not patch the Pack source itself.
|
||||
|
||||
Clearing `RTE/Acceleration` prevents stale generated files, old configuration headers, and old component references from being mixed with the new Pack version. If the directory contains hand-written project configuration, migrate or back it up into the project's own board/application configuration area first.
|
||||
|
||||
Common project files:
|
||||
|
||||
```text
|
||||
RTE/Acceleration/arm_2d_cfg.h
|
||||
RTE/Acceleration/arm_2d_disp_adapter_0.h
|
||||
board/arm2d/
|
||||
applications/
|
||||
```
|
||||
|
||||
The exact paths may differ. The important split is:
|
||||
|
||||
- Pack files provide generic Arm-2D source and components.
|
||||
- RTE/project-local files provide configuration.
|
||||
- board/driver code provides screen flush, DMA, cache, and synchronization hooks.
|
||||
- application code provides scenes and demos.
|
||||
|
||||
### Initialization Flow
|
||||
|
||||
Typical startup sequence:
|
||||
|
||||
```c
|
||||
arm_2d_init();
|
||||
disp_adapter0_init();
|
||||
```
|
||||
|
||||
Then drive the display adapter from the main loop or an RTOS thread:
|
||||
|
||||
```c
|
||||
while (1) {
|
||||
(void)__disp_adapter0_task();
|
||||
}
|
||||
```
|
||||
|
||||
When measuring benchmark/demo performance, avoid adding unnecessary delay/yield calls unless they are part of the intended scheduling model.
|
||||
|
||||
### Display Adapter Configuration
|
||||
|
||||
A display adapter should define:
|
||||
|
||||
- Screen width and height.
|
||||
- Pixel format, such as RGB565, RGB888, or ARGB8888.
|
||||
- PFB block width and height.
|
||||
- PFB count or heap size.
|
||||
- Whether dirty-region optimization is enabled.
|
||||
- Whether flushing is synchronous or asynchronous.
|
||||
- Whether RGB565 byte swapping is needed.
|
||||
- Cache clean/invalidate policy.
|
||||
|
||||
First make the display correct, then tune performance.
|
||||
|
||||
### PFB Configuration
|
||||
|
||||
PFB is Arm-2D's partial framebuffer/block rendering buffer. Tune it with these tradeoffs in mind:
|
||||
|
||||
- Too small: too many blocks, more scheduling and flush overhead.
|
||||
- Too large: higher memory usage and sometimes less effective dirty-region clipping.
|
||||
- PFB size should be measured against screen resolution, cache behavior, DMA alignment, bus efficiency, and scene type.
|
||||
- If dynamic PFB is enabled, confirm that manual settings are actually taking effect.
|
||||
- Alignment settings can improve DMA/display efficiency or increase the actual rendered area. Measure both.
|
||||
|
||||
Use hardware data as the final judge.
|
||||
|
||||
### Dirty Regions
|
||||
|
||||
Dirty regions are useful for UI, gauges, small animations, and scenes where only part of the screen changes.
|
||||
|
||||
- They can reduce CPU rendering and display flush workload.
|
||||
- They may help less for fullscreen shaders, video-like content, or full-screen scrolling.
|
||||
- Do not assume disabling dirty regions is faster. Measure FPS, flush count, and actual flushed area.
|
||||
|
||||
When validating dirty regions, check:
|
||||
|
||||
- FPS/frame time.
|
||||
- Flush count per frame.
|
||||
- Flush pixel count or region sizes.
|
||||
- Missing updates, ghosting, or a scene that stops after one frame.
|
||||
|
||||
### Synchronous And Asynchronous Flushing
|
||||
|
||||
Synchronous flushing:
|
||||
|
||||
- Is simple and deterministic.
|
||||
- Works well with one PFB or very fast display flushes.
|
||||
- Can be faster when flush time is already low.
|
||||
|
||||
Asynchronous flushing:
|
||||
|
||||
- Tries to let the CPU prepare the next PFB while DMA/display hardware flushes the previous one.
|
||||
- Usually requires at least two PFB buffers.
|
||||
- Requires correct flush-complete, buffer-release, cache, and concurrency handling.
|
||||
- With only one PFB, async flushing usually cannot provide real overlap and may add callback/ISR/state-machine overhead.
|
||||
|
||||
Enable async flushing because measurements prove it helps, not because the name sounds faster.
|
||||
|
||||
### Display Flush Port
|
||||
|
||||
The display flush port is the key bridge between Arm-2D and the physical display.
|
||||
|
||||
Common implementations:
|
||||
|
||||
- CPU copy into a framebuffer.
|
||||
- 2D DMA/accelerator copy into a framebuffer.
|
||||
- SPI/QSPI/MIPI/8080 transfer of the updated region.
|
||||
- LTDC/RGB display with a framebuffer where flush updates the matching region.
|
||||
|
||||
Verify:
|
||||
|
||||
- Source PFB address, target framebuffer address, and stride.
|
||||
- Pixel format compatibility.
|
||||
- RGB565 byte order.
|
||||
- Cache clean/invalidate behavior.
|
||||
- DMA completion callback or synchronous wait logic.
|
||||
- Error flags or counters.
|
||||
|
||||
If a 2D DMA/GPU/accelerator is available, prefer using it for large copies or format conversion while the CPU handles scene logic and required software rendering.
|
||||
|
||||
### Demo And Scene Integration
|
||||
|
||||
Keep Arm-2D demos in the application layer and decoupled from the display port.
|
||||
|
||||
Common organization:
|
||||
|
||||
```text
|
||||
applications/arm2d_demo.c
|
||||
applications/arm2d_demo.h
|
||||
applications/arm2d_scene_xxx.c
|
||||
applications/arm2d_scene_xxx.h
|
||||
```
|
||||
|
||||
Scene files often include:
|
||||
|
||||
```c
|
||||
#include "arm_2d.h"
|
||||
#include "arm_2d_helper.h"
|
||||
#include "arm_2d_disp_adapter_0.h"
|
||||
```
|
||||
|
||||
Common Arm-2D scene render callback form:
|
||||
|
||||
```c
|
||||
arm_fsm_rt_t fnScene(void *pTarget,
|
||||
const arm_2d_tile_t *ptTile,
|
||||
bool bIsNewFrame);
|
||||
```
|
||||
|
||||
For custom scenes:
|
||||
|
||||
- Use Arm-2D tile/region APIs to get the valid area of the current PFB.
|
||||
- Do not assume every render pass covers the full screen.
|
||||
- For fullscreen animation, explicitly enable full-frame refresh or mark the frame dirty every frame.
|
||||
- Benchmarks, official demos, and custom demos should be replaceable without hardwiring display-driver code into the demo.
|
||||
|
||||
### Arm-2D API Notes And Pitfalls
|
||||
|
||||
Do not guess Arm-2D APIs from intuition. Check the headers and demos from the active Pack version before writing adapter or scene code.
|
||||
|
||||
Core concepts:
|
||||
|
||||
- `arm_2d_tile_t` represents a 2D pixel area. It may be a framebuffer, PFB, child tile, image asset, or generic render target.
|
||||
- `arm_2d_region_t` represents a rectangular area inside a tile, including location and size.
|
||||
- `arm_2d_location_t` represents a 2D coordinate.
|
||||
- `arm_fsm_rt_t` is a common Arm-2D async/state-machine return type. Do not treat it as plain `void` or `bool`.
|
||||
- `arm_2d_scene_t` is the scene base used by the scene player. User scenes commonly embed it with `implement(arm_2d_scene_t);`.
|
||||
|
||||
A common scene callback signature is:
|
||||
|
||||
```c
|
||||
arm_fsm_rt_t fnScene(void *pTarget,
|
||||
const arm_2d_tile_t *ptTile,
|
||||
bool bIsNewFrame);
|
||||
```
|
||||
|
||||
The first parameter is usually `void *pTarget`, not `arm_2d_scene_t *`. If custom scene state is needed, follow the current demo/framework pattern to recover it. Do not invent a signature.
|
||||
|
||||
When getting the valid PFB area and pixel pointer from a tile, prefer Arm-2D helper/API calls instead of hand-rolled pointer arithmetic:
|
||||
|
||||
```c
|
||||
arm_2d_location_t tOffset;
|
||||
arm_2d_region_t tValidRegion;
|
||||
uint16_t *phwBuffer;
|
||||
|
||||
arm_2d_get_address_and_region_from_tile(ptTile,
|
||||
&tValidRegion,
|
||||
&tOffset,
|
||||
uint16_t,
|
||||
phwBuffer);
|
||||
```
|
||||
|
||||
When writing pixels, distinguish:
|
||||
|
||||
- The current valid PFB region `tValidRegion`.
|
||||
- The current PFB offset in full-screen coordinates `tOffset`.
|
||||
- The destination tile stride.
|
||||
- The pixel format and pixel type, such as `uint16_t` for RGB565.
|
||||
|
||||
Common mistakes:
|
||||
|
||||
- Assuming every scene callback receives the full-screen framebuffer.
|
||||
- Ignoring PFB offset so every block renders as if it starts at logical `(0,0)`.
|
||||
- Using region width as stride when the actual stride is the destination tile/framebuffer line width.
|
||||
- Ignoring `bIsNewFrame`, which can break animation timing, state updates, or dirty marking.
|
||||
- Forgetting full-frame refresh, causing procedural animation or shaders to render one frame and stop.
|
||||
- Treating Arm-2D async return values as synchronous function results.
|
||||
|
||||
For fullscreen animation, shaders, or real-time gauges that change every frame, explicitly enable full-frame refresh or mark dirty regions every frame:
|
||||
|
||||
```c
|
||||
arm_2d_helper_pfb_full_frame_refresh_mode(&DISP_ADAPTER.use_as__arm_2d_helper_pfb_t,
|
||||
true);
|
||||
```
|
||||
|
||||
Replace `DISP_ADAPTER` with the actual display adapter object used by the project.
|
||||
|
||||
Scene/player/demo guidance:
|
||||
|
||||
- Start from the Pack-provided demo initialization and scene append flow.
|
||||
- Do not bypass the scene player and poke internal fields unless the current version's docs or demos show that pattern.
|
||||
- Re-check function signatures and struct fields after changing Arm-2D Pack versions.
|
||||
- Keep custom demos decoupled from the display flush port.
|
||||
|
||||
### Performance Verification
|
||||
|
||||
Establish observability before optimizing.
|
||||
|
||||
Recommended metrics:
|
||||
|
||||
- FPS.
|
||||
- Frame time.
|
||||
- Flush time.
|
||||
- Flush count.
|
||||
- Flush pixel count or region size.
|
||||
- CPU load.
|
||||
- DMA/display error count.
|
||||
- Cache, bus, or alignment-related faults.
|
||||
|
||||
Bottleneck interpretation:
|
||||
|
||||
- Low flush time but high frame time: likely CPU rendering bottleneck.
|
||||
- High flush time: inspect display bus, DMA, framebuffer, cache, and region size.
|
||||
- High FPS but no increasing flush count: likely stale/fake data or a scene that is not refreshing.
|
||||
- Ghosting or partial updates: inspect dirty regions, tile regions, and full-frame refresh logic.
|
||||
|
||||
### Build, Download, And Runtime Verification
|
||||
|
||||
Generic flow:
|
||||
|
||||
1. Build the project.
|
||||
2. Download it to the target.
|
||||
3. Confirm the program resets and runs.
|
||||
4. Read or print Arm-2D runtime statistics.
|
||||
5. Compare PFB, dirty-region, async, DMA, and cache configurations.
|
||||
6. After enabling LTO or changing optimization levels, re-check symbol addresses and runtime state.
|
||||
|
||||
In MDK command-line workflows, UV4 can automate build/download. With a debugger/probe tool, read RAM statistics to validate runtime behavior.
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- Redeploying Arm-2D Pack/RTE without clearing `RTE/Acceleration`, which can mix stale generated files with new components.
|
||||
- Arm-2D Pack/RTE components are selected, but include paths miss the project-local config directory.
|
||||
- PFB settings are changed, but dynamic PFB or other macros override them.
|
||||
- RGB565 colors are wrong because of RGB/BGR order or byte order.
|
||||
- DCache is enabled but clean/invalidate is missing, so DMA/display reads stale data.
|
||||
- Async flush is enabled with only one PFB and no real pipeline overlap happens.
|
||||
- Dirty-region logic causes a scene to render one frame and then stop.
|
||||
- Benchmark FPS does not match application FPS because the scene workload is different.
|
||||
- LTO moves symbols and the debug tool reads old addresses.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Keil MDK 工程修改笔记
|
||||
|
||||
## 常见文件
|
||||
|
||||
嵌入式 ARM-2D 工程常见路径:
|
||||
|
||||
```text
|
||||
project/mdk/template.uvprojx
|
||||
project/mdk/template.uvoptx
|
||||
project/mdk/RTE/Acceleration/arm_2d_scene_0.c
|
||||
project/mdk/RTE/Acceleration/arm_2d_scene_0.h
|
||||
```
|
||||
|
||||
## 添加源文件
|
||||
|
||||
新增 `.c/.h` 后,通常要改:
|
||||
|
||||
- `.uvprojx`:真正参与构建。
|
||||
- `.uvoptx`:IDE 文件树显示,非绝对必须,但建议同步。
|
||||
- IncludePath:如果新增了目录。
|
||||
|
||||
例如新增:
|
||||
|
||||
```text
|
||||
project/mdk/shader/user_generic_loader_shader_art.c
|
||||
project/mdk/shader/user_generic_loader_shader_art.h
|
||||
```
|
||||
|
||||
需要在 `.uvprojx` 增加 Group,并加入:
|
||||
|
||||
```xml
|
||||
<File>
|
||||
<FileName>user_generic_loader_shader_art.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>.\shader\user_generic_loader_shader_art.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>user_generic_loader_shader_art.h</FileName>
|
||||
<FileType>5</FileType>
|
||||
<FilePath>.\shader\user_generic_loader_shader_art.h</FilePath>
|
||||
</File>
|
||||
```
|
||||
|
||||
如果头文件目录不在 include path,需要加入:
|
||||
|
||||
```text
|
||||
.;..\..\deivers;..\..\application;.\3d;.\fire;.\shader
|
||||
```
|
||||
|
||||
## 构建与优化等级
|
||||
|
||||
除非用户明确指定其它优化等级,处理 ARM-2D / Keil MDK 工程时默认使用 `-Ofast` / `Ofast` 优化等级,并默认勾选 / 开启 Link Time Optimization(LTO,链接时优化)。
|
||||
|
||||
原因:ARM-2D 场景、PFB 刷新、generic loader、RGB565 像素处理和伪 shader 往往对逐像素循环性能非常敏感;在 MCU 上,默认低优化等级很容易把可运行 demo 变成明显掉帧。LTO 可以进一步跨文件内联和裁剪无用代码,通常对 Flash 体积和热路径性能都有帮助。
|
||||
|
||||
执行要求:
|
||||
|
||||
- 新建或调整 Keil 工程配置时,优先设置为 `Ofast`。
|
||||
- 新建或调整 Keil 工程配置时,默认开启 Link Time Optimization / LTO。
|
||||
- 如果用户指定 `O0` / `O1` / `O2` / `O3` / `Os` / `Oz`,以用户指定为准。
|
||||
- 如果用户明确要求关闭 LTO,或工程/库在 LTO 下出现链接错误、弱符号覆盖异常、启动文件/中断向量/段保留问题,则按用户要求或实际问题关闭,并说明原因。
|
||||
- 如果任务目标是调试异常、单步跟踪、定位内存破坏或查看变量,允许临时降低优化等级或关闭 LTO,但必须说明这是为了调试。
|
||||
- 修改优化等级或 LTO 设置后,要在报告里明确写出当前使用的优化等级,以及 LTO 是否开启。
|
||||
- 若 `Ofast` 引发浮点、严格别名、未定义行为或时序相关问题,优先提醒风险并建议针对问题文件局部降级,而不是全工程退回低优化。
|
||||
|
||||
## 构建
|
||||
|
||||
如果本机有 Keil,可以用命令行构建:
|
||||
|
||||
```powershell
|
||||
& 'D:\keil538\UV4\UV4.exe' -b '.\template.uvprojx' -j0 -o '.\Objects\build.log'
|
||||
```
|
||||
|
||||
注意:路径因机器而异,先检查 `UV4.exe` 位置。
|
||||
|
||||
## 报告构建结果
|
||||
|
||||
每次构建后报告:
|
||||
|
||||
```text
|
||||
Program Size: Code=... RO-data=... RW-data=... ZI-data=...
|
||||
0 Error(s), 0 Warning(s)
|
||||
```
|
||||
|
||||
如果和上一次相比 RAM/Flash 变化明显,要说明原因。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 不要只改 `.uvoptx`,那不一定参与构建。
|
||||
- 不要忘记 include path。
|
||||
- 不要把大体积生成 `.c` 盲目加入工程,先评估 Flash。
|
||||
- 如果用户已经能显示素材,不要绕回验证链路,优先解决压缩/运行时问题。
|
||||
@@ -0,0 +1,129 @@
|
||||
# RP2040 / Tufty2040 性能与内存笔记
|
||||
|
||||
## 基本判断
|
||||
|
||||
RP2040 跑 ARM-2D 可以做出很漂亮的小屏 UI,但不要把它当桌面 GPU。
|
||||
|
||||
Keil 编译报告里:
|
||||
|
||||
```text
|
||||
Program Size: Code=... RO-data=... RW-data=... ZI-data=...
|
||||
```
|
||||
|
||||
其中 ZI/RW 只是静态内存视角。运行时还要留给:
|
||||
|
||||
- stack
|
||||
- heap
|
||||
- Arm-2D scratch memory
|
||||
- PFB block buffer
|
||||
- generic loader 内部资源
|
||||
- 外设/DMA/SDK 运行时结构
|
||||
|
||||
所以 ZI 接近上限时,即使编译通过,也可能运行时黑屏或卡死。
|
||||
|
||||
## 高风险写法
|
||||
|
||||
在 per-pixel 路径里尽量避免:
|
||||
|
||||
- 64-bit division
|
||||
- 64-bit multiplication 太密集
|
||||
- 复杂迭代
|
||||
- 每像素 sin/exp/log,即使是 LUT 也要小心周边计算
|
||||
- 每帧全屏 240x240 动态计算
|
||||
- 大尺寸双缓存
|
||||
|
||||
## 推荐写法
|
||||
|
||||
### 低分辨率缓存 + 放大
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
80x80 -> 240x240
|
||||
60x60 -> 240x240
|
||||
```
|
||||
|
||||
优点:
|
||||
|
||||
- 计算量显著下降。
|
||||
- ROI 输出简单。
|
||||
- 可配合双缓存。
|
||||
|
||||
缺点:
|
||||
|
||||
- 画面会糊。
|
||||
- 适合抽象特效,不适合精细 UI。
|
||||
|
||||
### 预计算 map 放 Flash
|
||||
|
||||
例如黑洞/旋涡/极坐标效果:
|
||||
|
||||
- radius map
|
||||
- angle map
|
||||
- distortion map
|
||||
- palette index map
|
||||
|
||||
运行时只做:
|
||||
|
||||
- frame offset
|
||||
- palette lookup
|
||||
- 简单亮度变化
|
||||
|
||||
### Palette animation
|
||||
|
||||
老派 demo-scene 技法,非常适合 MCU。
|
||||
|
||||
特点:
|
||||
|
||||
- SRAM 低。
|
||||
- CPU 低。
|
||||
- 视觉动态强。
|
||||
|
||||
### Tile/block 更新
|
||||
|
||||
如果资源是压缩动画,优先只更新变化 tile。
|
||||
|
||||
## 双核注意事项
|
||||
|
||||
RP2040 有双核,但在 Keil/CMSIS-Pack 工程中要谨慎。
|
||||
|
||||
潜在问题:
|
||||
|
||||
- `multicore_launch_core1()` 依赖 SDK runtime。
|
||||
- core1 stack 配置可能不够。
|
||||
- FIFO 可能和其他 SDK 逻辑冲突。
|
||||
- 调试器/启动代码可能导致时序差异。
|
||||
- core1 重计算可能暴露 IRQ/PIO/DMA 时序问题。
|
||||
|
||||
建议:
|
||||
|
||||
- 先做单核稳定版本。
|
||||
- 双核用编译开关控制。
|
||||
- 如果双核导致黑屏,先关闭验证主链路。
|
||||
|
||||
推荐开关:
|
||||
|
||||
```c
|
||||
#ifndef FEATURE_USE_CORE1
|
||||
# define FEATURE_USE_CORE1 0
|
||||
#endif
|
||||
```
|
||||
|
||||
## 判断瓶颈
|
||||
|
||||
如果显示能跑但帧率低:
|
||||
|
||||
1. 降低缓存分辨率。
|
||||
2. 降低迭代次数。
|
||||
3. 去掉除法。
|
||||
4. 用查表代替计算。
|
||||
5. 减少每帧更新区域。
|
||||
6. 用预计算资源代替实时计算。
|
||||
|
||||
如果直接黑屏/跑不起来:
|
||||
|
||||
1. 降低 ZI。
|
||||
2. 关闭双核。
|
||||
3. 用色条/渐变替代复杂算法验证 loader。
|
||||
4. 检查 Arm-2D scratch/PFB 是否申请失败。
|
||||
5. 检查 scene 是否正确 append。
|
||||
@@ -1,7 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { closeSync, openSync } from "node:fs";
|
||||
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
@@ -30,7 +30,8 @@ const AGENT_WORKSPACE_SEED_FILES = [
|
||||
{ source: "tools/src/runtime-endpoint-resolver.ts", target: "tools/src/runtime-endpoint-resolver.ts" },
|
||||
{ source: "skills/hwpod-cli/SKILL.md", target: ".agents/skills/hwpod-cli/SKILL.md" },
|
||||
{ source: "skills/hwpod-ctl/SKILL.md", target: ".agents/skills/hwpod-ctl/SKILL.md" },
|
||||
{ source: "skills/hwlab-agent-runtime/SKILL.md", target: ".agents/skills/hwlab-agent-runtime/SKILL.md" }
|
||||
{ source: "skills/hwlab-agent-runtime/SKILL.md", target: ".agents/skills/hwlab-agent-runtime/SKILL.md" },
|
||||
{ source: "skills/arm2d-skill", target: ".agents/skills/arm2d-skill", type: "directory" }
|
||||
] as const;
|
||||
|
||||
type EnvLike = Record<string, string | undefined>;
|
||||
@@ -941,10 +942,34 @@ function agentTraceIdentityArtifact(run: PreparedCaseRun, agent: AgentRunStage)
|
||||
});
|
||||
}
|
||||
|
||||
async function collectDirectorySeedFiles(sourceRel: string, targetRel: string, files: AgentWorkspaceFile[]): Promise<void> {
|
||||
const sourceDir = path.join(SOURCE_ROOT, sourceRel);
|
||||
const stack: string[] = [""];
|
||||
while (stack.length > 0) {
|
||||
const sub = stack.pop()!;
|
||||
const dirPath = path.join(sourceDir, sub);
|
||||
const entries = await readdir(dirPath, { withFileTypes: true });
|
||||
for (const dirent of entries) {
|
||||
const relPath = sub ? path.join(sub, dirent.name) : dirent.name;
|
||||
if (dirent.isDirectory()) {
|
||||
stack.push(relPath);
|
||||
} else if (dirent.isFile()) {
|
||||
const filePath = path.join(dirPath, dirent.name);
|
||||
const targetPath = path.join(targetRel, relPath).replace(/\\/g, "/");
|
||||
files.push({ path: targetPath, content: await readFile(filePath, "utf8"), encoding: "utf8" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function agentWorkspaceFilesForRun(run: PreparedCaseRun): Promise<AgentWorkspaceFile[]> {
|
||||
const content = await readFile(run.specPath, "utf8");
|
||||
const files: AgentWorkspaceFile[] = [{ path: ".hwlab/hwpod-spec.yaml", content, encoding: "utf8" }];
|
||||
for (const entry of AGENT_WORKSPACE_SEED_FILES) {
|
||||
if ("type" in entry && (entry as any).type === "directory") {
|
||||
await collectDirectorySeedFiles(entry.source, (entry as any).target, files);
|
||||
continue;
|
||||
}
|
||||
const sourcePath = path.join(SOURCE_ROOT, entry.source);
|
||||
try {
|
||||
files.push({ path: entry.target, content: await readFile(sourcePath, "utf8"), encoding: "utf8" });
|
||||
|
||||
@@ -419,7 +419,8 @@ async function workspaceReplace(args: any) {
|
||||
assertExpectedSha(before, args.expectedSha, relativePath);
|
||||
const find = requiredRawString(args.find, "find");
|
||||
const replace = rawString(args.replace ?? args.replacement);
|
||||
const normalizedFind = normalizeContentLineEndings(find, "\n", false, true);
|
||||
const unescapedFind = String(find).replace(/\\r\\n/gu, "\r\n").replace(/\\n/gu, "\n").replace(/\\r(?!\n)/gu, "\r");
|
||||
const normalizedFind = normalizeContentLineEndings(unescapedFind, "\n", false);
|
||||
const normalizedReplace = normalizeContentLineEndings(replace, "\n", false);
|
||||
const normalizedContent = before.normalizedContent;
|
||||
const occurrences = countOccurrences(normalizedContent, normalizedFind);
|
||||
@@ -443,7 +444,8 @@ async function workspaceInsertAfter(args: any) {
|
||||
assertExpectedSha(before, args.expectedSha, relativePath);
|
||||
const anchor = requiredRawString(args.anchor, "anchor");
|
||||
const insertText = requiredRawString(args.line ?? args.insert ?? args.text, "line");
|
||||
const normalizedAnchor = normalizeContentLineEndings(anchor, "\n", false, true);
|
||||
const unescapedAnchor = String(anchor).replace(/\\r\\n/gu, "\r\n").replace(/\\n/gu, "\n").replace(/\\r(?!\n)/gu, "\r");
|
||||
const normalizedAnchor = normalizeContentLineEndings(unescapedAnchor, "\n", false);
|
||||
const normalizedInsert = normalizeContentLineEndings(insertText, "\n", false);
|
||||
const lines = splitNormalizedLines(before.normalizedContent, before.hadFinalNewline);
|
||||
const anchorLines = splitNormalizedLines(normalizedAnchor, false);
|
||||
@@ -611,7 +613,7 @@ function splitNormalizedLines(content: string, hadFinalNewline: boolean) {
|
||||
}
|
||||
|
||||
function diagnosticDetails(fileState: any, expected: string, relativePath: string) {
|
||||
const expectedLines = splitNormalizedLines(normalizeContentLineEndings(expected, "\n", false, true), false).filter((line) => line.length > 0);
|
||||
const expectedLines = splitNormalizedLines(normalizeContentLineEndings(expected, "\n", false), false).filter((line) => line.length > 0);
|
||||
const normalizedLines = splitNormalizedLines(fileState.normalizedContent, fileState.hadFinalNewline);
|
||||
const candidates = expectedLines.slice(0, 4).flatMap((line) => candidateLines(normalizedLines, line)).slice(0, 12);
|
||||
return {
|
||||
@@ -700,9 +702,8 @@ function lineEndingFromArgs(args: any, fallback: string) {
|
||||
throw cliError("invalid_line_ending", "lineEnding must be preserve, lf, or crlf", { lineEnding: value });
|
||||
}
|
||||
|
||||
function normalizeContentLineEndings(content: string, lineEnding: string, finalNewline: boolean, unescapeEscapeSequences = false) {
|
||||
const base = unescapeEscapeSequences ? String(content).replace(/\\r\\n/gu, "\r\n").replace(/\\n/gu, "\n").replace(/\\r(?!\n)/gu, "\r") : String(content);
|
||||
const normalized = base.replace(/\r\n?/gu, "\n");
|
||||
function normalizeContentLineEndings(content: string, lineEnding: string, finalNewline: boolean) {
|
||||
const normalized = String(content).replace(/\r\n?/gu, "\n");
|
||||
const withFinal = finalNewline && !normalized.endsWith("\n") ? `${normalized}\n` : normalized;
|
||||
return lineEnding === "\r\n" ? withFinal.replace(/\n/gu, "\r\n") : withFinal;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user