本章讨论如何创建和使用 MTLComputeCommandEncoder object 来 encode data-parallel compute processing state 以及 command 以及 submit them for execution on a device
下面是 perform data-parallel computation 的步骤
MTLFunction 包含 data-parallel code ,可以executed by MTLComputePipelineState。 MTLComputeCommandEncoder encode command 去设置 argument 以及执行 compute function。由于创建compute pipeline state 需要昂贵的编译 metal shading language code,所以可以以block 的方式 或者异步的方式 schdule 这种工作来的到最好的性能
在创建 MTLComputePipelineState object 的时候,也可以选择同时创建 reflection data 包含 compute function的detail以及argument。newComputePipelineStateWithFunction:options:reflection:error: 和 newComputePipelineStateWithFunction:options:completionHandler: 方法可以做到这一点。如果不需要,则避免创建 reflection data。
MTLComputeCommandEncoder 的 setComputePipelineState 方法指定 state,包含一个编译好的 compute shader function 用于 data-parallel compute pass。在任意时间,compute command encoder 只能关联一个 compute function
MTLComputeCommandEncoder 方法定义了资源(buffer、texture、sampler state、threadgroup memory)作为argument用于MTLComputePIpelineState 的 compute function
上述每个方法都将一个或者多个resource 对应到argument,如下图所示
buffer、texture、sampler state argument table 的最大数量限制以及threadgroup memory 的最大限制,参考 Implementation Limits
调用 MTLComputeCommandEncoder 的 dispatchThreadgroups:threadsPerThreadgroup: 方法去执行 compute function,同时设置了threadgroup 的dimension和数量。可以查询 MTLComputePipelineState 的 threadExecutionWidth and maxTotalThreadsPerThreadgroup 属性去优化 compute function 在当前 device 上的执行
threadgroup中thread 的数量的计算方式是:threadsPerThreadgroup: threadsPerThreadgroup.width * threadsPerThreadgroup.height * threadsPerThreadgroup.depth。 maxTotalThreadsPerThreadgroup 属性指定当前 compute function 在当前设备上 一个 threadgroup 最多可以用的thread数量。
compute command 按照 它们 encoded into command buffer 的顺序执行。当所有thredgroup都完毕切所有结果都写入memory 的时候,该compute command 才算结束。由于这个顺序,一个compute command 的结果可以被用于该command buffer 中之后 encode 的command。
当结束 encoding command 到 compute command encoder 的时候,调用 MTLComputeCommandEncoder 的endEncoding 方法。结束了前一个 command encoder后,可以创建一个新的任意类型的command encoder encode 其它command到当前command buffer中
下面例子,创建和使用 MTLComputeCommandEncoder 执行针对一个 image transformation on specified data 的 parallel 计算(本例子不展示 device、library、command queue、resource 的创建和初始化)。这个例子创建了一个 command buffer然后使用它创建 MTLComputeCommandEncoder。然后创建一个 MTLFunction,并对应从 MTLLibrary 加载的 filter_main。然后这个function被用于创建 MTLComputePipelineState object 叫做 filterState
该compute function 针对 image inputImage 执行 image transformation 和 filter 操作,并将结束返回到 outputImage。首先调用 setTexture:atIndex: and setBuffer:offset:atIndex: 方法来将texture 和buffer 指定到 argument table。parasBuffer 指定 image transformation 中用到的数据,inputTableData 指定 filter weights。compute function 按照 2D threadgroup 执行,size 16*16 in each dimension。dispatchThreadgroups:threadsPerThreadgroup: 方法用于 dispatch thread 执行 compute function,然后调用 MTLComputeCommandEncoder 的 endEncoding 方法。最终调用 MTLCommandBuffer 的 commit 方法使得command 尽快被执行
id <.MTLDevice> device; id <.MTLLibrary> library; id <.MTLCommandQueue> commandQueue; id <.MTLTexture> inputImage; id <.MTLTexture> outputImage; id <.MTLTexture> inputTableData; id <.MTLBuffer> paramsBuffer; // ... Create and initialize device, library, queue, resources // Obtain a new command buffer id <.MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer]; // Create a compute command encoder id <.MTLComputeCommandEncoder> computeCE = [commandBuffer computeCommandEncoder]; NSError *errors; id <.MTLFunction> func = [library newFunctionWithName:@"filter_main"]; id <.MTLComputePipelineState> filterState = [device newComputePipelineStateWithFunction:func error:&errors]; [computeCE setComputePipelineState:filterState]; [computeCE setTexture:inputImage atIndex:0]; [computeCE setTexture:outputImage atIndex:1]; [computeCE setTexture:inputTableData atIndex:2]; [computeCE setBuffer:paramsBuffer offset:0 atIndex:0]; MTLSize threadsPerGroup = {16, 16, 1}; MTLSize numThreadgroups = {inputImage.width/threadsPerGroup.width, inputImage.height/threadsPerGroup.height, 1}; [computeCE dispatchThreadgroups:numThreadgroups threadsPerThreadgroup:threadsPerGroup]; [computeCE endEncoding]; // Commit the command buffer [commandBuffer commit];
下面代码为上述例子的 shader code ( read_and_transform and filter_table function 为 placeholders)
kernel void filter_main( texture2d<.float,access::read> inputImage [[ texture(0) ]], texture2d<.float,access::write> outputImage [[ texture(1) ]], uint2 gid [[ thread_position_in_grid ]], texture2d<.float,access::sample> table [[ texture(2) ]], constant Parameters* params [[ buffer(0) ]] ) { float2 p0 = static_cast<.float2>(gid); float3x3 transform = params->transform; float4 dims = params->dims; float4 v0 = read_and_transform(inputImage, p0, transform); float4 v1 = filter_table(v0,table, dims); outputImage.write(v1,gid); }
本节教程就到此结束,希望大家继续阅读我之后的教程。
谢谢大家,再见!
原创技术文章,撰写不易,转载请注明出处:电子设备中的画家|王烁 于 2022 年 5 月 26 日发表,原文链接(http://geekfaner.com/shineengine/blog51_MetalProgrammingGuide_6.html)