SwiftUI: BBMetal Filters(Part 2) — Main Function.

Menura Wijesekara
3 min readJul 10, 2024

--

Welcome to part 2 of BBMetal filter blog. In the previous post we learned about the supportive functions we need to know to do the image processing. Today we will learn about the main function and how the metal file will be used to do the image processing.

In order to do the image processing we need a metal file handle the image processing functionality. Let's see how we can use it you can refer the metal files in BBMetal filter git page.

The following metal file is used to change the Brightness in images.

#include <metal_stdlib>
using namespace metal;

kernel void brightnessKernel(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
constant float *brightness [[buffer(0)]],
uint2 gid [[thread_position_in_grid]]) {

if ((gid.x >= outputTexture.get_width()) || (gid.y >= outputTexture.get_height())) { return; }

const half4 inColor = inputTexture.read(gid);
const half4 outColor(inColor.rgb + half3(*brightness), inColor.a);
outputTexture.write(outColor, gid);
}

This Metal shader kernel, defined using the Metal shading language, implements a simple brightness adjustment filter. It takes two input textures: outputTexture, which is writable and represents the destination texture, and inputTexture, which is read-only and serves as the source texture. Additionally, it accepts a constant buffer brightness containing a single float value to adjust the brightness level. The kernel first checks if the current thread's position (gid) is within the bounds of the output texture dimensions. If not, it returns early. For valid threads, it reads the color from inputTexture at the given thread position (gid). It then computes a new color outColor by adding the brightness value to the RGB components of inColor while keeping the alpha component unchanged. Finally, it writes the modified outColor to the outputTexture at the same position (gid). This shader effectively applies a uniform brightness adjustment to each pixel in the input texture based on the provided brightness constant.

We will be using a new function to do the image processing and calling the metal filter.

func kernelFunction(input :MTLTexture , brightness : Float , device:MTLDevice){


var bright = brightness
let library = device.makeDefaultLibrary()!

let computeFunction: MTLFunction = library.makeFunction(name: "brightnessKernel")!

let computePipelineState = try? device.makeComputePipelineState(function: computeFunction)

let commandQueue:MTLCommandQueue = device.makeCommandQueue()!

let commandBuffer: MTLCommandBuffer = commandQueue.makeCommandBuffer()!

let computeCommandEncoder: MTLComputeCommandEncoder = commandBuffer.makeComputeCommandEncoder()!

computeCommandEncoder.setComputePipelineState(computePipelineState!)
computeCommandEncoder.setTexture(input, index: 0)
computeCommandEncoder.setTexture(input, index: 1)
computeCommandEncoder.setBytes(&bright, length: MemoryLayout<Float>.size, index: 0)
let threadGroupCount = MTLSizeMake(16, 16, 1)

var threadGroups = MTLSizeMake((input.width) / threadGroupCount.width, (input.height) / threadGroupCount.height, 1)

if((input.width) % threadGroupCount.width != 0 || (input.height) % threadGroupCount.height != 0){
threadGroups = MTLSizeMake((input.width) / threadGroupCount.width + 1, (input.height) / threadGroupCount.height + 1, 1)
}

computeCommandEncoder.dispatchThreadgroups(threadGroups, threadsPerThreadgroup: threadGroupCount)
computeCommandEncoder.endEncoding()
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
}

This Swift function kernelFunction serves as a bridge to execute a Metal compute kernel named "brightnessKernel" on a given input MTLTexture to adjust its brightness. The function begins by loading the default Metal library from the provided MTLDevice, retrieving the compute function named "brightnessKernel" from this library. It then attempts to create a MTLComputePipelineState from the compute function, which encapsulates the compiled GPU code needed to execute the kernel efficiently.

After initializing the necessary Metal objects like MTLCommandQueue and MTLCommandBuffer, it sets up a MTLComputeCommandEncoder to encode commands for GPU execution. The compute pipeline state is set on the command encoder, followed by setting the input texture (input) twice (once for reading and once for writing, though typically only one would be necessary). The brightness value (bright) is passed to the shader via setBytes, ensuring it's available as a constant buffer inside the shader. Thread group and thread counts are calculated to efficiently distribute work across the GPU cores, ensuring optimal performance. Finally, the compute command encoder dispatches the work to the GPU using dispatchThreadgroups, commits the command buffer for execution, and waits for the completion of GPU processing using waitUntilCompleted.

Overall, this function demonstrates the orchestration of Metal API calls to configure and execute a GPU-accelerated compute kernel for brightness adjustment on an input texture. It highlights Metal’s capability to efficiently handle parallel processing tasks on the GPU, leveraging compute shaders to perform complex calculations like image processing with high performance and scalability.

Now we know how to use the metal files and how to convert the texture to the required value. In the next blog we will see how to implement everything in a single code. Until that Happy Coding….

--

--