# Harbeth: a texture-first Metal engine for image, video and camera pipelines on Apple platforms

> Harbeth is a Swift and Metal rendering library for Apple platforms that turns an image or frame description into a GPU result, with two integration routes, a direct IO path and a graph-based node path. It is a rendering core, not a product, and its API surface assumes you already own the media lifecycle.

**yangKJ/Harbeth** — 🎨 GPU accelerated image / video and camera filter library based on Metal. Support macOS & iOS. 图像、视频、相机滤镜框架

- Repository: https://github.com/yangKJ/Harbeth
- Stars: 722 · Forks: 92
- Language: Metal
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/yangkj-harbeth

## A rendering core, with the product decisions left to the host

Harbeth is described as a texture-first Metal render engine for image and frame pipelines on Apple platforms, and the README is unusually clear about what it is not. It is the rendering core inside a product, not the product workflow itself. The host decides interaction, media lifecycle, persistence and product policy, while Harbeth turns one image or frame description into a GPU-rendered result with explicit output semantics.

That boundary is the single most important thing to understand before reading any API. If you are looking for a filter library that comes with a picker, a slider and a save button, this is the wrong shape. What it gives you is a set of composable filters, render graphs, masks, transitions, geometry and optics primitives, output contracts, diagnostics and preview hosting, and it deliberately leaves the application around them to you.

The input side is broad. The README lists the source types it processes: `UIImage` and `NSImage`, `CGImage`, `CIImage`, `MTLTexture`, `CVPixelBuffer` and `CMSampleBuffer`. That covers still images, Core Image graphs, textures, camera pixel buffers and video sample buffers, which is the range you need for a photo editor, a video filter chain and a live camera effect in one codebase.

The audience is a developer writing a real-time image or video product on iOS, iPadOS, macOS or tvOS who has already made the decision to use Metal and wants the filter graph, the contract handling and the preview plumbing handled by a library rather than by hand.

## Two integration routes, and choosing between them is the first decision

Harbeth exposes two normal integration routes, and the README stresses that the runtime, analysis, recipes and preview hosts support these routes rather than adding extra entry points.

The first is `HarbethIO`, for the case where you already have a source and a filter chain. Its canonical result is `try output()` or `await transmitOutput()`. This is the direct path: a source in, a filter list, an image out.

The second is `ImageNode`, for the case where you need the advanced unified graph with editing, contract, inspection and delivery surfaces. Its canonical results are `makeTexture()`, `makeFrame()` and `makeFrameAsync()`. This is the structured path, where the render description carries more than a one-shot filter chain: reusable sources, structured edits, output intent, metadata, cache policy, diagnostics or multiple delivery forms.

The source and result map in the README shows the practical difference. With `HarbethIO`, a `UIImage` or `NSImage` comes back as the same image type, a `CGImage` or `CIImage` comes back as the same source type, and an `MTLTexture` comes back as an `MTLTexture`. With `ImageNode`, the same inputs become a texture, a frame, or an image readback. Encoded data and `ImageAsset` are the exception, where the README says to use `ImageNode` for decoded assets.

So the choice is between keeping your own types and getting a result fast, and moving to a graph model that carries metadata and delivery semantics. Trying to do the second with the first route is what the two-route design is meant to prevent.

## Installing with Swift Package Manager or CocoaPods

The library is distributed through both of the normal channels for Apple platforms. The Swift Package Manager declaration is:

```swift
dependencies: [
    .package(url: "https://github.com/yangKJ/Harbeth.git", from: "3.0.0")
]
```

The README says to add `Harbeth` to the target that renders images or frames, which is the only integration step the package manager route needs.

The CocoaPods route is a single line:

```ruby
pod 'Harbeth', '~> 3.0'
```

The toolchain requirements are where the real constraint sits. The README lists minimum platform versions of iOS and iPadOS 15.0, macOS 12.0 and tvOS 15.0, and a toolchain of Xcode 16 or newer with Swift 6. The installation section then tightens that: build with Xcode 26 or later, meaning Swift 6.2 and Apple platform SDKs 26 or later, while the deployment targets remain iOS and iPadOS 15, macOS 12 and tvOS 15. CI is pinned to Xcode 26.3 and the Swift language mode stays at 6.

Read that as two separate facts. The library runs on the same systems as before, but you need a recent compiler to build it. The project also ships a podspec, a Package.swift, an Xcode workspace and project, a Demo directory with iOS, macOS and SwiftUI demo apps, a Tests directory, a CHANGELOG, a Document directory and a docs directory, which is the shape of a library that is developed with Xcode rather than purely from the command line.

