Metal入門資料017-Metal最佳實踐指南

寫在前面:

對Metal技術感興趣的同學,可以關注我的專題:Metal專輯
也可以關注我個人的簡書賬號:張芳濤
所有的代碼存儲的Github地址是:Metal

正文

本文是摘抄的蘋果官方文檔,蘋果的文檔里面有Metal Best Practices Guide這個章節。

Metal提供對GPU的最低開銷訪問,使您能夠在iOSmacOStvOS上最大化應用程序的圖形和計算潛力。每毫秒和每一點都是Metal應用程序和用戶體驗的組成部分 ,所以 我們有責任通過遵循本指南中描述的最佳實踐來確保Metal應用程序盡可能高效地運行。除非另有說明,否則這些最佳實踐適用于支持Metal的所有平臺。

一個高性能的Metal應用程序需要具備以下特點:

  • CPU開銷。Metal旨在減少或消除許多CPU端性能瓶頸。只有按照建議使用Metal API,應用才能從此受益。

  • 最佳GPU性能。Metal允許創建并向GPU提交命令。要優化GPU性能,您的應用應優化這些命令的配置和組織。

  • 處理器持續和并行工作能力。Metal旨在最大化CPUGPU并行性。應用應該讓這些處理器起作用并同時工作。

  • 有效的資源管理。Metal為您的資源對象提供簡單而強大的接口。應用應該有效地管理這些資源,以減少內存消耗并提高訪問速度。

資源管理

對象持久化

最佳實踐:盡早創建持久對象并經常重用它們。

Metal框架提供了在應用程序的整個生命周期內管理持久對象的協議。這些對象的創建成本很高,但通常會初始化一次并經常重復使用。不需要在每個渲染或計算循環的開頭創建這些對象。

首先初始化的設備和命令隊列

MTLCreateSystemDefaultDevice在應用程序啟動時 調用該函數以獲取默認系統設備。接下來,調用newCommandQueue或者 newCommandQueueWithMaxCommandBufferCount:方法創建一個命令隊列,用于在該設備上執行GPU指令。

所有應用程序應該只MTLDevice為每個GPU 創建一個對象,并將其重用于該GPU上的所有Metal工作。大多數應用程序應該MTLCommandQueue每個GPU 只創建一個對象,但如果每個命令隊列代表不同的Metal工作(例如,非實時計算處理和實時圖形渲染),您可能需要更多。

  • 一些macOS設備具有多個GPU。如果需要使用多個GPU,請調用該MTLCopyAllDevices函數以獲取可用設備的數組。為您使用的每個GPU創建并保留至少一個命令隊列。

在構建時編譯函數并構建Library

有關在構建時編譯函數和構建庫的概述,請參閱Libraries最佳實踐。

在運行時,使用MTLLibraryMTLFunction對象訪問圖形庫和計算函數。避免在運行時構建庫或在渲染或計算循環期間獲取函數。

如果需要配置多個渲染或計算管道,請MTLFunction盡可能重用對象。您可以釋放MTLLibraryMTLFunction建設的所有對象后,渲染并依賴于它們的計算pipelines

建立一次Pipelines并經常重復使用

構建可編程管道涉及對GPU狀態的評估工作非常消耗性能。應該只構建MTLRenderPipelineStateMTLComputePipelineState對象一次,然后為創建的每個新渲染或計算命令編碼器重用。不要為新的命令編碼器構建新的管道。有關異步構建多個pipelines的概述,請參閱pipelines最佳實踐。

  • 注意 : 除了渲染和計算管線,則可以選擇創建MTLDepthStencilStateMTLSamplerState封裝深度,模板,和采樣器狀態的對象。這些對象較輕便,但也應僅創建一次并經常重復使用。

預先分配資源存儲

資源數據可以是靜態的或動態的,并可在應用程序的整個生命周期的各個階段進行訪問。 但是,應盡早創建為此數據分配內存的MTLBufferMTLTexture對象。創建這些對象后,資源屬性和存儲分配是不可變的,但數據本身不是; 可以在必要時更新數據。

盡可能 重用MTLBufferMTLTexture對象,特別是對于靜態數據。避免在渲染或計算循環期間創建新資源,即使對于動態數據也是如此。有關緩沖區和紋理的更多信息,請參閱資源管理三重緩沖最佳實踐。

資源選項

最佳實踐:設置適當的資源存儲模式和紋理使用選項。

必須正確配置您的Metal資源,以利用快速內存??訪問和驅動程序性能優化。資源存儲模式允許定義MTLBufferMTLTexture對象的存儲位置和訪問權限。紋理使用選項允許顯式聲明打算如何使用MTLTexture對象。

熟悉設備內存模型

