news 2026/9/13 14:15:34

Flipper Zero Unleashed Firmware 帧缓冲直访指南:用 canvas_get_buffer() 实现像素级碰撞与全屏渲染

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flipper Zero Unleashed Firmware 帧缓冲直访指南:用 canvas_get_buffer() 实现像素级碰撞与全屏渲染

Flipper Zero Unleashed Firmware 帧缓冲直访指南:用 canvas_get_buffer() 实现像素级碰撞与全屏渲染

【免费下载链接】unleashed-firmwareFlipper Zero Unleashed Firmware项目地址: https://gitcode.com/GitHub_Trending/un/unleashed-firmware

导读

本文以 Unleashed Firmware 官方示例 example_canvas_buffer 为切入点,深入讲解 Canvas 帧缓冲直访 APIcanvas_get_buffer()的设计动机、缓冲布局与读写路径。读者将掌握:如何从普通外部应用(FAP)绕过逐图元绘制接口,直接对 128×64 单色帧缓冲做像素级读回与写入,并理解为什么像素读回(pixel read-back)是实现在屏幕文字与图形之间做精确碰撞检测的唯一合规途径。

示例概述:一个「愤怒的像素」3D 立方体

example_canvas_buffer是一个演示型外部应用:一个线框立方体在屏幕上飞行,撞到屏幕边缘以及画面上已有的文字后反弹。它有两个关键特征:

  • 文字由标准 Canvas API 绘制:示例中通过canvas_draw_str()绘制了 "Angry pixels"、"run!"、"bonk!"、"ouch!"、"the floor is lava" 等文字;
  • 碰撞检测是像素级物理:立方体撞击后,每个发生碰撞的顶点会对自转轴施加一个力矩(r × F),三个轴的转动惯量略有差异,自转轴像不对称陀螺那样缓慢进动,因此翻滚始终是真实的三维运动,而不会退化成扁平的二维旋转。

该应用通过 application.fam 声明为外部应用(apptype=FlipperAppType.EXTERNAL),入口函数为example_canvas_buffer_app,仅依赖gui模块,栈大小为 2KB。

canvas_get_buffer():读与写的统一入口

API 声明与实现

在 canvas.h 中声明了两个配套接口:

/** Get canvas buffer. * * @param canvas Canvas instance * * @return pointer to buffer */ uint8_t* canvas_get_buffer(Canvas* canvas); /** Get canvas buffer size. * * @param canvas Canvas instance * * @return size of canvas in bytes */ size_t canvas_get_buffer_size(const Canvas* canvas);

在 canvas.c 中,两者的实现直通 u8g2 渲染内核:

uint8_t* canvas_get_buffer(Canvas* canvas) { furi_check(canvas); return u8g2_GetBufferPtr(&canvas->fb); } size_t canvas_get_buffer_size(const Canvas* canvas) { furi_check(canvas); return u8g2_GetBufferTileWidth(&canvas->fb) * u8g2_GetBufferTileHeight(&canvas->fb) * 8; }

即:返回的是 Canvas 背后 u8g2 帧缓冲(canvas->fb)的真实指针。也就是说,这一 API 把固件内部渲染器的帧缓冲直接暴露给了外部应用——返回的指针就是当前帧的活缓冲(live framebuffer),同帧内 Canvas API 先画好的内容已经光栅化在其中,外部应用既能在其上叠加绘制,也能把它读回来做像素判断。

缓冲布局:单字节覆盖垂直 8 像素条

返回的帧缓冲是 128×64 的 1-bit 帧缓冲,按 8 页(page)排列,每页是垂直的 8 像素条。寻址公式(示例源码 display.c 中有完整注释):

byte = (y / 8) * width + x bit = y % 8 // LSB 在最上面(bit 0 对应 y%8 == 0 的像素)

以 128×64 屏幕为例,每行 128 个像素由 16 个字节覆盖(128 / 8),总字节数为(64 / 8) * 128 = 1024字节。外部应用完全可以根据此公式把(x, y)映射到缓冲中的字节与位,实现任意像素的读写。

读写两条路径的源码实现

写路径:Bresenham 直线光栅化

示例没有使用canvas_draw_line(),而是自己在帧缓冲上实现了 Bresenham 直线光栅化(display.c):