Note also that the README is bilingual, with a Simplified Chinese version, and the description of the project lists Core Image, camera, VR and graphics among its topics, so the library sits in a wider Apple graphics ecosystem than filters alone.

## HarbethIO: synchronous output, async transmit, and one real-time switch

The direct route is a filter chain applied to a source. The synchronous form looks like this:

```swift
let outputImage = try HarbethIO(
    element: inputImage,
    filters: [
        C7Exposure(exposure: 0.25),
        C7Contrast(contrast: 1.08),
        C7Saturation(saturation: 0.94)
    ]
).output()
```

The README explains why `output()` is the primary synchronous API: rendering failures remain observable. Nothing is swallowed on the way back, which is the property you want when a filter chain can fail on a bad input.

The asynchronous counterpart returns the same image type without blocking the caller:

```swift
let outputImage = try await HarbethIO(
    element: inputImage,
    filters: filters
).transmitOutput()
```

Callback-based integrations have their own entry point, `transmitOutput(outputColorSpace:complete:)`. Two behaviours matter here. Filtered work is encoded on Harbeth's render operation queue and completes after GPU completion under the default profile, so the callback fires after the GPU is actually done. The callback queue is unspecified, which means UI updates must return to `MainActor`. The no-filter fast path may complete inline.

For low-latency texture pipelines there is a single public control, the `transmitOutputRealTimeCommit` switch:

```swift
var io = HarbethIO(
    element: inputTexture,
    filters: filters
)
io.transmitOutputRealTimeCommit = true
let outputTexture = try await io.transmitOutput()
```

When the switch is true, asynchronous texture-first output may be delivered after its command buffer is scheduled rather than completed. The switch does not change synchronous `output()` behaviour, and image, pixel buffer and sample buffer outputs still wait for GPU completion before CPU readback even when it is enabled. That is a precise contract, and it is the difference between a texture pipeline that can run ahead and one that cannot.

## ImageNode: a graph that carries metadata, cache policy and output intent

`ImageNode` is the advanced unified entry, used when a render description must carry more than a one-shot filter chain. The chaining style reads like a builder:

```swift
let node = ImageNode.image(inputImage)
    .applying(C7Exposure(exposure: 0.25))
    .applying(C7Contrast(contrast: 1.08))
    .transforming(ImageTransformRecipe(rotationDegrees: 90))
    .withCachePolicy(.transient)
```

and the delivery calls are chosen by what the caller needs:

```swift
let previewFrame = try node.makeFrame(profile: .stablePreview)
let backgroundFrame = try await node.makeFrameAsync(profile: .stablePreview)
let exportTexture = try node.makeTexture(profile: .exportQuality)
```

Use `makeTexture()` for a texture result, `makeFrame()` when metadata and host delivery matter, `makeFrameAsync()` when the caller must not block, and `makeRenderRequest()` when execution must be deferred or inspected.

The capability list is where the difference from a filter library becomes obvious. Unified sources cover image, `CGImage`, `CIImage`, texture, pixel buffer, sample buffer, encoded data and `ImageAsset`. Composable processing includes filters, explicit kernel contracts, plugins, cache policy and sampler policy. Structured editing covers `EditRecipe`, Geometry, Optics, local effects, gradient, shape, path and composite masks, and reusable preview and final modes. Multi-source composition provides transitions and ordered layer composites with masks, transforms, blend contracts and output contracts.

The delivery semantics are stated as a property rather than a feature: `RenderProfile`, `ImageDerivativeSpec`, and color, alpha, orientation and source-tier metadata travel with `RenderedFrame` along with texture ownership. Inspection and replay cover the image graph, diagnostics, debug snapshots, deferred `RenderRequest`, histogram and statistics probes, masks and output attachments. When a node already exists, the README asks you to prefer instance chains such as `node.editing(...)`, `node.transforming(...)` and `node.applying(optics: ...)`, and it is explicit that recipe families are editing-description primitives inside the `ImageNode` route rather than a third public route.

## Preview hosting, and the boundaries the README leaves open

The point of a render engine is to see the result, and Harbeth includes preview hosting for that. UIKit and AppKit can host a `RenderedFrame` directly:

```swift
renderView.display(previewFrame)
```

