[MetalKit]46-Introduction to compute using Metal 用 Metal 進行計算的簡介

本系列文章是對 metalkit.org 上面MetalKit內容的全面翻譯和學習.c++

MetalKit系統文章目錄git


在 GPU 編程的領域中,計算或者說GPGPU,是 GPU 編程中除渲染外的另外一種用途。它們都涉及到了 GPU 並行編程,不一樣之處在於在計算中對線程的工做方式進行了更精細的控制。這樣,當你想要某些線程來處理問題的某一部分,同時其餘線程去處理該問題的另外一部分時,就會頗有用。github

本文是一系列關於計算的文章的開始篇。本文中的主題是關於圖像處理,由於它是引入計算和線程管理的最簡單方法。編程

注意:本文假設您知道如何建立一個微型的Metal項目或playground,能夠將屏幕清除爲純色。swift

第一個不一樣點就是,你須要建立一個MTLComputePipelineState以取代之前渲染時用的MTLRenderPipelineStatebash

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中也稱爲wavefrontwarp)是GPU組合在一塊兒的線程數,所以它們能夠並行地在不一樣的數據上執行相同的指令。組中的線程數應該是threadExecutionWidth的倍數,但毫不能大於maxTotalThreadsPerThreadgroup

那太棒了!如何找到辦法,來避免作這些未充分利用和邊界檢查呢?Metal 也在這裏給你提供了幫助。 無需使用dispatchThreadgroups(),API提供了更新的dispatchThreads()函數,它實現了兩件事:

  1. 經過自動建立非均勻線程組(例如3 x 4)來適應邊緣狀況,這樣就避免讓你處理未充分利用的問題。
  2. 它甚至能夠決定須要多少組,前提是您爲其提供網格大小和您想要使用的組大小。

注意: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 章完成。

下次見!

相關文章
相關標籤/搜索