static void fb_draw_line(uint8_t* fb, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { int32_t dx = abs(x1 - x0); int32_t dy = -abs(y1 - y0); int32_t step_x = x0 < x1 ? 1 : -1; int32_t step_y = y0 < y1 ? 1 : -1; int32_t err = dx + dy; for(;;) { fb_put_pixel(fb, x0, y0); if(x0 == x1 && y0 == y1) break; int32_t e2 = 2 * err; if(e2 >= dy) { err += dy; x0 += step_x; } if(e2 <= dx) { err += dx; y0 += step_y; } } }

底层单像素写入fb_put_pixel直接按缓冲布局置位,越界坐标被忽略:

static inline void fb_put_pixel(uint8_t* fb, int32_t x, int32_t y) { if((uint32_t)x < SCREEN_WIDTH && (uint32_t)y < SCREEN_HEIGHT) { fb[((y >> 3) * SCREEN_WIDTH) + x] |= 1 << (y & 7); } }

读路径:与 Canvas API 同帧渲染的文字做碰撞

读回函数fb_hit(display.c)的语义与写路径对称:先查边界,再把目标字节对应位取出。特别之处在于屏幕外的坐标一律返回 true,这使屏幕边缘天然成为碰撞用的「实心墙」:

static inline bool fb_hit(const uint8_t* fb, int32_t x, int32_t y) { if((uint32_t)x >= SCREEN_WIDTH || (uint32_t)y >= SCREEN_HEIGHT) return true; return (fb[((y >> 3) * SCREEN_WIDTH) + x] & (1 << (y & 7))) != 0; }

碰撞流程位于绘制回调cube_draw_callback(display.c):

  1. 先用canvas_draw_str()绘制文字(由 Canvas API 光栅化进同一帧缓冲);
  2. 调用canvas_get_buffer(canvas)拿到缓冲指针;
  3. 读回阶段:分别对「X 方向推进后的位置」和「Y 方向推进后的位置」探测 8 个投影顶点的像素,确定立方体会在哪个轴向上撞上已绘制像素,同时根据碰撞顶点的位置向量与冲量方向(r × F)累加力矩;
  4. 力矩经各轴不同的逆转动惯量(inv_inertia[3] = {18, 16, 13})换算后叠加到自转角速度上,并做阻尼衰减;
  5. 写回阶段:把 12 条棱按投影坐标直接光栅化进缓冲,叠加在 Canvas API 本帧绘制的内容之上。

帧循环(display.c)则通过furi_message_queue_get等待输入、调用view_port_update请求重绘,Back 键按下即退出。

为什么没有缓冲访问就做不到:Canvas API 是只写的

这是示例文档反复强调的核心论点:公开的 Canvas API 严格只写(write-only)

  • 不存在canvas_get_pixel()之类的像素读回接口——通读 canvas.h 可以看到,全部公开接口都是canvas_draw_*canvas_set_*canvas_width/height这类写侧或查询接口;
  • 字体光栅化发生在固件私有的 u8g2 内核中,外部应用无法通过其他途径获得字形位图(glyph bitmap)。

因此「撞上字母并反弹」这件事,必须回读帧缓冲;而读帧缓冲的唯一官方合规途径,就是canvas_get_buffer()。这正是示例文档所宣称的:这个示例离开了canvas_get_buffer()就无法复现。

演进史:全屏渲染的六种历史方案

示例文档按时间顺序记录了真实项目尝试过的全屏渲染方案(FPS 数据为作者在真实硬件上对全屏动画的测量值):

#方案结论
1仅用 Canvas 图元常常不够用:无像素读回,全屏软件渲染按图元逐条调用太慢
2自有帧缓冲 +canvas_draw_xbm()可行、完全官方,但全屏动画位图路径最高约8 FPScanvas_draw_xbm实现见 canvas.c)
3gui_add_framebuffer_callback()快(实测67+ FPS)但根本性错误:回调是用于 RPC 屏幕流传输的「发送后捕获」钩子
4gui_direct_draw_acquire()/gui_direct_draw_release()合法解决了接管问题,但仍只暴露只写绘制图元,不给原始缓冲
5u8g2_GetBufferPtr(&canvas->fb)hack本地编译可过,但发布 FAP 不可能:API 检查拒绝该符号,只有用./fbt在树内构建才可达
6canvas_get_buffer()/canvas_get_buffer_size()公开 API本示例:普通 FAP 即可获得全速读写访问,且保留正常的 ViewPort 输入处理与合成

为什么回调方案在原理上就是错的

方案 3 的缺陷由 canvas.c 的实现直接坐实——canvas_commit()u8g2_SendBuffer()把帧发给屏幕,然后才遍历执行提交回调:

void canvas_commit(Canvas* canvas) { furi_check(canvas); u8g2_SendBuffer(&canvas->fb); // Iterate over callbacks canvas_lock(canvas); for M_EACH(p, canvas->canvas_callback_pair, CanvasCallbackPair_t) { p->callback( canvas_get_buffer(canvas), canvas_get_buffer_size(canvas), canvas_get_orientation(canvas), p->context); } canvas_unlock(canvas); }

因此:回调里的写入晚了一帧(下一次canvas_reset()会清掉),应用也从未真正持有 GUI 焦点(输入事件不被捕获),ViewPort 与桌面还会反复争夺屏幕。GUI 的合成与重绘流程gui_redraw(gui.c)也印证了这一点:正常路径是canvas_reset → 各层 ViewPort 绘制 → canvas_commit,外部回调没有参与任何一层的合成决策。

历史反例代码(仅供参考,勿用于新代码)

示例文档完整保留了当年 Arduboy 运行时早期构建中的提交回调写法,这里原样继承:

void rt_framebuffer_commit_callback( uint8_t* data, size_t size, CanvasOrientation orientation, void* context) { ArduboyRuntimeState* state = (ArduboyRuntimeState*)context; if(!state || !data) return; if(size < RuntimeBufferSize) return; (void)orientation; const uint8_t* src = state->screen_buffer; bool inverted = __atomic_load_n((bool*)&state->screen_inverted, __ATOMIC_ACQUIRE); for(size_t i = 0; i < RuntimeBufferSize; i++) { if(inverted) { data[i] = src[i]; } else { data[i] = (uint8_t)(src[i] ^ 0xFF); } } if(state->pending_clear) { memset(state->screen_buffer, 0x00, RuntimeBufferSize); state->pending_clear = false; } }

它曾在那个时代的 GUI 内部实现上达到 67+ FPS,但在当前固件上无法正确工作:回调运行在帧已发送给显示屏之后、应用收不到输入、ViewPort 合成也会被打乱。示例文档将其记录为「历史上能跑、但逻辑不正确」的反例。

合法的中途方案:gui_direct_draw_acquire / release

方案 4 中的直接绘制接管接口至今仍存在于 gui.h 与 gui.c。其合法之处在于:GUI 暂停 ViewPort 合成、把 Canvas 交给应用,输入则通过RECORD_INPUT_EVENTSpubsub 获取。但它的局限同样明确——它只给出只写绘制图元,而非原始缓冲,因此无法满足像素读回的需求。gui_redrawif(gui->direct_draw) break;的存在也说明直接绘制模式下 GUI 会跳过正常合成流程,与常规 ViewPort 应用的输入/合成语义不同。

构建与运行

示例作为外部应用(FAP)构建,在仓库根目录执行:

./fbt fap_example_canvas_buffer

构建产物可拷贝到 SD 卡以 FAP 形式加载。应用清单(application.fam)中的关键字段:

  • appid="example_canvas_buffer",显示名[GUI] 3D cube
  • apptype=FlipperAppType.EXTERNAL,入口example_canvas_buffer_app
  • requires=["gui"]stack_size=2 * 1024
  • 图标为cube_10px.png,归类在Examples类别下。

运行后立方体会在全屏层(GuiLayerFullscreen,见 display.c)上飞行并反弹,按 Back 键退出;退出时应用会依次释放 ViewPort、关闭 GUI 与通知记录,并把背光恢复为自动模式(sequence_display_backlight_enforce_auto)。

对 FAP 开发者的实践结论

  • 需要像素读回时canvas_get_buffer()是唯一官方途径,配合canvas_get_buffer_size()使用(后者返回缓冲字节数,示例中即为 1024 字节);
  • 缓冲只应在绘制回调内触碰:示例源码头部注释明确强调,绘制回调执行期间缓冲内容对当前帧有效(display.c);
  • 读回与写回可以在同一帧内共存:先让 Canvas API 绘制文字/图形,再读回做碰撞/命中测试,最后直接写缓冲叠加自定义渲染——这正是本示例展示的标准范式;
  • 不要使用提交回调或 u8g2 内部 hack:前者晚一帧且不持有焦点,后者无法通过 FAP 的 API 符号检查,无法以外部应用形态发布。

延伸阅读

  • 示例文档:applications/examples/example_canvas_buffer/README.md
  • 完整示例源码:applications/examples/example_canvas_buffer/display.c
  • 应用清单:applications/examples/example_canvas_buffer/application.fam
  • Canvas API 声明:applications/services/gui/canvas.h
  • Canvas 实现(含canvas_commit回调时序):applications/services/gui/canvas.c
  • GUI 合成与直接绘制接管:applications/services/gui/gui.c、applications/services/gui/gui.h

【免费下载链接】unleashed-firmwareFlipper Zero Unleashed Firmware项目地址: https://gitcode.com/GitHub_Trending/un/unleashed-firmware

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 14:14:12

Zotero PDF选不中文字?从类型判断到插件排查的完整解决指南

1. 问题定位&#xff1a;先搞清你的PDF到底是"哪坏了"每次有朋友跑来问我&#xff1a;"Zotero里附件PDF打开了&#xff0c;但是鼠标怎么点都选不中文字&#xff0c;翻译插件也罢工"&#xff0c;我的第一反应永远是让他们先别急着重装软件。这个问题的坑点其…

作者头像 李华
网站建设 2026/9/13 14:13:39

多模态与视觉大模型开发实战:从概念、微调到RAG与Agent落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 14:09:59

Boss直聘岗位数据分析实战:从接口采集到可视化全流程解析

简介&#xff1a;面向求职平台数据研究场景的 Python 数据分析毕业设计项目&#xff0c;围绕 Boss直聘热门城市岗位信息&#xff0c;完整实现数据采集、清洗、分析与可视化流程。项目基于 Scrapy 爬虫框架抓取岗位数据并以 CSV 格式落盘&#xff0c;针对高耦合脏数据设计预处理…

作者头像 李华
网站建设 2026/9/13 14:09:54

DataEase柱形图实战:从数据源到仪表板的完整制作流程

做数据可视化这几年&#xff0c;我上手过不少工具&#xff0c;从 Excel 折腾到开源 BI&#xff0c;再到各种在线平台。DataEase 是其中一个让我愿意持续用下去的&#xff1a;开源免费&#xff0c;部署简单&#xff0c;图表类型覆盖日常 80% 的需求。这一篇我直接聚焦一个最基础…

作者头像 李华