The README then notes that SwiftUI can use the same Harbeth result, though the section is cut off in the version of the documentation available here, so the exact SwiftUI call is not something this review can confirm.

Other things the documentation does not settle are worth knowing before you commit. The README does not document a rollback procedure for the package, and it does not describe how a host should persist or cache results, which is consistent with the stated division of labour where persistence belongs to the host rather than the library. Cache policy appears inside the node route as `.withCachePolicy(.transient)`, but the eviction behaviour behind different policies is not described.

The `RenderProfile` values used in the examples, `.stablePreview` and `.exportQuality`, are the visible surface of that concept. What differs between profiles beyond the names is not documented in the README, so a team that needs to reason about quality versus latency has to read the sources or the Document and docs directories in the repository.

There is also a diagnostics story, with histograms, statistics and colour probes, and an attachments mechanism, which is what you would reach for when a filter chain produces something unexpected and you need to see the intermediate state rather than guess.

## MIT licensed, versioned releases, and a fast moving Swift core

The licence is MIT, and the README links the LICENSE file for the full terms. That is a factual reading of the identifier and not legal advice, but a permissive licence with no copyleft obligations is a straightforward choice for a commercial graphics library, and it means you can vendor Harbeth into a closed source app without the licensing gymnastics some Apple frameworks impose.

The release history shows a project moving quickly. Version 1.3.1 was published on 2026-02-12 with a note about an ExternalMTLLibraryProvider, version 2.0.0 on 2026-03-26, and version 3.0.1 on 2026-09-22. A 2.0.0 followed by a 3.0.0 inside three months is a signal that the public API is still changing, and the package requirement in the README, `from: "3.0.0"`, plus the CocoaPods constraint `~> 3.0`, both point at the 3.x line as the current one.

Activity is current: the last push to the master branch was on 2026-10-19, and CI runs on GitHub Actions with a badge in the README. The CocoaPods badge and the release badge are both wired up, so distribution is automated rather than manual.

The upgrade cost for an adopter is therefore about the toolchain and the API churn, not about the licence. A team on an older Xcode will not be able to build current Harbeth at all, and a team that adopted the 1.x or 2.x filter API should expect to read the CHANGELOG before moving to 3.x, because the two-route structure and the `RenderedFrame` contract are not the shape of the earlier releases.

## Conclusion

Harbeth is the right choice for an Apple platform team that already has a media pipeline and wants a GPU filter graph with explicit output semantics, structured editing and inspection tooling, and who can accept a Swift 6 and Xcode 26 build environment. It is the wrong choice for a team that wants a photo filter pipeline to drop in with no host code, since the README is explicit that the host owns interaction, media lifecycle, persistence and product policy. Before adopting, read the route table and the source and result map in the README and pick one route deliberately, then confirm the deployment target you need, iOS and iPadOS 15, macOS 12 and tvOS 15, against the Xcode 26 and Swift 6.2 toolchain the build requires, since a library that needs a current compiler is a real cost for a product that supports older systems.

## FAQ

### How do I add Harbeth to an Xcode project?

The README gives a Swift Package Manager dependency on https://github.com/yangKJ/Harbeth.git from version 3.0.0, and tells you to add `Harbeth` to the target that renders images or frames. A CocoaPods alternative is `pod 'Harbeth', '~> 3.0'`. The build needs Xcode 26 or later with Swift 6.2.

### What is the difference between HarbethIO and ImageNode?

HarbethIO is the direct route for when you already have a source and a filter chain, and it returns the same type of source you passed in. ImageNode is the advanced unified graph route, returning a texture, a frame or an image readback, and carrying cache policy, metadata, output intent and diagnostics.

### Which Apple platforms and versions does Harbeth support?

The minimum versions listed are iOS and iPadOS 15.0, macOS 12.0 and tvOS 15.0, with a toolchain requirement of Xcode 16 or newer and Swift 6. The installation section adds that building requires Xcode 26 or later, so Swift 6.2 and platform SDKs 26 or later, while the deployment targets stay at iOS 15, macOS 12 and tvOS 15.

## Sources

- [Issues](https://github.com/yangKJ/Harbeth/issues)
- [License: MIT](https://github.com/yangKJ/Harbeth/blob/master/LICENSE)
- [README](https://github.com/yangKJ/Harbeth/blob/master/README.md)
- [Releases](https://github.com/yangKJ/Harbeth/releases)
- [yangKJ/Harbeth on GitHub](https://github.com/yangKJ/Harbeth)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/yangkj-harbeth
