- 编译器
- 图像处理
- 编程语言
- 高性能计算
【免费下载链接】Halide
a language for fast, portable>项目地址:https://gitcode.com/gh_mirrors/ha/Halide
导读
本文以 Halide 仓库中的 apps/HelloiOS 为蓝本,深入讲解如何把用 Halide 语言编写的图像/数值内核,通过 Generator 机制交叉编译为 iOS 静态库,再以 Xcode 工程 + Metal 渲染的方式跑在 iPhone 真机与模拟器上。读完本文,你将掌握:从零构建 Halide 本体并配置Halide_ROOT的完整流程;setup.sh一次性引导与HalideKernels.xcframework的打包机制;反应扩散(reaction-diffusion)三阶段生成器的算法与 CPU/GPU 调度细节;以及 Objective-C++ 应用中 Metal 与 CPU 双后端的运行时切换原理。
项目概览:一个用 Halide 驱动的交互式反应扩散 iOS 应用
apps/HelloiOS/README.md 明确将该示例定位为 "An interactive reaction-diffusion iOS demo powered by Halide"——一个由 Halide 计算内核驱动的、可触控交互的 iOS 反应扩散模拟。所谓反应扩散,是一类用于模拟形态生成(morphogenesis)的偏微分方程系统,能在屏幕上演化出流动、斑斓的图案;本示例还支持手指触摸,触摸点会"加速反应"形成扰动。
从工程架构看,整个示例被清晰地拆成两部分(这也是 README "How It Works" 一节的骨架):
- Generators/—— 一个 CMake 工程,在宿主机(Mac)上编译 Halide Generator,并运行它以产出面向 iOS 的静态库与头文件。每个反应扩散内核都同时为
iphoneos(真机)和iphonesimulator(模拟器)两种平台生成,这样你在 Xcode 中切换 Destination 时无需重新跑 setup。 - HelloiOS/—— iOS 应用的原生 Xcode 工程,负责编译 Objective-C++ 源码并链接上述生成的 Halide 库。应用用Metal渲染反应扩散模拟,双击屏幕可切换到CPU后端作为对照。
两部分共同存在于顶层工作区HelloiOS.xcworkspace中。工作区文件 HelloiOS.xcworkspace/contents.xcworkspacedata 实际引用了两个工程:HelloiOS/HelloiOS.xcodeproj与 CMake 生成在Generators/build下的HelloiOS-Generators.xcodeproj。一次性的setup.sh负责配置 CMake 侧并引导出HalideKernels.xcframework(一个同时打包真机与模拟器内核、运行时和头文件的二进制框架);Xcode 工程直接链接该 xcframework,并且 HelloiOS target 里有一个名为Build Halide Generators的 Run Script 构建阶段(见 project.pbxproj,其脚本为xcodebuild -project ".../Generators/build/HelloiOS-Generators.xcodeproj" -target HalideKernels -configuration Release),每次构建都会重建它,从而自动拾取生成器的改动。
环境准备:先构建并安装 Halide 本体
README 的 "Prerequisites" 指出,使用 HelloiOS 之前必须先构建(或安装)Halide。命令如下:
cd /path/to/Halide cmake -S . -B build -DCMAKE_BUILD_TYPE=Release cmake --build build cmake --install build --prefix install三点需要特别说明:
- 这里
/path/to/Halide指向 Halide 仓库源码根目录,--prefix install指定了安装前缀,后续的Halide_ROOT环境变量要指向该前缀。 - 本项目的 CMake 侧要求较高版本的工具链:Generators/CMakeLists.txt 声明了
cmake_minimum_required(VERSION 3.28),C++ 标准要求 C++17(CMAKE_CXX_STANDARD 17)。 - 由于生成器需要运行在宿主机上并交叉产出 iOS 代码,这一步必须在 macOS 上完成,且需要安装 Xcode 及对应的 iOS SDK。
快速开始:setup.sh 引导 + Xcode 构建
README 的 "Quick Start" 给出三条命令:
cd apps/HelloiOS Halide_ROOT=/path/to/halide/install ./setup.sh open HelloiOS.xcworkspace然后,在 Xcode 中选择HelloiOSscheme,挑一个 Destination(真机或模拟器),直接 Build 即可。
setup.sh实际做了两件关键事(见 setup.sh):
- 参数校验:脚本开头
: "${Halide_ROOT:?Set Halide_ROOT in the environment to the Halide install prefix.}"会在Halide_ROOT未设置时立即报错退出——这正是 README 环境变量表格中将其标记为(required)的原因。 - 引导生成(bootstrap):执行
cmake -G Xcode -S Generators -B Generators/build生成 Xcode 工程,再cmake --build Generators/build --config Release构建内核并把HalideKernels.xcframework打包好,这样 Xcode 才能正常加载工程。之后的构建则由 Xcode 的 Run Script 阶段驱动,不需要再手动跑 setup。
脚本结束时的输出还提示了两个对实操很重要的信息:
- 真机运行需要在 Xcode 工程设置的Signing & Capabilities页勾选 "Automatically manage signing" 并设置你的开发团队(Development Team)。
- 已知问题(issue #9049):修改生成器源码后,需要在 Xcode 中 build-and-run 两次才能看到效果。
Generator 侧:CMake 如何生成内核与打包 xcframework
Generators/CMakeLists.txt 是理解整个交叉编译流水线的核心。它依次做了四件事:
find_package(Halide REQUIRED)定位安装好的 Halide,暴露add_halide_generator、add_halide_library、add_halide_xcframework等 CMake 助手函数。add_halide_generator(reaction_diffusion_2_generator SOURCES reaction_diffusion_2_generator.cpp)把生成器源码编译成宿主机可执行程序。- 双层循环:外层遍历
""与"-simulator"两个平台后缀,内层遍历init、update、render三个阶段,为每个组合调用两次add_halide_library:- 普通 CPU 版本:
TARGETS "arm-64-ios${simulator}",FEATURES user_context; - Metal 版本:
TARGETS "arm-64-ios${simulator}",FEATURES metal user_context。 两者的OUTPUT_DIR均为arm-64-ios${simulator},但通过FUNCTION_NAME/FILE_BASE_NAME区分(例如reaction_diffusion_2_metal_update),最终产出reaction_diffusion_2_*.h头文件与对应的静态库。user_contextfeature 意味着生成的内核会接受一个用户上下文指针参数,供应用侧传递 Metal device/command queue。
- 普通 CPU 版本:
add_halide_xcframework(HalideKernels LIBRARIES ALL)把上述全部产物打成单一HalideKernels.xcframework,同时包含真机与模拟器 slice,这正是 README 所说"bundles the generated kernels, runtimes, and headers for both device and simulator"的实现依据。
也就是说,一共会生成 6 个 add_halide_library 目标(3 阶段 × CPU/Metal × 真机/模拟器共 12 个库文件,但以 6 组命名),最终统一收进 xcframework。
反应扩散内核:三个生成器的算法与调度
生成器全部位于 Generators/reaction_diffusion_2_generator.cpp,文件末尾通过三行HALIDE_REGISTER_GENERATOR(第 L195-L197 行)注册了三个阶段:reaction_diffusion_2_init、reaction_diffusion_2_update、reaction_diffusion_2_render。
init:随机初始状态
ReactionDiffusion2Init(第 L5-L27 行)极其简洁:output(x, y, c) = Halide::random_float();用一个随机数填充每个像素的 RGB 通道,作为模拟的初始条件。它的schedule()只在目标带 GPU feature 时生效:reorder(c, x, y)把通道 c 提到最内层,bound(c, 0, 3)限定三通道,vectorize(c)向量化,再gpu_tile(x, y, xi, yi, 4, 4)按 4×4 tile 映射到 GPU;同时通过set_stride强制内存布局为 interleaved(RGB 连续、channel stride 为 1),保证与 Metal 纹理和 CPU 后端的数据布局一致。
update:扩散、反应与触摸扰动
ReactionDiffusion2Update(第 L29-L127 行)是模拟的主循环,输入为当前state、触摸坐标mouse_x/mouse_y、帧号frame,输出new_state。算法分三步:
- 边界条件:
clamped = Halide::BoundaryConditions::constant_exterior(state, random_float(frame)),把越界像素当作随机值,既避免边界伪影又带随机性。 - 扩散:
blur_x/blur_y分别取 x、y 方向间隔 3 的五个采样点求和,blur = (blur_x + blur_y) / 10,实现各向同性的拉普拉斯式扩散。 - 反应:先对 R/G/B 分别做 sigmoid 式增益
(1-s) + s*C*(3-2*C)将颜色"向外推",再按经典 Gray-Scott 风格方程组更新:dR = B*(1-R-G)、dG = (1-B)*(R-G)、dB = 0.5*(1-B+2*G*R-R-G),最后clamp到[0,1]。
触摸交互体现在第 L71-L77 行:以触摸点为圆心计算距离平方radius,扰动幅度bump = 0.002 * 状态宽 * 状态高 / max(1, radius),并在mouse_x < 0(无触摸)时置零;t = 0.04 + bump作为反应步长,触摸点附近反应被极大加速。输出用mux(c, {R,G,B})组合成三通道缓冲。
调度上,CPU 与 GPU 各有一套策略:GPU 路径(第 L97-L108 行)中blur以compute_at(new_state, xi)内联进 tile,new_state.gpu_tile(x, y, xi, yi, 8, 2),并显式声明state/new_state的 interleaved stride;CPU 路径(第 L109-L121 行)则split(y, y, yi, 32).parallel(y)并行化、vectorize(x, natural_vector_size<float>())向量化,clamped用store_in(MemoryType::Stack)放进栈内存并store_at/compute_at控制其生命周期。
render:轮廓提取与 BGRA/RGBA 输出
ReactionDiffusion2Render(第 L129-L191 行)把浮点状态映射成可显示的像素。先算轮廓强度contour = pow(state*(1-state)*4, 2)(状态值接近 0 或 1 时轮廓趋零、在 0.5 附近最强),再映射为 R/G/B/A 通道。代码中第 L149-L163 行有一个值得注意的设计:Metal 与 CGImage 需要不同的像素布局,因此同时计算bgra与rgba两种mux结果,用select(output_bgra == true, bgra, rgba)二选一;调度里再通过render.specialize(output_bgra == true)与render.specialize(output_bgra == false)生成两条特化路径,运行时按参数值直接跳转,避免多余计算(这也是该生成器以Input<int> output_bgra而非Input<bool>的原因——源码第 L132 行注释说明这是为规避 Issue #1760 的 workaround)。
渲染调度的 GPU 路径(第 L169-L175 行)同样reorder(c, x, y).unroll(c).gpu_tile(x, y, xi, yi, 32, 4);CPU 路径(第 L176-L184 行)vectorize(x)+split(y, y, yi, 64).parallel(y)。输出render被声明为 stride 4 的 4 通道 uint8 缓冲(RGBA/BGRA),为下一步 blit 到MTLPixelFormatBGRA8Unorm纹理做好准备。
App 侧:Objective-C++ 如何驱动 Halide 内核与 Metal
iOS 应用的渲染核心是 HelloiOS/HalideView.mm。它展示了 Halide 与 Metal 集成的标准姿势:
- 函数指针表:第 L17-L31 行定义了
HalideFuncs结构,持有init/update/render三个函数指针,并用两套静态表kHalideCPU与kHalideMetal分别指向reaction_diffusion_2_*和reaction_diffusion_2_metal_*六组由 CMake 生成的头文件所声明的入口。双击屏幕(touchesBegan中tapCount > 1)翻转use_metal标志即可在两者间切换。 - Metal 设备上下文:第 L272-L282 行实现了
halide_metal_acquire_context/halide_metal_release_context两个 C 函数,通过user_context指针把MTLCreateSystemDefaultDevice()创建的 device 和 command queue 传给 Halide Metal 运行时——这正是 CMake 里FEATURES user_context的落地。 - 双缓冲与像素缓冲:
initBufsWithWidth:height:using_metal:(第 L86-L113 行)为 Metal 路径创建make_interleaved(stride 3)缓冲、为 CPU 路径创建普通平面缓冲;pixel_buf则按 BGRA、stride 4、行末 64 字节对齐填充分配,并用crop去掉多余填充。 - 每帧流程:
initiateRender(第 L199-L264 行)先按需重建缓冲并调用init重置状态,随后renderOneFrame调用update(传入触摸坐标与帧号)和render(output_bgra = true),用CACurrentMediaTime()计时(帧耗时经 IIR 平滑后显示在底部 UILabel,见updateFrameTime:);最后取出halide_metal_get_buffer得到的 MTLBuffer,通过MTLBlitCommandEncoder把像素拷贝进CAMetalLayer的 drawable 纹理,命令缓冲完成回调里再调度下一帧,形成持续渲染的循环。值得留意的是每帧实际调用了两次renderOneFrame(第 L226-L228 行),源码注释解释了原因:Metal launch 存在较大的最小延迟,单次调用测得的时间会低估 GPU 吞吐,多跑几遍能收敛到更真实的稳态成本。
视图控制器 HelloiOS/HalideViewController.mm 负责把HalideView设为根视图、默认开启 Metal(use_metal = YES)、在底部安全区挂一个白色统计 Label,并在viewWillAppear触发首帧渲染。
在模拟器中启动运行
README 的 "Launching in Simulator" 给出了不依赖 Xcode 图形界面的命令行启动方式:
open -a Simulator xcrun simctl bootstatus "iPhone 16" -b xcrun simctl install booted HelloiOS/build/Release-iphonesimulator/HelloiOS.app xcrun simctl launch booted org.halide.HelloiOS说明:simctl bootstatus -b会阻塞等待指定设备(示例中的 "iPhone 16",可按你本机的模拟器设备名替换)完成启动;install把构建产物.app装进已启动的模拟器;launch使用应用 Bundle Identifierorg.halide.HelloiOS拉起应用。也可以更简单:在 Xcode 中为模拟器 Destination 构建后直接点 Run 按钮。
项目结构总览
README 给出的目录树与仓库实际内容完全一致,补充实际文件后如下:
apps/HelloiOS/ HelloiOS.xcworkspace/ 顶层工作区(引用 App 工程与 Generators 生成的工程) Generators/ CMakeLists.txt 构建生成器并打包 HalideKernels.xcframework reaction_diffusion_2_generator.cpp init/update/render 三阶段生成器 build/ 由 setup.sh 创建(含 HelloiOS-Generators.xcodeproj) HelloiOS/ HelloiOS.xcodeproj/ 原生 Xcode 工程(含 "Build Halide Generators" Run Script 阶段) AppDelegate.h / AppDelegate.mm 应用入口(Scene 生命周期管理) SceneDelegate.h / SceneDelegate.mm HalideView.h / HalideView.mm Metal/CPU 渲染核心(双缓冲、触控、帧率统计) HalideViewController.h / HalideViewController.mm main.mm 程序入口 HelloiOS-Info.plist Info 配置(要求 arm64,场景清单,全方向支持) HelloiOS-Prefix.pch Images.xcassets/ 应用图标资源 setup.sh 一次性引导脚本从 HelloiOS-Info.plist 还可以看到,应用通过UIRequiredDeviceCapabilities声明arm64,并通过UIApplicationSceneManifest指定SceneDelegate管理场景。
常见问题排查
README 的 "Troubleshooting" 记录了一个已知坑:旧版本 Xcode 可能误判生成器二进制是为 iOS 构建的,进而要求对生成器本身进行签名。如果遇到这种误判,终极手段是在 HelloiOS target 的 "Build Halide Generators" 构建阶段,给其中的xcodebuild调用加上如下前缀:
env -i USER="$USER" HOME="$HOME" PATH="$PATH" DEVELOPER_DIR="$DEVELOPER_DIR"README 同时明确警告:这是最后手段,因为它会丢掉全部构建上下文,可能在未来引发其他问题,务必只在必要时使用。另外,前面 setup.sh 输出中提到的 issue #9049(修改生成器后需 build-and-run 两次)也属于构建阶段的高频问题,遇到"改了生成器却没生效"时可先按此排查。
小结:一个可复用的 iOS + Halide 落地模板
HelloiOS 的价值不只在演示反应扩散本身,更在于它完整示范了一条可复用的技术路径:Generator 源码 → 宿主机 CMake 交叉编译(真机+模拟器、CPU+Metal 多组合)→ xcframework 统一打包 → Xcode Run Script 自动重建 → Objective-C++ 通过 user_context 接入 Metal 运行时 → Metal blit 上屏。无论是想为 iOS 应用引入 Halide 做实时图像处理,还是研究 Halide 的 Metal 后端与多目标构建,apps/HelloiOS 都是一个可以直接对照仓库源码阅读与改造的起点。
- 编译器
- 图像处理
- 编程语言
- 高性能计算
【免费下载链接】Halide
a language for fast, portable>项目地址:https://gitcode.com/gh_mirrors/ha/Halide
相关推荐
用 Halide 将反应-扩散流水线 AOT 编译为 WebAssembly:HelloWasm 实例深度解析
用 Halide 将反应 扩散流水线 AOT 编译为 WebAssembly:HelloWasm 实例深度解析 本文以 Halide 仓库中 apps/Hell
编译器图像处理编程语言高性能计算用 Halide 构建 PyTorch 算子扩展:HelloPyTorch 应用全解(pytorch_wrapper 封装、梯度回传与 CUDA 集成)
用 Halide 构建 PyTorch 算子扩展:HelloPyTorch 应用全解(pytorch_wrapper 封装、梯度回传与 CUDA 集成) 本指南
编译器图像处理编程语言高性能计算RisingWave 集成测试 Demo 指南:生态场景实战与 datagen 数据生成器全解析
RisingWave 集成测试 Demo 指南:生态场景实战与 datagen 数据生成器全解析 RisingWave 仓库的 integration_test
数据库流处理后端数据工程