設備內存型號因操作系統而異。iOStvOS設備支持統一的內存模型,其中CPUGPU共享系統內存。macOS設備支持具有CPU可訪問系統內存和GPU可訪問視頻內存的獨立內存模型。

  • 一些macOS設備具有集成的GPU。在這些設備中,驅動程序優化底層架構以支持離散內存模型。macOS Metal應該始終以離散內存模型為目標。
  • 所有iOStvOS設備都集成了GPU

選擇適當的資源存儲模式(iOS和tvOS)

iOStvOS中,Shared模式定義了CPUGPU Private都可訪問的系統內存,而模式定義了只能由GPU訪問的系統內存。

Shared模式通常是iOStvOS資源的正確選擇。Private僅當CPU從不訪問資源時才選擇模式。

  • iOStvOS中,memoryless存儲模式用于無記憶紋理。此存儲模式只能用于存儲在片上磁貼存儲器中的臨時渲染目標。有關詳細信息,請參閱Memoryless TexturesMetal編程指南
圖3-1 iOS和tvOS中的資源存儲模式

選擇適當的資源存儲模式(macOS)

在macOS中,Shared模式定義了CPUGPU都可訪問的系統內存,而Private模式定義了只能由GPU訪問的視頻內存。

此外,macOS實現了Managed為資源定義同步內存對的模式,其中一個副本位于系統內存中,另一個副本位于視頻內存中。管理資源受益于對每個資源副本的快速CPUGPU訪問,同步這些副本所需的API調用最少。

圖3-2 macOS中的資源存儲模式
  • macOS中,Shared模式僅適用于緩沖區,而不適用于紋理。緩沖區數據通常是線性的,從而產生簡單的GPU訪問模式。紋理更復雜,其數據通常是平鋪或調配的,從而導致更復雜的GPU訪問模式。

緩沖存儲模式(macOS)

使用以下準則確定特定緩沖區的適當存儲模式。

  • 如果GPU專門訪問緩沖區,請選擇Private模式。這是GPU生成的數據的常見情況,例如每個補丁鑲嵌的因子。

  • 如果CPU專門訪問緩沖區,請選擇Shared模式。這是一種罕見的情況,通常是blit操作中的中間步驟。

  • 如果CPUGPU都訪問緩沖區,就像大多數頂點數據一樣,請考慮以下幾點并參考表3-1

    • 對于頻繁更改的小型數據,請選擇Shared模式。將數據復制到視頻存儲器的開銷可能比直接訪問GPU系統存儲器的開銷更大。

    • 對于不經常更改的中型數據,請選擇Managed模式。始終在修改托管緩沖區的內容后調用適當的同步方法。

      執行CPU寫入后,調用didModifyRange:方法以通知Metal有關已修改的特定數據范圍; 這允許Metal僅更新視頻內存副本中的特定范圍。

      在編寫GPU寫入之后,編碼包括對synchronizeResource:方法的調用的blit操作; 這允許Metal在相關命令緩沖區完成執行后更新系統內存副本。

    • 對于永不更改的大型數據,請選擇Private模式。使用Shared模式初始化并填充源緩沖區,然后將其數據blit到具有Private模式的目標緩沖區。這是一次性成本的最佳操作。

  • 表3-1為CPUGPU訪問的緩沖區數據選擇存儲模式

Data size Resource dirtiness Update frequency Storage mode
Small Full Every frame Shared
Medium Partial Every n frames Managed
Large N/A Once Private
(來自共享源緩沖區的blit之后)
紋理存儲模式(macOS)

macOS中,紋理的默認存儲模式是Managed。使用以下準則確定特定紋理的適當存儲模式。

  • 如果GPU專門訪問紋理,請選擇Private模式。這是GPU生成的數據的常見情況,例如可顯示的渲染目標。

  • 如果CPU專門訪問紋理,請選擇Managed模式。這是一種罕見的情況,通常是blit操作中的中間步驟。

  • 如果紋理由CPU初始化一次并由GPU頻繁訪問,則使用Managed模式初始化源紋理,然后將其數據blit到具有Private模式的目標紋理。這是靜態紋理的常見情況,例如漫反射貼圖。

  • 如果CPUGPU頻繁訪問紋理,請選擇Managed模式。這是動態紋理的常見情況,例如圖像濾鏡。始終在修改托管紋理的內容后調用適當的同步方法。

對GPU寫入進行編碼后,對包含對以下任一方法的調用的blit操作進行編碼。這允許Metal在關聯的命令緩沖區完成執行后更新系統內存副本。
* synchronizeResource:
* synchronizeTexture:slice:level:

設置適當的紋理使用標志

Metal可以根據其預期用途優化給定紋理的GPU操作。如果您事先知道它們,請始終聲明顯式紋理使用選項。不要依賴Unknown選項; 雖然此選項為紋理提供了最大的靈活性,但卻會產生顯著的性能損失。如果驅動程序不知道您打算如何使用紋理,則無法執行任何優化。有關可用紋理使用選項的說明,請參閱MTLTextureUsage參考。

