2023 Developer ToolsGraphics & Games
WWDC23 · 27 min · Developer Tools / Graphics & Games
Bring your game to Mac, Part 3: Render with Metal
Discover how you can support Metal in your rendering code as we close out our three-part series on bringing your game to Mac. Once you’ve evaluated your existing Windows binary with the game porting toolkit and brought your HLSL shaders over to Metal, learn how you can optimally implement the features that high-end, modern games require. We’ll show you how to manage GPU resource bindings, residency, and synchronization. Find out how to optimize GPU commands submission, render rich visuals with MetalFX Upscaling, and more. To get the most out of this session, we recommend first watching “Bring your game to Mac, Part 1: Make a game plan” and “Bring your game to Mac, Part 2: Compile your shaders" from WWDC23.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 7 snippets
Encode the texture tables.
// Encode the texture tables outside of the rendering loop.
id<MTLBuffer> textureTable = [device newBufferWithLength:sizeof(MTLResourceID) * texturesCount
options:MTLResourceStorageModeShared];
MTLResourceID* textureTableCPUPtr = (MTLResourceID*)textureTable.contents;
for (uint32_t i = 0; i < texturesCount; ++i)
{
// create the textures.
id<MTLTexture> texture = [device newTextureWithDescriptor:textureDesc[i]];
// encode texture in argument buffer
textureTableCPUPtr[i] = texture.gpuResourceID;
} Encode the sampler tables.
// Encode the sampler tables outside of the rendering loop.
id<MTLBuffer> samplerTable = [device newBufferWithLength:sizeof(MTLResourceID) * samplersCount
options:MTLResourceStorageModeShared];
MTLResourceID* samplerTableCPUPtr = (MTLResourceID*)samplerTable.contents;
for (uint32_t i = 0; i < samplersCount; ++i)
{
// create sampler descriptor
MTLSamplerDescriptor* desc = [MTLSamplerDescriptor new];
desc.supportArgumentBuffers = YES;
. . .
// create a sampler
id<MTLSamplerState> sampler = [device newSamplerStateWithDescriptor:desc];
// encode the sampler in argument buffer
samplerTableCPUPtr[i] = sampler.gpuResourceID;
} Encode the top level argument buffer.
// Encode the top level argument buffer.
struct TopLevelAB
{
MTLResourceID* textureTable;
float* myBuffer;
uint32_t myConstant;
MTLResourceID* samplerTable;
};
id<MTLBuffer> topAB = [device newBufferWithLength:sizeof(TopLevelAB)
options:MTLResourceStorageModeShared];
TopLevelAB* topABCPUPtr = (TopLevelAB*)topAB.contents;
topABCPUPtr->textureTable = (MTLResourceID*)textureTable.gpuAddress;
topABCPUPtr->myBuffer = (float*)myBuffer.gpuAddress;
topABCPUPtr->myConstant = 128;
topABCPUPtr->samplerTable = (MTLResourceID*)samplerTable.gpuAddress; Allocate the read-only resources.
// Allocate the read-only resources from a heap.
MTLHeapDescriptor* heapDesc = [MTLHeapDescriptor new];
heapDesc.size = requiredSize;
heapDesc.type = MTLHeapTypeAutomatic;
id<MTLHeap> heap = [device newHeapWithDescriptor:heapDesc];
// Allocate the textures and the buffers from the heap.
id<MTLTexture> texture = [heap newTextureWithDescriptor:desc];
id<MTLBuffer> buffer = [heap newBufferWithLength:length options:options];
. . .
// Make the heap resident once for each encoder that uses it.
[encoder useHeap:heap]; Allocate the writable resources.
// Allocate the writable resources individually.
id<MTLTexture> textureRW = [device newTextureWithDescriptor:desc];
id<MTLBuffer> bufferRW = [device newBufferWithLength:length options:options];
// Mark these resources resident when they're needed in the current encoder.
// Specify the resource usage in the encoder using MTLResourceUsage.
[encoder useResource:textureRW usage:MTLResourceUsageWrite stages:stage];
[encoder useResource:bufferRW usage:MTLResourceUsageRead stages:stage]; Encode the execute indirect
// Encode the execute indirect command as a series of indirect draw calls.
for (uint32_t i = 0; i < maxDrawCount; ++i)
{
// Encode the current indirect draw call.
[renderEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
indexType:MTLIndexTypeUInt16
indexBuffer:indexBuffer
indexBufferOffset:indexBufferOffset
indirectBuffer:drawArgumentsBuffer
indirectBufferOffset:drawArgumentsBufferOffset];
// Advance the draw arguments buffer offset to the next indirect arguments.
drawArgumentsBufferOffset += sizeof(MTLDrawIndexedPrimitivesIndirectArguments);
} Translate the indirect draw arguments to ICB.
// Kernel written in Metal Shading Language to translate the indirect draw arguments to an ICB.
kernel void translateToICB(device const Command* indirectCommands [[ buffer(0) ]],
device const ICBContainerAB* icb [[ buffer(1) ]],
. . .)
{
. . .
device const Command* indirectCommand = &indirectCommands[commandIndex];
device const MTLDrawIndexedPrimitivesIndirectArguments* args =
&command->mdiBuffer[mdiIndex];
render_command drawCall(icb->buffer, indirectCommand->mdiCmdStart + mdiIndex);
if(args->indexCount > 0 && args->instanceCount > 0) {
encodeCommand(indirectCommand, args, drawCall);
}
else {
cmd.reset();
}
}
// Encode a render command on the GPU.
void encodeCommand(device const Command* indirectCommand,
device const MTLDrawIndexedPrimitivesIndirectArguments* args,
thread render_command& drawCall)
{
drawCall.set_render_pipeline_state(indirectCommand->pso);
for(ushort i = 0; i < indirectCommand->vertexBuffersCount; ++i) {
drawCall.set_vertex_buffer(indirectCommand->vertexBuffer[i].buffer,
indirectCommand->vertexBuffer[i].slot);
}
for(ushort i = 0; i < indirectCommand->fragmentBuffersCount; ++i) {
drawCall.set_fragment_buffer(indirectCommand->fragmentBuffer[i].buffer,
indirectCommand->fragmentBuffer[i].slot);
}
drawCall.draw_indexed_primitives(primitive_type::triangle,
args->indexCount,
indirectCommand->indexBuffer + args->indexStart,
args->instanceCount,
args->baseVertex,
args->baseInstance);
} Resources
Related sessions
-
15 min -
19 min -
22 min -
40 min -
24 min -
14 min -
34 min -
45 min