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:
Codex Agent
2026-06-07 14:40:48 +08:00
parent fd23405470
commit 712db9c597
21 changed files with 4707 additions and 27 deletions
+2
View File
@@ -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。
## 适用范围
+6 -2
View File
@@ -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
+5 -7
View File
@@ -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 sidecarforwarder 不暴露 Service/NodePort/Ingress/hostPort
- `deepseek` profile 仍指向 DeepSeek bridge/Moon Bridge`codex-api` profile 不依赖 DeepSeek bridge。
完整 CI/CD、GitOps render 或发布流水线只能在目标 Pod 最小闭环已经通过、且当前 CI/CD 基础设施可用时继续。
+3 -10
View File
@@ -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 定义和流程记录。
+1
View File
@@ -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。
+202
View File
@@ -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/公开头文件明确允许访问;必须访问时要先说明版本绑定和维护风险。
+544
View File
@@ -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)
+190
View File
@@ -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}")
+811
View File
@@ -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:])
+111
View File
@@ -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:])
+585
View File
@@ -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 解码。
+677
View File
@@ -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 / ARMCLANGperf_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 屏幕使用 framebufferflush 只更新 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 OptimizationLTO,链接时优化)。
原因: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。
+27 -2
View File
@@ -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" });
+7 -6
View File
@@ -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;
}