三重緩沖

最佳實踐:實現三重緩沖模型以更新動態緩沖區數據。

動態緩沖區數據是指存儲在緩沖區中的頻繁更新的數據。為避免每幀創建新緩沖區并最小化幀之間的處理器空閑時間,我們需要實現三重緩沖模型。

防止訪問沖突并減少處理器空閑時間

動態緩沖區數據通常由CPU寫入并由GPU讀取。如果這些操作同時發生,則會發生訪問沖突; CPU必須在GPU可以讀取之前完成數據寫入,并且GPU必須在CPU覆蓋之前讀取該數據。如果動態緩沖區數據存儲在單個緩沖區中,則當CPU停止運行或GPU缺乏時,這會導致處理器空閑時間延長。為使處理器并行工作,CPU應至少在GPU前一幀工作。此解決方案需要多個動態緩沖區數據實例,因此CPU可以在幀n+1讀取數據時為幀寫入數據n

減少內存開銷和幀延遲

您可以使用可重用緩沖區的FIFO隊列管理多個動態緩沖區數據實例。但是,分配太多緩沖區會增加內存開銷,并可能限制其他資源的內存分配。此外,如果CPU的工作距離GPU工作太遠,分配太多緩沖區會增加幀延遲。

  • 避免每幀創建新的緩沖區。有關預先分配資源存儲的概述,請參閱持久對象最佳實踐。

允許命令緩沖區事務的時間

動態緩沖區數據被編碼并綁定到瞬態命令緩沖區。在提交執行后,將此命令緩沖區從CPU傳輸到GPU需要一定的時間。類似地,GPU需要一定的時間來通知CPU它已完成該命令緩沖區的執行。對于單個幀,此序列詳述如下:

  • 1:CPU寫入動態數據緩沖區并將命令編碼到命令緩沖區中。
  • 2: CPU調度完成處理程序(addCompletedHandler:),提交命令緩沖區(commit),并將命令緩沖區傳輸到GPU
  • 3: GPU執行命令緩沖區并從動態數據緩沖區讀取。
  • 4:GPU完成其執行并調用命令緩沖區完成處理程序([MTLCommandBufferHandler](https://developer.apple.com/documentation/metal/mtlcommandbufferhandler))。

此序列可以與兩個動態數據緩沖區并行化,但是如果任一處理器正在等待繁忙的動態數據緩沖區,則命令緩沖區事務可能導致CPU停止或GPU閑置。

實現三重緩沖模型

在考慮處理器空閑時間,內存開銷和幀延遲時,添加第三個動態數據緩沖區是理想的解決方案。圖4-1顯示了三重緩沖時間線,清單4-1顯示了三重緩沖實現。

圖4-1三重緩沖時間線

清單4-1三重緩沖實現:

static const NSUInteger kMaxInflightBuffers = 3;
/* Additional constants */

@implementation Renderer
{
dispatch_semaphore_t _frameBoundarySemaphore;
NSUInteger _currentFrameIndex;
NSArray <id <MTLBuffer>> _dynamicDataBuffers;
/* Additional variables */
}

- (void)configureMetal
{
// Create a semaphore that gets signaled at each frame boundary.
// The GPU signals the semaphore once it completes a frame's work, allowing the CPU to work on a new frame
_frameBoundarySemaphore = dispatch_semaphore_create(kMaxInflightBuffers);
_currentFrameIndex = 0;
/* Additional configuration */
}

- (void)makeResources
{
// Create a FIFO queue of three dynamic data buffers
// This ensures that the CPU and GPU are never accessing the same buffer simultaneously
MTLResourceOptions bufferOptions = /* ... */;
NSMutableArray *mutableDynamicDataBuffers = [NSMutableArray arrayWithCapacity:kMaxInflightBuffers];
for(int i = 0; i < kMaxInflightBuffers; i++)
{
    // Create a new buffer with enough capacity to store one instance of the dynamic buffer data
    id <MTLBuffer> dynamicDataBuffer = [_device newBufferWithLength:sizeof(DynamicBufferData) options:bufferOptions];
    [mutableDynamicDataBuffers addObject:dynamicDataBuffer];
}
_dynamicDataBuffers = [mutableDynamicDataBuffers copy];
}

- (void)update
{
// Advance the current frame index, which determines the correct dynamic data buffer for the frame
_currentFrameIndex = (_currentFrameIndex + 1) % kMaxInflightBuffers;

// Update the contents of the dynamic data buffer
DynamicBufferData *dynamicBufferData = [_dynamicDataBuffers[_currentFrameIndex] contents];
/* Perform updates */
}

- (void)render
{
// Wait until the inflight command buffer has completed its work
dispatch_semaphore_wait(_frameBoundarySemaphore, DISPATCH_TIME_FOREVER);

// Update the per-frame dynamic buffer data
[self update];

// Create a command buffer and render command encoder
id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id <MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:_renderPassDescriptor];

// Set the dynamic data buffer for the frame
[renderCommandEncoder setVertexBuffer:_dynamicDataBuffers[_currentFrameIndex] offset:0 atIndex:0];
/* Additional encoding */
[renderCommandEncoder endEncoding];

// Schedule a drawable presentation to occur after the GPU completes its work
[commandBuffer presentDrawable:view.currentDrawable];

__weak dispatch_semaphore_t semaphore = _frameBoundarySemaphore;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
    // GPU work is complete
    // Signal the semaphore to start the CPU work
    dispatch_semaphore_signal(semaphore);
}];

