Как часть проекта по созданию симуляции реакции диффузии на основе графического процессора
, я заявил, что на этих выходных я расскажу об использовании
Metal в
Swift .
В прошлом я проделывал аналогичную работу, ориентируясь на Flash Player и используя
AGAL . Металл — это язык намного более высокого уровня, чем AGAL: он основан на C ++ с более богатым синтаксисом и включает в себя
вычислительные функции . В то время как в AGAL для запуска клеточных автоматов я создавал прямоугольник из двух треугольников с помощью вершинного шейдера и выполнял функции диффузии реакции в отдельном фрагментном шейдере, вычислительный шейдер более прямой: я могу получать и устанавливать текстуры и может работать с отдельными пикселями этой текстуры без необходимости использования вершинного шейдера.
Код Swift, который я обсуждаю в этой статье, основан на двух статьях
Metal By Example :
Введение в компьютерное программирование в металле и
основы обработки изображений в металле . Оба из них включают исходный код Objective-C, так что, надеюсь, моя реализация Swift поможет некоторым.
UIImage , применить функцию ядра к этой текстуре, преобразовать вновь сгенерированную текстуру обратно в
UIImage и отобразить ее. Я использую простой пример шейдера, который изменяет насыщенность входного изображения. поэтому я также добавил ползунок, который изменяет значение насыщенности.
Давайте посмотрим на каждый шаг один за другим:
Инициализация Metal довольно проста: внутри переопределенного viewDidLoad () моего контроллера представления
я создаю указатель на устройство Metal по умолчанию:
var device: MTLDevice! = nil
[...]
device = MTLCreateSystemDefaultDevice()
Мне также нужно создать библиотеку и очередь команд:
defaultLibrary = device.newDefaultLibrary()
commandQueue = device.newCommandQueue()
Наконец, я добавляю ссылку на свою функцию Metal в библиотеку и синхронно создаю и компилирую состояние конвейерного вычисления:
et kernelFunction = defaultLibrary.newFunctionWithName("kernelShader")
pipelineState = device.newComputePipelineStateWithFunction(kernelFunction!, error: nil)
KernelShader указывает на функцию обработки изображения насыщенности, написанную на Metal, которая находится в моем файле Shaders.metal :
kernel void kernelShader(texture2d<float, access::read> inTexture [[texture(0)]],
texture2d<float, access::write> outTexture [[texture(1)]],
constant AdjustSaturationUniforms &uniforms [[buffer(0)]],
uint2 gid [[thread_position_in_grid]])
{
float4 inColor = inTexture.read(gid);
float value = dot(inColor.rgb, float3(0.299, 0.587, 0.114));
float4 grayColor(value, value, value, 1.0);
float4 outColor = mix(grayColor, inColor, uniforms.saturationFactor);
outTexture.write(outColor, gid);
}
Есть несколько шагов по преобразованию
UIImage в
экземпляр MTLTexture . Я создаю массив
UInt8 для хранения пустого
CGBitmapInfo , затем использую
CGContextDrawImage () для копирования изображения в растровый контекст
let image = UIImage(named: "grand_canyon.jpg")
let imageRef = image.CGImage
let imageWidth = CGImageGetWidth(imageRef)
let imageHeight = CGImageGetHeight(imageRef)
let bytesPerRow = bytesPerPixel * imageWidth
var rawData = [UInt8](count: Int(imageWidth * imageHeight * 4), repeatedValue: 0)
let bitmapInfo = CGBitmapInfo(CGBitmapInfo.ByteOrder32Big.toRaw() | CGImageAlphaInfo.PremultipliedLast.toRaw())
let context = CGBitmapContextCreate(&rawData, imageWidth, imageHeight, bitsPerComponent, bytesPerRow, rgbColorSpace, bitmapInfo)
CGContextDrawImage(context, CGRectMake(0, 0, CGFloat(imageWidth), CGFloat(imageHeight)), imageRef)
После того, как все эти шаги выполнены, я могу создать новую текстуру, используя метод replaceRegion (), чтобы записать в нее изображение:
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(MTLPixelFormat.RGBA8Unorm, width: Int(imageWidth), height: Int(imageHeight), mipmapped: true)
texture = device.newTextureWithDescriptor(textureDescriptor)
let region = MTLRegionMake2D(0, 0, Int(imageWidth), Int(imageHeight))
texture.replaceRegion(region, mipmapLevel: 0, withBytes: &rawData, bytesPerRow: Int(bytesPerRow))
Я также создаю пустую текстуру, в которую функция ядра будет записывать:
let outTextureDescriptor = MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(texture.pixelFormat, width: texture.width, height: texture.height, mipmapped: false)
outTexture = device.newTextureWithDescriptor(outTextureDescriptor)
Следующий блок работы — установить текстуры и другую переменную в функции kerne и запустить шейдер. Первым шагом является создание экземпляра буфера команд и кодировщика команд:
let commandBuffer = commandQueue.commandBuffer()
let commandEncoder = commandBuffer.computeCommandEncoder()
… затем установите состояние конвейера (мы получили из device.newComputePipelineStateWithFunction () ранее) и текстуры в кодировщике команд:
commandEncoder.setComputePipelineState(pipelineState)
commandEncoder.setTexture(texture, atIndex: 0)
commandEncoder.setTexture(outTexture, atIndex: 1)
Фильтру требуется дополнительный параметр, который определяет величину насыщенности. Это передается в шейдер через MTLBuffer . Чтобы заполнить буфер, я создал небольшую структуру:
struct AdjustSaturationUniforms
{
var saturationFactor: Float
}
Затем newBufferWithBytes () для передачи моего значения с плавающей точкой saturationFactor
var saturationFactor = AdjustSaturationUniforms(saturationFactor: self.saturationFactor)
var buffer: MTLBuffer = device.newBufferWithBytes(&saturationFactor, length: sizeof(AdjustSaturationUniforms), options: nil)
commandEncoder.setBuffer(buffer, offset: 0, atIndex: 0)
Теперь это доступно внутри шейдера в качестве аргумента его функции ядра:
constant AdjustSaturationUniforms &uniforms [[buffer(0)]]
Теперь я готов вызвать саму функцию. Металлические функции ядра используют группы потоков, чтобы разбить их рабочую нагрузку на куски. В моем примере я создаю 64 группы потоков, а затем отправляю их в графический процессор:
let threadGroupCount = MTLSizeMake(8, 8, 1)
let threadGroups = MTLSizeMake(texture.width / threadGroupCount.width, texture.height / threadGroupCount.height, 1)
commandQueue = device.newCommandQueue()
commandEncoder.dispatchThreadgroups(threadGroups, threadsPerThreadgroup: threadGroupCount)
commandEncoder.endEncoding()
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
Наконец, теперь, когда функция ядра выполнена, нам нужно сделать все наоборот, чтобы получить изображение, хранящееся в
outTexture, в
UIImage, чтобы его можно было отобразить. Опять же, я использую регион для определения размера и getBytes () текстуры
для заполнения массива в
UInt8 :
let imageSize = CGSize(width: texture.width, height: texture.height)
let imageByteCount = Int(imageSize.width * imageSize.height * 4)
let bytesPerRow = bytesPerPixel * UInt(imageSize.width)
var imageBytes = [UInt8](count: imageByteCount, repeatedValue: 0)
let region = MTLRegionMake2D(0, 0, Int(imageSize.width), Int(imageSize.height))
outTexture.getBytes(&imageBytes, bytesPerRow: Int(bytesPerRow), fromRegion: region, mipmapLevel: 0)
Теперь, когда imageBytes содержит необработанные данные, для создания CGImage требуется несколько строк :
let providerRef = CGDataProviderCreateWithCFData(
NSData(bytes: &imageBytes, length: imageBytes.count * sizeof(UInt8))
)
let bitmapInfo = CGBitmapInfo(CGBitmapInfo.ByteOrder32Big.toRaw() | CGImageAlphaInfo.PremultipliedLast.toRaw())
let renderingIntent = kCGRenderingIntentDefault
let imageRef = CGImageCreate(UInt(imageSize.width), UInt(imageSize.height), bitsPerComponent, bitsPerPixel, bytesPerRow, rgbColorSpace, bitmapInfo, providerRef, nil, false, renderingIntent)
imageView.image = UIImage(CGImage: imageRef)
… и мы закончили!
Для Metal требуется процессор A7 или A8, и этот код был собран и протестирован под Xcode 6. Весь исходный код доступен в моем
репозитории GitHub здесь .