# Implementing LUT Loading and the Color Grading Pipeline in Palmier Pro

> Learn how to implement LUT loading and the color grading pipeline in Palmier Pro. Our modular system parses .cube LUT files and applies them via Metal for powerful video color correction.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: internals
- Published: 2026-07-27

---

**Palmier Pro implements a modular, cache-driven color grading pipeline that parses `.cube` LUT files into RGBA Float32 buffers, applies them through a custom Metal tetrahedral interpolation kernel, and exposes the effect through both the Adjust Tab inspector and the Agent tool chain.**

The color grading pipeline in **Palmier Pro** centers on three tightly-coupled layers: file parsing, GPU kernel execution, and effect registration. By chaining **`LUTLoader`**, **`LUTTetraKernel`**, and **`EffectRegistry`**, the app delivers persistent, **thread-safe** LUT storage and real-time Metal-accelerated interpolation. This article walks through the source code to show exactly how Palmier Pro implements LUT loading and the color grading pipeline.

## Parsing and Caching `.cube` Files with LUTLoader

All LUT file handling in Palmier Pro begins in [`Sources/PalmierPro/Compositing/LUTLoader.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Compositing/LUTLoader.swift). The `**LUTLoader**` class reads 3-D **`.cube`** files, validates their headers, normalizes color domains, and packs the data into an RGBA `Float32` buffer suitable for Core Image.

The parser strictly enforces format constraints. It rejects 1-D LUTs and caps the dimension at 128. Domain values defined by `DOMAIN_MIN` and `DOMAIN_MAX` are clamped to `[0, 1]`, and an **alpha channel** of `1` is appended to every entry to produce a fully opaque RGBA buffer.

Once parsed, the `store(path:projectId:)` helper copies the source file into the app-support `luts` directory so the LUT survives project moves. The resulting **`CubeLUT`** object is then inserted into a thread-safe dictionary named **`cachedLUTs`** that is keyed by the absolute file path. Because the cache is **immutable** after insertion, the code uses `nonisolated(unsafe)` paired with a global **`NSLock`** to protect concurrent reads and writes.

```swift
let lutPath = "/Users/me/Lookups/film.cube"
do {
    // Move the file into the project’s LUT folder and cache it
    let storedPath = try LUTLoader.store(path: lutPath, projectId: editor.projectId)
    // Retrieve the parsed LUT for later use
    guard let cube = LUTLoader.load(path: storedPath) else { fatalError("Failed to load LUT") }
    // Apply it to a CIImage with full intensity
    let graded = LUTTetraKernel.apply(inputImage, cube: cube, key: storedPath, intensity: 1.0)
} catch {
    print("LUT error: \(error)")
}

```

## GPU-Accelerated Tetrahedral Interpolation in LUTTetraKernel

After parsing, the actual color transformation runs on the GPU inside [`Sources/PalmierPro/Compositing/Kernels/LUTTetraKernel.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Compositing/Kernels/LUTTetraKernel.swift). `**LUTTetraKernel**` wraps a custom Metal kernel called **`lutTetra`** through **`CIKernelLoader`**.

For each frame, the kernel receives the source `CIImage`, a LUT image created from the RGBA buffer, the LUT dimension, and an **intensity** factor. A dedicated **ROI callback** ensures the LUT image is only read where needed, which avoids expensive per-frame re-uploads. A small per-LUT image cache managed by **`Cache.store`** holds up to 16 entries, guaranteeing fast reuse when identical LUTs are applied across multiple clips.

The `intensity` parameter is a floating-point value in the range `[0, 1]`. It blends the LUT result with the underlying grade, giving users precise control over the strength of the **color grade**.

```swift
EffectRegistry.register(
    id: "color.lut",
    displayName: "LUT",
    category: "Color"
) { parameters, image in
    guard let path = parameters.string("path"),
          let cube = LUTLoader.load(path: path) else { return image }
    return LUTTetraKernel.apply(image, cube: cube, key: path,
                                intensity: parameters.value("intensity"))
}

```

## Effect Registration and UI Integration