// CPU work is complete
// Commit the command buffer and start the GPU work
[commandBuffer commit];
}
@end

緩沖區綁定

最佳實踐:使用適當的方法將緩沖區數據綁定到圖形或計算功能。

setVertexBytes:length:atIndex:方法是將極小量(小于4 KB)的動態緩沖區數據綁定到頂點函數的最佳選項,如清單5-1所示。此方法避免了創建中間MTLBuffer對象的開銷。相反,Metal為我們管理瞬態緩沖區。

float _verySmallData = 1.0;
[renderEncoder setVertexBytes:&_verySmallData length:sizeof(float) atIndex:0];

如果數據大小大于4 KB,請創建一次MTLBuffer對象并根據需要更新其內容。調用setVertexBuffer:offset:atIndex:方法將緩沖區綁定到頂點函數; 如果緩沖區包含多個繪制調用中使用的數據,則setVertexBufferOffset:atIndex:稍后調用該方法以更新緩沖區偏移量,使其指向相應繪制調用數據的位置,如清單5-2所示。如果僅更新其偏移量,則無需重新綁定當前綁定的緩沖區。

清單5-2更新綁定緩沖區的偏移量

// Bind the vertex buffer once
[renderEncoder setVertexBuffer:_vertexBuffer[_frameIndex] offset:0 atIndex:0];
for(int i=0; i<_drawCalls; i++)
{
//  Update the vertex buffer offset for each draw call
[renderEncoder setVertexBufferOffset:i*_sizeOfVertices atIndex:0];

// Draw the vertices
[renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:_vertexCount];
}

Drawables

最佳實踐:盡量不要長期持有Drawable

大多數Metal應用程序實現由CAMetalLayer對象定義的圖層支持視圖。該層提供符合CAMetalDrawable協議的有效可顯示資源,通常稱為可繪制的drawable提供了一個MTLTexture對象,該對象通常用作附加到MTLRenderPassDescriptor對象的可顯示渲染目標,目標是呈現在屏幕上。

通過presentDrawable:在調用其commit方法之前調用命令緩沖區的方法來注冊drawable。但是,只有在命令緩沖區完成執行并且已繪制或寫入drawable之后,才會實現drawable本身。

drawable跟蹤它是否具有出色的呈現或寫入請求,并且在這些請求完成之前不會出現。命令緩沖區僅在計劃執行時注冊其可繪制請求。在調度命令緩沖區之后注冊可繪制的表示可確保在實際呈現drawable之前完成所有命令緩沖區工作。在注冊可繪制的演示文稿之前,不要等待命令緩沖區完成其GPU工作; 這將導致相當大的CPU停滯。

盡量不要長期持有Drawable

Drawable是由Core Animation框架創建和維護的相當消耗性能的系統資源。它們存在于有限且可重復使用的資源池中,并且在您的應用程序請求時可能可用,也可能不可用。如果在請求時沒有可用的可繪制,則調用線程將被阻塞,直到新的可繪制可用(通常在下一個顯示刷新間隔)。

Drawable是由Core Animation框架創建和維護的昂貴系統資源。它們存在于有限且可重復使用的資源池中,并且在您的應用程序請求時可能可用,也可能不可用。如果在請求時沒有可用的可繪制,則調用線程將被阻塞,直到新的可繪制可用(通常在下一個顯示刷新間隔)。

要盡可能簡短地持有drawable,請執行以下兩個步驟:

  • 1:總是盡可能晚地獲得抽簽; 優選地,緊接在編碼屏幕上渲染通道之前。幀的CPU工作可能包括動態數據更新和屏幕外渲染過程,您可以在獲取可繪制之前執行這些過程。

  • 2:務必盡快釋放drawable,越早越好。在完成幀的CPU工作之后立即執行。在自動釋放池塊中需要包含渲染循環,以避免可能出現多個drawable的死鎖情況。

  • iOS 10tvOS 10開始,可以安全地保存drawables以用于演示后屬性查詢,例如drawableIDpresentedTime。否則,drawables應該在不再需要時釋放,這通常是在調用命令緩沖區的presentDrawable:方法之后。

