本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.c++
在 GPU 編程的領域中,計算或者說GPGPU
,是 GPU 編程中除渲染外的另外一種用途。它們都涉及到了 GPU 並行編程,不一樣之處在於在計算中對線程的工做方式進行了更精細的控制。這樣,當你想要某些線程來處理問題的某一部分,同時其餘線程去處理該問題的另外一部分時,就會頗有用。github
本文是一系列關於計算的文章的開始篇。本文中的主題是關於圖像處理,由於它是引入計算和線程管理的最簡單方法。編程
注意:本文假設您知道如何建立一個微型的Metal項目或playground,能夠將屏幕清除爲純色。swift
第一個不一樣點就是,你須要建立一個MTLComputePipelineState
以取代之前渲染時用的MTLRenderPipelineState
:bash
let function = library.makeFunction(name: "compute")
let pipelineState = device.makeComputePipelineState(function: function)
複製代碼
第二件事是,你須要一個紋理,以供線程使用。若是你使用的是playground,那你只須要下面幾行:函數
let textureLoader = MTKTextureLoader(device: device)
let url = Bundle.main.url(forResource: "nature", withExtension: "jpg")!
let image = try textureLoader.newTexture(URL: url, options: [:])
複製代碼
第三件事,你須要一個MTLComputeCommandEncoder
對象,以便將先前建立的管線狀態對象和紋理,都附着上去:post
commandEncoder.setComputePipelineState(pipelineState)
commandEncoder.setTexture(image, index: 0)
複製代碼
第四件事,你須要一個kernel shader內核着色器
,要記得,你以前開始時就爲其建立了一個名爲compute的函數。固然,你能夠將內核代碼放到 .metal文件裏:學習
kernel void compute(texture2d<float, access::read> input [[texture(0)]],
texture2d<float, access::write> output [[texture(1)]],
uint2 id [[thread_position_in_grid]]) {
float4 color = input.read(id);
output.write(color, id);
}
複製代碼
在着色代碼中,輸入是你先前建立的MTLTexture
對象,稱爲image,輸出是一個可繪製紋理,你將向其中寫入數據,而後就能夠被呈現到屏幕上了:優化
let drawable = view.currentDrawable
commandEncoder.setTexture(drawable.texture, index: 1)
複製代碼
第五件事也是最後一件事是,你須要調度線程來幹活。有趣的事情就從如今開始了!你須要作的是在commandEncoder
中結束編碼以前,加上幾句代碼:
let threadsPerGroup = MTLSizeMake(100, 10, 1)
let groupsPerGrid = MTLSizeMake(15, 90, 1)
commandEncoder.dispatchThreadgroups(groupsPerGrid, threadsPerThreadgroup: threadsPerGroup)
複製代碼
那麼這裏是怎麼作的呢?線程是以網格(grid)形式來調度處理數據的,網格能夠是 1-,2-,或3-維的。在本例中,你用的是 2D 的網格,由於要處理的是一張圖片。不考慮維度的話,網格老是分割成多個線程組的,以下面的公式:
gridSize = groupsPerGrid * threadsPerGroup
複製代碼
在本例中,你定義一個組有100 x10
個線程,每一個網格有15 x 90
組。若是你運行你的 playground,你會看到相似下面的狀況:
邊上的紅色是什麼東西?這是由於你試圖去猜想圖片的尺寸大小而致使的問題,線程數和組數應該用更「聰明」的方式獲取。
顯然,圖像在兩個維度上都大於分派的線程數。您能夠作的一件事是使用圖像大小進行有根據的猜想,以得到真正應該使用的組數量:
let width = Int(view.drawableSize.width)
let height = Int(view.drawableSize.height)
let w = threadsPerGroup.width
let h = threadsPerGroup.height
let groupsPerGrid = MTLSizeMake(width / w, height / h, 1)
複製代碼
運行一下,圖片看起來會好不少了:
這裏又出現一個新的問題---利用不足。請看下圖的圖表:
一般,您會認爲正確設計的網格是3 x 2組,每組4 x 4個線程,所以網格爲12 x 8個線程。然而,底部和右側邊緣的一些螺紋未獲得充分利用,由於它們沒有工做要作。
若是你製做一個較小的網格,好比8 x 4,它將會填滿整個組,又會產生你在開始時看到的紅色條帶。這意味着惟一可接受的解決方案是修復未充分利用問題。您能夠經過在每一個維度中添加額外的組來解決此問題,以下所示:
let groupsPerGrid = MTLSizeMake((width + w - 1) / w, (height + h - 1) / h, 1)
複製代碼
你所作的就是用(w-1, h-1, 1)
來實際擴大網格尺寸。這又帶來了另外一個風險 --- 訪問越界座標。要處理這個問題,您須要在讀取輸入圖像以前向內核着色器添加邊界檢查:
if (id.x >= output.get_width() || id.y >= output.get_height()) {
return;
}
複製代碼
這將處理那些不該該作任何工做的線程,並處理越界的訪問。
那個線程組的大小怎麼樣 --- 沒法優化嗎?到目前爲止,你一直在猜這些尺寸。固然,還有一種方法能夠得到最佳的羣組尺寸。硬件提供了一些能夠經過管道狀態對象(pipeline state object)訪問的功能:
var w = pipelineState.threadExecutionWidth
var h = pipelineState.maxTotalThreadsPerThreadgroup / w
let threadsPerGroup = MTLSizeMake(w, h, 1)
複製代碼
線程執行寬度(在其餘API中也稱爲wavefront
或warp
)是GPU組合在一塊兒的線程數,所以它們能夠並行地在不一樣的數據上執行相同的指令。組中的線程數應該是threadExecutionWidth
的倍數,但毫不能大於maxTotalThreadsPerThreadgroup
。
那太棒了!如何找到辦法,來避免作這些未充分利用和邊界檢查呢?Metal 也在這裏給你提供了幫助。 無需使用dispatchThreadgroups()
,API提供了更新的dispatchThreads()
函數,它實現了兩件事:
3 x 4
)來適應邊緣狀況,這樣就避免讓你處理未充分利用的問題。注意:
dispatchThreads()
函數適用於全部macOS設備,但它不適用於使用A10或更舊處理器的iOS設備。
你須要作的就是,就下面代碼替換計算每一個網格組數的代碼:
w = Int(view.drawableSize.width)
h = Int(view.drawableSize.height)
let threadsPerGrid = MTLSizeMake(w, h, 1)
commandEncoder.dispatchThreads(threadsPerGrid, threadsPerThreadgroup: threadsPerGroup)
複製代碼
可是等一下,我是否是說過:這裏是最好玩的地方?是的,而後來到 kernel shader 中,移除邊界檢查代碼,由於如今已經不須要它了。而後在最後一行前,添加下面代碼,倒轉顏色通道:
color = float4(color.g, color.b, color.r, 1.0);
複製代碼
運行一下 playground,你會看到相似下面的圖像:
將上一行用下面代碼替換,它將灰度應用於圖像:
color.xyz = (color.r * 0.3 + color.g * 0.6 + color.b * 0.1) * 1.5;
複製代碼
運行一下 playground,你會看到相似下面的圖像:
最後,將下面代碼替換:
float4 color = input.read(id);
color.xyz = (color.r * 0.3 + color.g * 0.6 + color.b * 0.1) * 1.5;
複製代碼
替換爲下面的代碼,這裏將圖片將圖像像素化爲5像素的正方形:
uint2 index = uint2((id.x / 5) * 5, (id.y / 5) * 5);
float4 color = input.read(index);
複製代碼
運行一下 playground,你會看到相似下面的圖像:
玩得開心麼?但願你玩得開心。若是你想要學習更多關於圖像處理的知識,Simon Gladman有一本好書,Core Image For Swift。本文只是一個對 GPGPU 和GPU計算功能的簡短介紹。請繼續關注新主題。
源代碼已經發布在Github
上。本文基於書籍Metal by Tutorials的第 16 章完成。
下次見!