PortfolioPrism / Graphics engineSoftware engineeringIndex / 04

04/Graphics engine/2026

Prism

GPU image processing for Swift

A Swift and Metal image-processing engine for Apple platforms. A compact API hides an explicit render graph, graph optimization, pooled textures and instrumentation. Core Image is not used to implement effects.

Role
Design and engineering
Stack
Swift 6, Metal
Platform
macOS 14+, iOS 17+
Dependencies
None
  1. 00/Input

    Source photograph

    Original photograph: a white house with green window trim above a garden path and a clothes line

     

  2. 01/Exposure

    +0.3 EV

    The photograph after Exposure +0.3 EV, noticeably brighter
    • Exposure0.125 ms
    • GPU total0.23 ms
  3. 02/Contrast

    1.30

    After Contrast 1.30, with deeper shadows and a brighter wall
    • Exposure+​Contrast0.177 ms
    • GPU total0.25 ms
  4. 03/Saturation

    1.35

    After Saturation 1.35, with richer greens and a warmer path
    • Exposure+​Contrast+​Saturation0.136 ms
    • GPU total0.18 ms
  5. 04/Sharpen

    amount 2.0, radius 2.5

    After Sharpen, amount 2.0, radius 2.5, with crisper stone and foliage texture
    • Exposure+​Contrast+​Saturation0.104 ms
    • Sharpen blur horizontal0.104 ms
    • Sharpen blur vertical0.098 ms
    • Sharpen combine0.031 ms
    • GPU total0.37 ms
Fig. 03.1 / One pipeline rendered by Prism: 6 passes as planned, 4 after optimization, 4 live textures instead of 51066 × 1066 photograph. Apple M5, macOS 27.0.1, release build. Detailed instrumentation, best of 15 renders after 5 warm-ups.

A render graph
behind a small API.

Pipelines are reusable values. A Renderer owns the caches and pooled resources and is safe to share across concurrent tasks. Existing Metal textures can enter and leave without a CPU round trip.

Underneath, each pipeline is lowered to a validated graph, optimized, planned for resource lifetimes, and executed asynchronously on the GPU. The package depends only on Apple frameworks.

let pipeline = ImagePipeline {
    Exposure(0.25)
    Contrast(1.1)
    GaussianBlur(radius: 6)
}

let output = try await pipeline.render(inputCGImage)

01/Five decisions

  1. 01

    A validated graph

    A pipeline is lowered to a DAG that is checked for dependencies, cycles, dimensions and formats before any resource is allocated.

  2. 02

    Optimization

    Identity parameters, dead nodes and compatible pointwise operations are removed or fused before execution.

  3. 03

    Lifetimes, not allocations

    Planning computes each resource’s final use, so a ten-pass chain needs an input and two alternating intermediates, not one texture per pass.

  4. 04

    Custom Metal kernels

    Color, separable blur, sharpen, resize, vignette and LUT grading are hand-written. The tiled blur caches rows and columns in threadgroup memory.

  5. 05

    Instrumentation

    Three levels, Instruments signposts and per-pass GPU timestamps report texture reuse, cache behavior and eliminated passes.


02/Evidence

  • 3.30 ms4K Gaussian blur, GPU, after optimization. 7.70 ms before
  • −57%Blur GPU time after the tiled, register-blocked kernel
  • 4.6 ms4K exposure, end to end, optimized image path. 16.8 ms before
  • 182Tests in 22 suites

Medians from the committed result files on one Apple M5, macOS 27.0.1, release build, deterministic synthetic inputs. Numbers vary with GPU state, and performance data covers one GPU generation.

Selected medians
CaseResolutionResult
Exposure, original image path4K16.8 ms end to end
Exposure, optimized image path4K4.6 ms end to end
5-stage pipeline, texture path4K2.8–3.1 ms
Gaussian blur σ=8, before tiled kernel4K7.70 ms GPU
Gaussian blur σ=8, optimized4K3.30 ms GPU
LUT encode, before cache—0.42–0.47 ms
LUT encode, after cache—0.02–0.04 ms

Measure, change one thing, measure again

The baseline showed image conversion, not GPU work, dominating end-to-end time: at 4K, exporting a result took 10.8 ms of a 16.7 ms total against 1.3 ms on the GPU. The image path lost its redundant copies.

Blur turned out to be bound by texture fetches, roughly 120 billion per second regardless of radius, so the kernel now fetches each texel once per tile. LUT textures were being rebuilt on every render and are now cached.

One early estimate, that fusing per-pixel passes would cut time in proportion to pass count, was wrong. The optimization log keeps the original claim and records the correction beside it.

Every operation has a Double-precision CPU reference. Random graph tests cover optimizer semantics and resource aliasing. Concurrency, cancellation and precision mixing are tested, Thread and Address Sanitizer ran clean, and important tests were mutation-checked.


03/Limits

  • CGImage and MTLTexture are the only supported interop types.

  • Bilinear resize has no downsampling prefilter, and there is no public composite operation yet.

  • Peak memory scales with renders in flight, and Prism does not limit them.

  • High precision does not imply HDR or professional wide-color correctness.


SourceView on GitHub