圖6-1顯示了drawable相對于其他CPU工作的生命周期。

圖6-1 drawable的生命周期

使用MetalKit視圖與Drawables交互

使用MTKView對象是與drawable交互的首選方式。一個MTKView目的是通過一個備份CAMetalLayer對象并提供currentDrawable獲取用于當前幀中的可拉伸性。當前幀呈現到此drawable中,并且該presentDrawable:方法調度實際呈現以在下一顯示刷新間隔發生。該currentDrawable屬性在每幀結束時自動更新。

一個MTKView對象還提供了currentRenderPassDescriptor一個引用當前繪制的紋理簡便屬性; 使用此屬性創建渲染命令編碼器,該編碼器呈現為當前的drawable。對currentRenderPassDescriptor屬性的調用隱式獲取當前幀的drawable,然后將其存儲在currentDrawable屬性中。

  • 如果創建由對象支持的自己的子類UIViewNSView子類,則CAMetalLayer必須顯式獲取drawable并使用其紋理來配置渲染過程描述符。您也可以為自己的MTKView對象執行此操作,但簡單地使用currentRenderPassDescriptor便利屬性要容易得多。有關如何從a UIViewNSView子類獲取drawable 的示例,請參閱MetalBasic3D示例。

下面的代碼顯示了如何使用帶有MetalKit視圖的drawable

- (void)render:(MTKView *)view {
// Update your dynamic data
[self update];

// Create a new command buffer
id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];

// BEGIN encoding any off-screen render passes
/* ... */
// END encoding any off-screen render passes

// BEGIN encoding your on-screen render pass
// Acquire a render pass descriptor generated from the drawable's texture
// 'currentRenderPassDescriptor' implicitly acquires the drawable
MTLRenderPassDescriptor* renderPassDescriptor = view.currentRenderPassDescriptor;

// If there's a valid render pass descriptor, use it to render into the current drawable
if(renderPassDescriptor != nil) {
    id<MTLRenderCommandEncoder> renderCommandEncoder = [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
    /* Set render state and resources */
    /* Issue draw calls */
    [renderCommandEncoder endEncoding];
    // END encoding your on-screen render pass

    // Register the drawable presentation
    [commandBuffer presentDrawable:view.currentDrawable];
}

/* Register optional callbacks */
// Finalize the CPU work and commit the command buffer to the GPU
[commandBuffer commit];
}

- (void)drawInMTKView:(MTKView *)view {
@autoreleasepool {
    [self render:view];
}
}

原生屏幕比例(iOS和tvOS)

最佳實踐:以目標顯示的精確像素大小渲染繪圖。

drawables的像素大小應始終與目標顯示的精確像素大小相匹配。這對于避免渲染到屏幕外像素或產生額外的采樣階段至關重要。

UIScreen類提供限定天然尺寸和物理屏幕的比例因子兩個屬性:nativeBoundsnativeScale。查詢nativeBounds屬性以確定屏幕的本機邊界矩形(以像素為單位)。查詢nativeScale屬性以確定用于將點轉換為像素的本機比例因子。

  • iOStvOS中,大多數繪圖技術都以磅而不是像素來衡量大小。您的金屬應用應始終以像素為單位測量大小,并完全避免使用點數。要了解有關這兩個單位之間差異的更多信息,請參閱點數與像素數

使用MetalKit視圖支持本機屏幕比例

MTKView級自動支持本機屏幕比例。默認情況下,視圖當前drawable的大小始終保證與視圖本身的大小相匹配。

  • 如果創建UIViewCAMetalLayer對象支持的子類,則必須先設置視圖的contentScaleFactor屬性以匹配屏幕的nativeScale屬性。接下來,確保在視圖大小發生變化時調整渲染目標的大小。最后,調整圖層的drawableSize屬性以匹配本機屏幕比例。

幀率(iOS和tvOS)

最佳實踐:以一致且穩定的幀速率顯示drawables

大多數應用程序的目標幀速率為60 FPS,相當于每幀16.67 ms。但是,在此時間內始終無法完成幀工作的應用應針對較低的幀速率以避免抖動。

  • 實時游戲的最低可接受幀速率為30 FPS。較低的幀速率被認為是糟糕的用戶體驗,應該避免這種情的發生。如果應用無法保持30 FPS的最低可接受幀速率,則應考慮進一步優化或減少工作負載(每幀花費少于33.33ms)。

查詢和調整幀率

可以通過maximumFramesPerSecond屬性查詢iOS和tvOS設備的最大幀速率。對于iOS設備,此值通常為60 FPS; 對于tvOS設備,此值可能會因附加屏幕的硬件功能或Apple TV上用戶選擇的分辨率而異。

使用MTKView對象是調整應用程序幀速率的推薦方法。默認情況下,視圖呈現為60 FPS; 要定位不同的幀速率,需要將視圖的preferredFramesPerSecond屬性設置為所需的值。

  • 一個MetalKit視圖總是舍入的值preferredFramesPerSecond到設備的最接近的因素maximumFramesPerSecond值。如果您的應用無法保持其最大目標幀速率(例如60 FPS),則將此屬性設置為較低因子幀速率(例如30 FPS)。將值設置preferredFramesPerSecond為非因子幀速率可能會產生意外結果。
  • 維持目標幀速率要求您的應用程序在允許的渲染間隔時間內完全更新,編碼,調度和執行幀的工作(例如,每幀少于16.67ms以保持60 FPS幀速率)。

調整Drawable Presentation時間

presentDrawable:方法注冊一個drawable presentation,以便盡快發生,通常在繪制或寫入drawable之后的下一個顯示刷新間隔。如果應用程序可以保持其最大目標幀速率(通過preferredFramesPerSecond屬性設置),那么只需調用該presentDrawable:方法即可保持一致且穩定的幀速率。

presentDrawable:afterMinimumDuration:方法允許為每個drawable指定最小顯示時間,這意味著只有在前一個drawable在顯示器上消耗了足夠的時間之后才會出現可繪制的演示文稿。這使我們可以將drawable的演示時間與應用程序的渲染循環同步。下面的代碼顯示了preferredFramesPerSecondpresentDrawable:afterMinimumDuration:API 之間的關系

view.preferredFramesPerSecond = 30;
/* ... */
[commandBuffer presentDrawable:view.currentDrawable afterMinimumDuration:1.0/view.preferredFramesPerSecond];

命令行相關介紹

加載和存儲操作

最佳實踐:為渲染目標設置適當的加載和存儲操作。

必須正確配置對Metal渲染目標執行的操作,以避免在渲染過程的開始(加載操作)或結束(存儲操作)時進行高能耗且不必要的渲染工作。

選擇適當的加載操作

使用以下準則確定特定渲染目標的相應加載操作。表9-1中還總結了這些指南。

  • 如果渲染了所有渲染目標像素,請選擇該DontCare操作。沒有與此操作相關的成本,紋理數據始終被解釋為未定義。

  • 如果不需要保留渲染目標的先前內容并且僅渲染其某些像素,請選擇該Clear操作。此操作會產生為每個像素寫入清晰值的成本。

  • 如果需要保留渲染目標的先前內容并且僅渲染其某些像素,請選擇該Load操作。此操作會產生加載先前內容的成本。

表9-1選擇渲染目標加載操作

以前的內容保留 像素渲染到 加載動作
N / A 所有 DontCare
沒有 一些 Clear
一些 Load

選擇適當的Store Action

使用以下準則確定特定渲染目標的相應存儲操作。

  • 如果不需要保留渲染目標的內容,請選擇該DontCare操作。沒有與此操作相關的成本,紋理數據始終被解釋為未定義。這是深度和模板渲染目標的常見情況。

    • 如果需要保留渲染目標的內容,請選擇Store操作。對于drawable和其他可顯示的渲染目標,情況總是如此。
  • 如果渲染目標是多重采樣紋理,請看下面的表格。

保留多重采樣內容 解析指定的紋理 已解決的內容已保留 存儲操作
storeAndMultisampleResolve
沒有 MultisampleResolve
沒有 N / A Store
沒有 沒有 N / A DontCare

在某些情況下,可能不會預先知道特定渲染目標的存儲操作。要推遲此決定,請unknown在創建MTLRenderPassAttachmentDescriptor對象時設置臨時值。在完成渲染過程的編碼之前,必須指定已知的存儲操作,否則會發生錯誤。設置該unknown值可以避免通過Store過早設置存儲操作而產生的潛在成本。

評估渲染過程之間的操作

應仔細評估在多個渲染過程中使用的渲染目標,以獲得渲染過程之間的存儲和加載操作的最佳組合。下面的表格列出了這些組合。

首先渲染傳遞存儲操作 第二次渲染傳遞加載動作
DontCare 以下操作之一:
DontCare
Clear
以下操作之一:
Store
MultisampleResolve
storeAndMultisampleResolve
Load

渲染命令編碼器(iOS和tvOS)

最佳實踐:盡可能合并渲染命令編碼器。

消除不必要的渲染命令編碼器可減少內存帶寬并提高性能。如果可能,可以通過將渲染命令編碼器合并到單個渲染過程中來實現這些目標。要確定兩個渲染命令編碼器是否兼容兼容,您必須仔細評估其渲染目標,加載和存儲操作,關系和依賴關系。兩個合并兼容的最簡單的標準渲染指令編碼器,RCE1并且RCE2,如下所示:

  • RCE1RCE2在同一幀中創建。
  • RCE1RCE2從同一個命令緩沖區創建。
  • RCE1是在之前創建的RCE2
  • RCE2共享相同的渲染目標RCE1
  • RCE2不從任何渲染目標中采樣RCE1
  • RCE1渲染目標存儲操作是StoreDontCare,并且RCE2渲染目標加載操作是LoadDontCare
  • RCE1和之間沒有創建其他渲染命令編碼器RCE2
    如果滿足這些條件,RCE1并且RCE2可以合并到單個渲染命令編碼器中,如圖下圖所示。
簡單的渲染命令編碼器合并

此外,如果RCE1能與之前(創建一個渲染指令編碼器合并RCE0),并RCE2可以與后(創建一個渲染指令編碼器合并RCE3),然后RCE0RCE1RCE2,并且RCE3都可以合并。

假設滿足所有其他條件,以下部分提供了評估渲染命令編碼器之間的合并兼容性的指南。

  • 渲染通道的詳細信息特定于您的應用; 因此,本指南無法提供有關如何合并特定渲染命令編碼器集的具體建議。渲染命令編碼器手動合并; 沒有Metal API可以自動為您執行合并。大多數合并是通過合并繪制調用,頂點或片段函數或渲染目標來完成的。有些合并甚至可以通過可編程混合來完成,如MetalDeferredLighting示例中所示。

合并命令編碼器中的渲染目標數量不得超過“ metal特征集”中記錄的限制。

評估 Rendering Pass Order

某些應用程序可能會開始編碼為渲染命令編碼器(RCE1),如果需要其他動態數據繼續,則會過早地結束初始渲染過程。然后,在單獨的渲染過程中使用第二渲染命令encoderRCE2)生成動態數據。然后,初始渲染過程繼續第三個渲染命令編碼器(RCE3)。下圖顯示了這種低效的順序,包括分離的渲染命令編碼器。

渲染過程的低效順序

如果RCE2不依賴RCE1,則RCE2不需要編碼RCE1。編碼RCE2首先允許RCE1RCE3合并,RCEM因為它們代表相同的渲染過程,并且它們的動態數據依賴性保證在渲染過程開始時可用。下圖顯示了這種改進的順序,包括合并的渲染命令編碼器。

渲染過程的改進順序

評估采樣依賴性

如果它們之間存在任何采樣依賴關系,則無法合并渲染命令編碼器。對于共享相同渲染目標的渲染命令編碼器,可以通過它們之間的其他渲染命令編碼器引入這些依賴關系,如下圖所示。

渲染命令編碼器之間的采樣依賴關系

RCE1RCE3共享相同的渲染目標,RT1RT2,和RT3。此外,之間的行動RCE1,并RCE3表示渲染通道的延續。但是,由于引入的采樣依賴性,這些渲染命令編碼器無法合并RCE2RCE2渲染到單獨的渲染目標RT4,由其進行采樣RCE3。此外,它后面的RCE2樣本RT3呈現RCE1。這些采樣依賴項定義了嚴格的渲染傳遞順序,可防止合并這些渲染命令編碼器。

評估渲染過程之間的操作

渲染命令編碼器渲染目標之間的存儲和加載操作并不像其他標準那樣重要,但有一些值得注意的額外考慮因素。使用以下準則進一步了解渲染命令編碼器之間的合并兼容性,RCE1RCE2基于其共享的渲染目標:

  • 如果存儲操作RCE1Store,并且加載操作RCE2Load,則渲染目標是合并兼容的,并且通常繼續渲染傳遞。

  • 如果存儲操作RCE1DontCare,并且加載操作RCE2[DontCare](https://developer.apple.com/documentation/metal/mtlloadaction/dontcare),則渲染目標是合并兼容的,并且通常用作中間資源。

  • 如果加載動作RCE2Clear,則如果可以在合并的渲染命令編碼器中執行基元清除操作,則首先將清除值渲染到顯示對齊的四邊形中,渲染目標是合并兼容的。

  • 有關為特定渲染目標選擇適當的加載和存儲操作的建議,請參閱加載和存儲操作最佳實踐。

命令緩沖區

最佳實踐:每幀提交盡可能少的命令緩沖區,而不會低估GPU的使用率。

命令緩沖區是Metal中提交的工作單元; 它們由CPU創建并由GPU執行。此關系允許您通過調整每幀提交的命令緩沖區數來平衡CPU和GPU工作。

大多數Metal應用程序通過實現三重緩沖,使其CPU工作比GPU工作提前一到兩幀。這意味著通常每個幀(最好是一個)僅提交一個或兩個命令緩沖區,通常排隊的CPU工作足以使GPU保持忙碌狀態。但是,如果CPU工作在GPU工作之前不能保持足夠遠,那么GPU將會處于閑置狀態。更頻繁的命令緩沖區提交可能會使GPU保持忙碌,但也可能引入CPU-GPU同步導致的CPU停頓。有效地管理這種權衡是提高性能的關鍵,可以通過儀器中的Metal System Trace分析模板來實現。

  • 有關三重緩沖的完整概述,請參閱三重緩沖最佳實踐。

間接緩沖

最佳實踐:如果您的繪制或調度調用參數是由GPU動態生成的,請使用間接緩沖區。

間接緩沖區是MTLBuffer具有表示繪制或調度調用參數的特定數據布局的對象。支持的布局由以下結構定義:

間接緩沖區允許發出依賴于調用時未知的動態參數的調用。這些參數可以在發出調用后動態生成,但是在關聯的渲染或計算傳遞開始執行時,它們必須始終可用。動態參數通常由GPU生成; 例如,補丁內核可以動態生成用于對曲面細分后頂點函數的補丁繪制調用的參數。

消除不必要的數據傳輸并減少處理器空閑時間

如果沒有間接緩沖區,GPU會生成調用參數并將其寫入常規緩沖區。CPU必須等到GPU完成所有工作,然后才能從常規緩沖區讀取參數并發出調用。然后GPU必須等到CPU完成所有工作才能執行調用。這種低效的序列如下圖所示。

在沒有間接緩沖區的情況下的調用

使用間接緩沖區,CPU不需要等待任何值,并且可以立即發出引用間接緩沖區的繪制調用。在CPU完成所有工作之后,GPU可以生成參數,在一次傳遞中將它們寫入間接緩沖區,并在另一次傳遞中執行與它們相關聯的調用。這種改進的順序如下圖所示。

使用間接緩沖區發出調用

間接緩沖區消除了CPUGPU之間不必要的數據傳輸,從而減少了處理器空閑時間。如果CPU不需要訪問繪制或調度調用的動態參數,請使用間接緩沖區。

匯編

函數和庫

最佳實踐:在構建時編譯函數并構建庫。

編譯Metal著色語言源代碼是Metal應用程序生命周期中最消耗資源的階段之一。Metal允許在構建時編譯圖形和計算函數,然后在運行時將它們作為庫加載,從而最大限度地降低了這一成本。

在編譯的時間里編譯你的庫

在構建應用程序時,Xcode會自動編譯.metal源文件并將它們構建到單個默認庫中。要獲取生成的MTLLibrary對象,請newDefaultLibrary在初始Metal設置期間調用該方法一次。

在運行時構建庫會導致顯著的性能成本。僅當圖形和計算功能是在運行時動態創建時才這樣做。在所有其他情況下,始終在構建時構建庫。

  • include用戶文件在運行時不支持該指令。

將您的功能分組到單個庫中

使用Xcode構建單個默認庫是最快,最有效的構建選項。如果必須使用Metal的命令行實用程序或運行時方法來構建庫,請合并Metal著色語言源代碼并將所有函數分組到單個庫中。如果可能,請避免創建多個庫。

Pipelines

最佳實踐:異步構建渲染和計算Pipelines。

擁有多個渲染或計算pipelines允許應用程序針對特定任務使用不同的狀態配置。異步構建這些管道可以最大限度地提高性能和并行性。預先構建所有已知的pipelines,避免延遲加載。下面代碼顯示了如何異步構建多個渲染pipelines

const uint32_t pipelineCount;
dispatch_queue_t dispatch_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// Dispatch the render pipeline build
__block NSMutableArray<id<MTLRenderPipelineState>> *pipelineStates = [[NSMutableArray alloc] initWithCapacity:pipelineCount];

dispatch_group_t pipelineGroup = dispatch_group_create();
for(uint32_t pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++)
{
id <MTLFunction> vertexFunction = [_defaultLibrary newFunctionWithName:vertexFunctionNames[pipelineIndex]];
id <MTLFunction> fragmentFunction = [_defaultLibrary newFunctionWithName:fragmentFunctionNames[pipelineIndex]];

MTLRenderPipelineDescriptor* pipelineDescriptor = [MTLRenderPipelineDescriptor new];
pipelineDescriptor.vertexFunction = vertexFunction;
pipelineDescriptor.fragmentFunction = fragmentFunction;
/* Configure additional descriptor properties */

dispatch_group_enter(pipelineGroup);
[_device newRenderPipelineStateWithDescriptor:pipelineDescriptor completionHandler: ^(id <MTLRenderPipelineState> newRenderPipeline, NSError *error )
 {
     // Add error handling if newRenderPipeline is nil
     pipelineStates[pipelineIndex] = newRenderPipeline;
     dispatch_group_leave(pipelineGroup);
 }];
}

/* Do more work */

// Wait for build to complete
dispatch_group_wait(pipelineGroup, DISPATCH_TIME_FOREVER);

/* Use the render pipelines */
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容