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 声明为外部应用apptypeFlipperAppType.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.cstatic 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_hitdisplay.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_callbackdisplay.c先用canvas_draw_str()绘制文字由 Canvas API 光栅化进同一帧缓冲调用canvas_get_buffer(canvas)拿到缓冲指针读回阶段分别对「X 方向推进后的位置」和「Y 方向推进后的位置」探测 8 个投影顶点的像素确定立方体会在哪个轴向上撞上已绘制像素同时根据碰撞顶点的位置向量与冲量方向r × F累加力矩力矩经各轴不同的逆转动惯量inv_inertia[3] {18, 16, 13}换算后叠加到自转角速度上并做阻尼衰减写回阶段把 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.c3gui_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_redrawgui.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_redraw中if(gui-direct_draw) break;的存在也说明直接绘制模式下 GUI 会跳过正常合成流程与常规 ViewPort 应用的输入/合成语义不同。构建与运行示例作为外部应用FAP构建在仓库根目录执行./fbt fap_example_canvas_buffer构建产物可拷贝到 SD 卡以 FAP 形式加载。应用清单application.fam中的关键字段appidexample_canvas_buffer显示名[GUI] 3D cubeapptypeFlipperAppType.EXTERNAL入口example_canvas_buffer_apprequires[gui]stack_size2 * 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.famCanvas API 声明applications/services/gui/canvas.hCanvas 实现含canvas_commit回调时序applications/services/gui/canvas.cGUI 合成与直接绘制接管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),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →