寫在前面:
對Metal技術感興趣的同學,可以關注我的專題:Metal專輯
也可以關注我個人的簡書賬號:張芳濤
所有的代碼存儲的Github地址是:Metal
正文
本文是摘抄的蘋果官方文檔,蘋果的文檔里面有Metal Best Practices Guide這個章節。
Metal
提供對GPU
的最低開銷訪問,使您能夠在iOS
,macOS
和tvOS
上最大化應用程序的圖形和計算潛力。每毫秒和每一點都是Metal
應用程序和用戶體驗的組成部分 ,所以 我們有責任通過遵循本指南中描述的最佳實踐來確保Metal
應用程序盡可能高效地運行。除非另有說明,否則這些最佳實踐適用于支持Metal
的所有平臺。
一個高性能的Metal
應用程序需要具備以下特點:
低
CPU
開銷。Metal
旨在減少或消除許多CPU
端性能瓶頸。只有按照建議使用Metal API
,應用才能從此受益。最佳
GPU
性能。Metal
允許創建并向GPU
提交命令。要優化GPU
性能,您的應用應優化這些命令的配置和組織。處理器持續和并行工作能力。
Metal
旨在最大化CPU
和GPU
并行性。應用應該讓這些處理器起作用并同時工作。有效的資源管理。
Metal
為您的資源對象提供簡單而強大的接口。應用應該有效地管理這些資源,以減少內存消耗并提高訪問速度。
資源管理
對象持久化
最佳實踐:盡早創建持久對象并經常重用它們。
Metal
框架提供了在應用程序的整個生命周期內管理持久對象的協議。這些對象的創建成本很高,但通常會初始化一次并經常重復使用。不需要在每個渲染或計算循環的開頭創建這些對象。
首先初始化的設備和命令隊列
MTLCreateSystemDefaultDevice在應用程序啟動時 調用該函數以獲取默認系統設備。接下來,調用newCommandQueue或者 newCommandQueueWithMaxCommandBufferCount:方法創建一個命令隊列,用于在該設備上執行GPU指令。
所有應用程序應該只MTLDevice為每個GPU 創建一個對象,并將其重用于該GPU上的所有Metal工作。大多數應用程序應該MTLCommandQueue每個GPU 只創建一個對象,但如果每個命令隊列代表不同的Metal工作(例如,非實時計算處理和實時圖形渲染),您可能需要更多。
- 一些
macOS
設備具有多個GPU
。如果需要使用多個GPU
,請調用該MTLCopyAllDevices函數以獲取可用設備的數組。為您使用的每個GPU
創建并保留至少一個命令隊列。
在構建時編譯函數并構建Library
有關在構建時編譯函數和構建庫的概述,請參閱Libraries最佳實踐。
在運行時,使用MTLLibrary和MTLFunction對象訪問圖形庫和計算函數。避免在運行時構建庫或在渲染或計算循環期間獲取函數。
如果需要配置多個渲染或計算管道,請MTLFunction盡可能重用對象。您可以釋放MTLLibrary和MTLFunction建設的所有對象后,渲染并依賴于它們的計算pipelines
。
建立一次Pipelines并經常重復使用
構建可編程管道涉及對GPU狀態的評估工作非常消耗性能。應該只構建MTLRenderPipelineState和MTLComputePipelineState對象一次,然后為創建的每個新渲染或計算命令編碼器重用。不要為新的命令編碼器構建新的管道。有關異步構建多個pipelines
的概述,請參閱pipelines最佳實踐。
- 注意 : 除了渲染和計算管線,則可以選擇創建MTLDepthStencilState和MTLSamplerState封裝深度,模板,和采樣器狀態的對象。這些對象較輕便,但也應僅創建一次并經常重復使用。
預先分配資源存儲
資源數據可以是靜態的或動態的,并可在應用程序的整個生命周期的各個階段進行訪問。 但是,應盡早創建為此數據分配內存的MTLBuffer和MTLTexture對象。創建這些對象后,資源屬性和存儲分配是不可變的,但數據本身不是; 可以在必要時更新數據。
盡可能 重用MTLBuffer和MTLTexture對象,特別是對于靜態數據。避免在渲染或計算循環期間創建新資源,即使對于動態數據也是如此。有關緩沖區和紋理的更多信息,請參閱資源管理和三重緩沖最佳實踐。
資源選項
最佳實踐:設置適當的資源存儲模式和紋理使用選項。
必須正確配置您的Metal
資源,以利用快速內存??訪問和驅動程序性能優化。資源存儲模式允許定義MTLBuffer和MTLTexture對象的存儲位置和訪問權限。紋理使用選項允許顯式聲明打算如何使用MTLTexture對象。
熟悉設備內存模型
設備內存型號因操作系統而異。iOS
和tvOS
設備支持統一的內存模型,其中CPU
和GPU
共享系統內存。macOS
設備支持具有CPU
可訪問系統內存和GPU
可訪問視頻內存的獨立內存模型。
- 一些
macOS
設備具有集成的GPU
。在這些設備中,驅動程序優化底層架構以支持離散內存模型。macOS Metal
應該始終以離散內存模型為目標。 - 所有
iOS
和tvOS
設備都集成了GPU
。
選擇適當的資源存儲模式(iOS和tvOS)
在iOS
和tvOS
中,Shared模式定義了CPU
和GPU
Private都可訪問的系統內存,而模式定義了只能由GPU
訪問的系統內存。
該Shared模式通常是iOS
和tvOS
資源的正確選擇。Private僅當CPU
從不訪問資源時才選擇模式。
- 在
iOS
和tvOS
中,memoryless存儲模式用于無記憶紋理。此存儲模式只能用于存儲在片上磁貼存儲器中的臨時渲染目標。有關詳細信息,請參閱Memoryless Textures
Metal編程指南。
選擇適當的資源存儲模式(macOS)
在macOS中,Shared模式定義了CPU
和GPU
都可訪問的系統內存,而Private模式定義了只能由GPU
訪問的視頻內存。
此外,macOS
實現了Managed為資源定義同步內存對的模式,其中一個副本位于系統內存中,另一個副本位于視頻內存中。管理資源受益于對每個資源副本的快速CPU
和GPU
訪問,同步這些副本所需的API
調用最少。
- 在
macOS
中,Shared模式僅適用于緩沖區,而不適用于紋理。緩沖區數據通常是線性的,從而產生簡單的GPU
訪問模式。紋理更復雜,其數據通常是平鋪或調配的,從而導致更復雜的GPU
訪問模式。
緩沖存儲模式(macOS)
使用以下準則確定特定緩沖區的適當存儲模式。
如果
GPU
專門訪問緩沖區,請選擇Private模式。這是GPU
生成的數據的常見情況,例如每個補丁鑲嵌的因子。如果
CPU
專門訪問緩沖區,請選擇Shared模式。這是一種罕見的情況,通常是blit
操作中的中間步驟。-
如果
CPU
和GPU
都訪問緩沖區,就像大多數頂點數據一樣,請考慮以下幾點并參考表3-1:對于頻繁更改的小型數據,請選擇Shared模式。將數據復制到視頻存儲器的開銷可能比直接訪問
GPU
系統存儲器的開銷更大。對于不經常更改的中型數據,請選擇Managed模式。始終在修改托管緩沖區的內容后調用適當的同步方法。
執行CPU
寫入后,調用didModifyRange:方法以通知Metal
有關已修改的特定數據范圍; 這允許Metal
僅更新視頻內存副本中的特定范圍。
在編寫GPU寫入之后,編碼包括對synchronizeResource:方法的調用的blit
操作; 這允許Metal
在相關命令緩沖區完成執行后更新系統內存副本。對于永不更改的大型數據,請選擇Private模式。使用Shared模式初始化并填充源緩沖區,然后將其數據
blit
到具有Private模式的目標緩沖區。這是一次性成本的最佳操作。
表3-1為
CPU
和GPU
訪問的緩沖區數據選擇存儲模式
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模式的目標紋理。這是靜態紋理的常見情況,例如漫反射貼圖。-
如果
CPU
和GPU
頻繁訪問紋理,請選擇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三重緩沖實現:
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
緩沖區綁定
最佳實踐:使用適當的方法將緩沖區數據綁定到圖形或計算功能。
- 本章使用頂點函數綁定作為示例。Metal為和類中的片段和內核函數提供了等效的API 。MTLRenderCommandEncoderMTLComputeCommandEncoder
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
,請調用命令緩沖區的presentDrawable:方法而不是drawable
的present方法。presentDrawable:是一種方便的方法,present通過命令緩沖區的addScheduledHandler:回調調用給定的drawable
方法。
盡量不要長期持有Drawable
Drawable
是由Core Animation
框架創建和維護的相當消耗性能的系統資源。它們存在于有限且可重復使用的資源池中,并且在您的應用程序請求時可能可用,也可能不可用。如果在請求時沒有可用的可繪制,則調用線程將被阻塞,直到新的可繪制可用(通常在下一個顯示刷新間隔)。
Drawable
是由Core Animation
框架創建和維護的昂貴系統資源。它們存在于有限且可重復使用的資源池中,并且在您的應用程序請求時可能可用,也可能不可用。如果在請求時沒有可用的可繪制,則調用線程將被阻塞,直到新的可繪制可用(通常在下一個顯示刷新間隔)。
要盡可能簡短地持有drawable
,請執行以下兩個步驟:
1:總是盡可能晚地獲得抽簽; 優選地,緊接在編碼屏幕上渲染通道之前。幀的
CPU
工作可能包括動態數據更新和屏幕外渲染過程,您可以在獲取可繪制之前執行這些過程。2:務必盡快釋放
drawable
,越早越好。在完成幀的CPU
工作之后立即執行。在自動釋放池塊中需要包含渲染循環,以避免可能出現多個drawable
的死鎖情況。從
iOS 10
和tvOS 10
開始,可以安全地保存drawables
以用于演示后屬性查詢,例如drawableID和presentedTime。否則,drawables應該在不再需要時釋放,這通常是在調用命令緩沖區的presentDrawable:方法之后。
圖6-1顯示了drawable
相對于其他CPU
工作的生命周期。
使用MetalKit視圖與Drawables交互
使用MTKView對象是與drawable
交互的首選方式。一個MTKView目的是通過一個備份CAMetalLayer對象并提供currentDrawable獲取用于當前幀中的可拉伸性。當前幀呈現到此drawable
中,并且該presentDrawable:方法調度實際呈現以在下一顯示刷新間隔發生。該currentDrawable屬性在每幀結束時自動更新。
一個MTKView對象還提供了currentRenderPassDescriptor一個引用當前繪制的紋理簡便屬性; 使用此屬性創建渲染命令編碼器,該編碼器呈現為當前的drawable
。對currentRenderPassDescriptor屬性的調用隱式獲取當前幀的drawable
,然后將其存儲在currentDrawable屬性中。
- 如果創建由對象支持的自己的子類UIView或NSView
子類,則
CAMetalLayer必須顯式獲取drawable并使用其紋理來配置渲染過程描述符。您也可以為自己的MTKView對象執行此操作,但簡單地使用currentRenderPassDescriptor便利屬性要容易得多。有關如何從a UIView或NSView子類獲取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類提供限定天然尺寸和物理屏幕的比例因子兩個屬性:nativeBounds和nativeScale。查詢nativeBounds屬性以確定屏幕的本機邊界矩形(以像素為單位)。查詢nativeScale屬性以確定用于將點轉換為像素的本機比例因子。
- 在
iOS
和tvOS
中,大多數繪圖技術都以磅而不是像素來衡量大小。您的金屬應用應始終以像素為單位測量大小,并完全避免使用點數。要了解有關這兩個單位之間差異的更多信息,請參閱點數與像素數。
使用MetalKit視圖支持本機屏幕比例
MTKView級自動支持本機屏幕比例。默認情況下,視圖當前drawable
的大小始終保證與視圖本身的大小相匹配。
- 如果創建UIView由CAMetalLayer對象支持的子類,則必須先設置視圖的contentScaleFactor屬性以匹配屏幕的nativeScale屬性。接下來,確保在視圖大小發生變化時調整渲染目標的大小。最后,調整圖層的drawableSize屬性以匹配本機屏幕比例。
幀率(iOS和tvOS)
最佳實踐:以一致且穩定的幀速率顯示drawables
。
大多數應用程序的目標幀速率為60 FPS
,相當于每幀16.67 ms
。但是,在此時間內始終無法完成幀工作的應用應針對較低的幀速率以避免抖動。
- 實時游戲的最低可接受幀速率為
30 FPS
。較低的幀速率被認為是糟糕的用戶體驗,應該避免這種情的發生。如果應用無法保持30 FPS
的最低可接受幀速率,則應考慮進一步優化或減少工作負載(每幀花費少于33.33
ms)。
查詢和調整幀率
可以通過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
的演示時間與應用程序的渲染循環同步。下面的代碼顯示了preferredFramesPerSecond與presentDrawable: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
和其他可顯示的渲染目標,情況總是如此。
- 如果需要保留渲染目標的內容,請選擇Store操作。對于
如果渲染目標是多重采樣紋理,請看下面的表格。
保留多重采樣內容 | 解析指定的紋理 | 已解決的內容已保留 | 存儲操作 |
---|---|---|---|
是 | 是 | 是 | storeAndMultisampleResolve |
沒有 | 是 | 是 | MultisampleResolve |
是 | 沒有 | N / A | Store |
沒有 | 沒有 | N / A | DontCare |
- 如果需要執行存儲和解析操作,請始終將storeAndMultisampleResolve操作與單個渲染命令編碼器一起使用。某些功能集不支持該storeAndMultisampleResolve操作; 相反,通過使用Store和MultisampleResolve動作,使用兩個渲染命令編碼器執行存儲和解析操作。
在某些情況下,可能不會預先知道特定渲染目標的存儲操作。要推遲此決定,請unknown在創建MTLRenderPassAttachmentDescriptor對象時設置臨時值。在完成渲染過程的編碼之前,必須指定已知的存儲操作,否則會發生錯誤。設置該unknown值可以避免通過Store過早設置存儲操作而產生的潛在成本。
評估渲染過程之間的操作
應仔細評估在多個渲染過程中使用的渲染目標,以獲得渲染過程之間的存儲和加載操作的最佳組合。下面的表格列出了這些組合。
首先渲染傳遞存儲操作 | 第二次渲染傳遞加載動作 |
---|---|
DontCare | 以下操作之一: DontCare Clear |
以下操作之一: Store MultisampleResolve storeAndMultisampleResolve |
Load |
渲染命令編碼器(iOS和tvOS)
最佳實踐:盡可能合并渲染命令編碼器。
消除不必要的渲染命令編碼器可減少內存帶寬并提高性能。如果可能,可以通過將渲染命令編碼器合并到單個渲染過程中來實現這些目標。要確定兩個渲染命令編碼器是否兼容兼容,您必須仔細評估其渲染目標,加載和存儲操作,關系和依賴關系。兩個合并兼容的最簡單的標準渲染指令編碼器,RCE1
并且RCE2
,如下所示:
-
RCE1
并RCE2
在同一幀中創建。 -
RCE1
并RCE2
從同一個命令緩沖區創建。 -
RCE1
是在之前創建的RCE2
。 -
RCE2
共享相同的渲染目標RCE1
。 -
RCE2
不從任何渲染目標中采樣RCE1
。 -
RCE1
渲染目標存儲操作是Store或DontCare,并且RCE2
渲染目標加載操作是Load或DontCare。 - 在
RCE1
和之間沒有創建其他渲染命令編碼器RCE2
。
如果滿足這些條件,RCE1
并且RCE2
可以合并到單個渲染命令編碼器中,如圖下圖所示。
此外,如果RCE1
能與之前(創建一個渲染指令編碼器合并RCE0
),并RCE2
可以與后(創建一個渲染指令編碼器合并RCE3
),然后RCE0
,RCE1
,RCE2
,并且RCE3
都可以合并。
假設滿足所有其他條件,以下部分提供了評估渲染命令編碼器之間的合并兼容性的指南。
- 渲染通道的詳細信息特定于您的應用; 因此,本指南無法提供有關如何合并特定渲染命令編碼器集的具體建議。渲染命令編碼器手動合并; 沒有Metal API可以自動為您執行合并。大多數合并是通過合并繪制調用,頂點或片段函數或渲染目標來完成的。有些合并甚至可以通過可編程混合來完成,如MetalDeferredLighting示例中所示。
合并命令編碼器中的渲染目標數量不得超過“ metal特征集”中記錄的限制。
評估 Rendering Pass Order
某些應用程序可能會開始編碼為渲染命令編碼器(RCE1
),如果需要其他動態數據繼續,則會過早地結束初始渲染過程。然后,在單獨的渲染過程中使用第二渲染命令encoder
(RCE2
)生成動態數據。然后,初始渲染過程繼續第三個渲染命令編碼器(RCE3
)。下圖顯示了這種低效的順序,包括分離的渲染命令編碼器。
如果RCE2
不依賴RCE1
,則RCE2
不需要編碼RCE1
。編碼RCE2
首先允許RCE1
和RCE3
合并,RCEM
因為它們代表相同的渲染過程,并且它們的動態數據依賴性保證在渲染過程開始時可用。下圖顯示了這種改進的順序,包括合并的渲染命令編碼器。
評估采樣依賴性
如果它們之間存在任何采樣依賴關系,則無法合并渲染命令編碼器。對于共享相同渲染目標的渲染命令編碼器,可以通過它們之間的其他渲染命令編碼器引入這些依賴關系,如下圖所示。
RCE1
和RCE3
共享相同的渲染目標,RT1
,RT2
,和RT3
。此外,之間的行動RCE1
,并RCE3
表示渲染通道的延續。但是,由于引入的采樣依賴性,這些渲染命令編碼器無法合并RCE2
。RCE2
渲染到單獨的渲染目標RT4
,由其進行采樣RCE3
。此外,它后面的RCE2
樣本RT3
呈現RCE1
。這些采樣依賴項定義了嚴格的渲染傳遞順序,可防止合并這些渲染命令編碼器。
評估渲染過程之間的操作
渲染命令編碼器渲染目標之間的存儲和加載操作并不像其他標準那樣重要,但有一些值得注意的額外考慮因素。使用以下準則進一步了解渲染命令編碼器之間的合并兼容性,RCE1
并RCE2
基于其共享的渲染目標:
如果存儲操作
RCE1
是DontCare,并且加載操作RCE2
是[DontCare](https://developer.apple.com/documentation/metal/mtlloadaction/dontcare)
,則渲染目標是合并兼容的,并且通常用作中間資源。如果加載動作
RCE2
是Clear,則如果可以在合并的渲染命令編碼器中執行基元清除操作,則首先將清除值渲染到顯示對齊的四邊形中,渲染目標是合并兼容的。有關為特定渲染目標選擇適當的加載和存儲操作的建議,請參閱加載和存儲操作最佳實踐。
命令緩沖區
最佳實踐:每幀提交盡可能少的命令緩沖區,而不會低估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
可以生成參數,在一次傳遞中將它們寫入間接緩沖區,并在另一次傳遞中執行與它們相關聯的調用。這種改進的順序如下圖所示。
間接緩沖區消除了CPU
和GPU
之間不必要的數據傳輸,從而減少了處理器空閑時間。如果CPU
不需要訪問繪制或調度調用的動態參數,請使用間接緩沖區。
匯編
函數和庫
最佳實踐:在構建時編譯函數并構建庫。
編譯Metal
著色語言源代碼是Metal
應用程序生命周期中最消耗資源的階段之一。Metal
允許在構建時編譯圖形和計算函數,然后在運行時將它們作為庫加載,從而最大限度地降低了這一成本。
在編譯的時間里編譯你的庫
在構建應用程序時,Xcode
會自動編譯.metal
源文件并將它們構建到單個默認庫中。要獲取生成的MTLLibrary對象,請newDefaultLibrary在初始Metal
設置期間調用該方法一次。
- 如果應用程序具有自定義構建
pipeline
,您可能更喜歡使用Metal
的命令行實用程序來構建庫。有關詳細信息,請參閱 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 */