Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

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 ↗

Transcript all transcripts

Chapters

Code shown on screen · 7 snippets

Encode the texture tables. objectivec · at 3:55 ↗
// 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. objectivec · at 4:33 ↗
// 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. objectivec · at 5:05 ↗
// 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. objectivec · at 6:49 ↗
// 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. objectivec · at 7:34 ↗
// 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 objectivec · at 19:31 ↗
// 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. cpp · at 21:48 ↗
// 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