The color-grading effect is registered in [`Sources/PalmierPro/Compositing/EffectRegistry.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Compositing/EffectRegistry.swift) under the identifier **`color.lut`**. When the compositing engine evaluates the effect, it loads the stored LUT path via `LUTLoader.load` and passes the resulting `CubeLUT` to `LUTTetraKernel.apply`.

In the inspector, [`Sources/PalmierPro/Inspector/Tabs/AdjustTab.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Inspector/Tabs/AdjustTab.swift) renders a **Choose LUT** button. Clicking it triggers an `NSOpenPanel` through `chooseLUT(clips:)`, calls `LUTLoader.store`, and creates an **undoable** edit via `commitEffects`. The **Agent tool chain**, defined in [`ToolDefinitions.swift`](https://github.com/palmier-io/palmier-pro/blob/main/ToolDefinitions.swift) and implemented in `Sources/PalmierPro/Agent/Tools/ToolExecutor+Color.swift`, validates and stores LUT files before invoking the effect. This maintains one consistent code path for both UI-driven and script-driven workflows.

```swift
Button {
    chooseLUT(clips: selectedClips)               // shows an NSOpenPanel
} label: {
    Label("Choose LUT", systemImage: "cube")
}

```

On the Agent side, validation is wrapped in explicit error handling:

```swift
func storeLUT(_ path: String) throws -> String {
    do {
        return try LUTLoader.store(path: path, projectId: editor.projectId)
    } catch let e as LUTStoreError {
        throw ToolError(e.errorDescription ?? "Invalid LUT.")
    }
}

```

Test coverage for the pipeline lives in [`Tests/PalmierProTests/Rendering/LUTLoaderTests.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Tests/PalmierProTests/Rendering/LUTLoaderTests.swift) and [`LUTTetraKernelTests.swift`](https://github.com/palmier-io/palmier-pro/blob/main/LUTTetraKernelTests.swift), which validate parsing edge cases and kernel behavior.

## Summary

- **`LUTLoader`** in [`LUTLoader.swift`](https://github.com/palmier-io/palmier-pro/blob/main/LUTLoader.swift) parses `.cube` files, enforces a maximum 3-D dimension of 128, normalizes domains to `[0, 1]`, and caches immutable `CubeLUT` instances behind an `NSLock`.
- **`LUTTetraKernel`** in [`LUTTetraKernel.swift`](https://github.com/palmier-io/palmier-pro/blob/main/LUTTetraKernel.swift) performs tetrahedral interpolation through a custom Metal kernel, reuses LUT images via a 16-entry cache, and respects an intensity slider for blending.
- **`EffectRegistry`** wires the `color.lut` identifier into the compositing pipeline, while [`AdjustTab.swift`](https://github.com/palmier-io/palmier-pro/blob/main/AdjustTab.swift) and the Agent tools in `ToolExecutor+Color.swift` share the same storage and validation logic.

## Frequently Asked Questions

### How does Palmier Pro parse `.cube` LUT files?

`LUTLoader` reads the file line by line, validates the `LUT_3D_SIZE` header, rejects 1-D LUTs, clamps `DOMAIN_MIN` and `DOMAIN_MAX` to `[0, 1]`, and packs the lattice into a contiguous RGBA `Float32` buffer. The parsed result is stored as a `CubeLUT` object that can be fed directly into Core Image or Metal shaders.

### What interpolation method does the color grading pipeline use?

The pipeline uses tetrahedral interpolation executed on the GPU. `LUTTetraKernel` dispatches the `lutTetra` Metal kernel through `CIKernelLoader`, sampling the 3-D lattice with a supplied intensity factor that blends the transformed color back toward the original image.

### How does Palmier Pro keep the LUT cache thread-safe?

A global `NSLock` guards the `cachedLUTs` dictionary in [`LUTLoader.swift`](https://github.com/palmier-io/palmier-pro/blob/main/LUTLoader.swift). Because entries are immutable after insertion, the code marks the cache `nonisolated(unsafe)`, allowing concurrent reads once the lock is released while still preventing data races during writes.

### Can LUTs be applied programmatically through the Agent tool chain?

Yes. The Agent pipeline in `ToolExecutor+Color.swift` exposes tools that validate incoming paths through `LUTLoader.store`, persist the file in the project’s LUT directory, and return a storable path. That path is later resolved by `EffectRegistry` when the `color.lut` effect is evaluated, ensuring UI and scripted workflows behave identically.