z Using Argument Buffers :: C++, 그래픽 기술 블로그

이전까지는 복잡한 모델의 텍스처와 질감 등 여러 요소들을 각자 따로 CPU에서 GPU로 할당하는 작업을 수행하였습니다. 이는 CPU 오버헤드를 유발하여 자원을 낭비하기 때문에, 이를 한번에 수행하기 위하여 argument buffer를 도입하였습니다. 이는 texture, sampler, buffer과 같이 렌더링에 있어 필수불가결하지만 비싼 작업들이 반복적으로 수행되어야 하는데, 이를 argument buffer로 감싸 한번에 설정하는 것이 가능해집니다.

 

이전에 argument buffer로 자원에 접근하는 방법을 배웠다면 이번에는 자원들의 배열과 자원 heap을 argument buffer과 함꼐 사용하는 방법을 배워보겠습니다. 특히 배열을 포함하는 argument buffer 구조체를 어떻게 정의하고 heap에서 자원을 어떻게 할당하고 사용하는 지에 집중하도록 하겠습니다. 그 후에 argument buffer 내의 자원을 그래픽 및 연산 함수에서 어떻게 인코딩 하는지 배우고 이를 어떻게 쓰고 읽는 지에 대해서 배워나가겠습니다.

 

Define argument buffers

MSL에서는 argument buffer를 사용자 지정 구조로 정의할 수 있어, 다음과 같이 결정할 수 있습니다. 밑의 예시는 metal3의 기준을 따랐습니다.

struct FragmentShaderArguments {
    texture2d<half>  exampleTexture;
    sampler          exampleSampler;
    DEVICE float    *exampleBuffer;
    uint32_t         exampleConstant;
};

이 때, metal2에서는 [[ id(n) ]] 속성 한정자로 개별 자원의 인덱스를 정의하였지만, 3에서는 직접적으로 버퍼에 리소스를 사용할 수 있습니다.

fragment float4
fragmentShader(       RasterizerData            in                 [[ stage_in ]],
               device FragmentShaderArguments & fragmentShaderArgs [[ buffer(AAPLFragmentBufferIndexArguments) ]])

 

Set resourse handles in an argument buffer with Metal 3

Metal 3에서는 렌더러가 직접 리소스에 GPU resourse handles를 씁니다. 렌더러는 공유되는 구조체로 연산자의 크기를 정의하여 버퍼에 필요한 크기를 결정하게 됩니다.

MTL::Function* pVertexFn = _pShaderLibrary->newFunction( NS::String::string( "vertexMain", UTF8StringEncoding ) );
MTL::ArgumentEncoder* pArgEncoder = pVertexFn->newArgumentEncoder( 0 );

MTL::Buffer* pArgBuffer = _pDevice->newBuffer( pArgEncoder->encodedLength(), MTL::ResourceStorageModeManaged );
_pArgBuffer = pArgBuffer;

pArgEncoder->setArgumentBuffer( _pArgBuffer, 0 );

pArgEncoder->setBuffer( _pVertexPositionsBuffer, 0, 0 );
pArgEncoder->setBuffer( _pVertexColorsBuffer, 0, 1 );

 

Set argument buffers

Metal은 GPU가 접근하는 메모리를 효율적으로 관리하기에, 어떠한 접근 레벨을 가졌는 지 사전에 확인을 합니다. 이는 이전 RenderCommandEncoder에서 이를 자원별로 관리를 하였지만, argument buffer에서는 자원을 개별적으로 관리할 수 없고, 이를 부분적으로 개별 검사하는 것은 성능 상의 이점을 상쇄시킵니다. 따라서, 접근할 때 RenderCommandEncoder에서 특정 메모리의 접근에 대한 지침을 미리 설정하여 이를 활용하게 됩니다.

 

Arrays of Arguments in the Metal Shading Language

 배열은 그래픽 및 연산 함수의 매개 변수로 사용될 수 있는데, 함수가 배열을 매개 변수로서 사용할 때, 첫 번째 리소스의 인덱스는 배열 매개 변수 자체의 기본 인덱스와 같습니다. 따라서, 배열의 각 후속 리소스에는 기본 인덱스 값에서 추가하여 후속 인덱스 값이 자동으로 할당됩니다.

fragment float4 exampleFragmentFunction(array<texture2d<float>, 10> textureParameters [[ texture(5) ]])

예를 들어, 다음 조각 함수인 exampleFragmentFunction에는 기본 인덱스 값이 5인 텍스처 배열인 textureParameters라는 매개 변수가 있습니다. textureParameters에는 [[ texture(5) ]] 속성 한정자가 있기 때문에 이 매개 변수를 설정하는 해당 Metal 프레임워크 메서드는 setFragmentTexture:atIndex:이며 여기서 인덱스 값은 5로 시작합니다. 따라서 배열 인덱스 0의 텍스처는 인덱스 번호 5로, 배열 인덱스 1의 텍스처는 인덱스 번호 6으로 설정된다. 배열의 마지막 텍스처인 배열 인덱스 9는 인덱스 번호 14로 설정됩니다.

 

Define Argument Buffers with Arrays

argument buffer 구조의 요소로 배열이 사용될 수 있으며, 이 경우 배열의 기본 인덱스 값 n을 가지는 argument buffer의 [[ id(n) ]] 속성 한정자는 함수 매개변수의 [[ texture(n) ]] 속성 한정자와 동일한 방식으로 동작합니다. 그러나 MTLenderCommandEncoder 개체에서 setFragmentTexture 메서드를 보다는 MTLArgumentEncoder 개체에서 setTexture를 통해 배열에서 argument buffer로 텍스처로 인코딩합니다. argument buffer의 구조는 다음과 같이 정의 가능합니다.

struct FragmentShaderArguments {
    array<texture2d<float>, AAPLNumTextureArguments> exampleTextures  [[ id(AAPLArgumentBufferIDExampleTextures)  ]];
    array<device float *,  AAPLNumBufferArguments>   exampleBuffers   [[ id(AAPLArgumentBufferIDExampleBuffers)   ]];
    array<uint32_t, AAPLNumBufferArguments>          exampleConstants [[ id(AAPLArgumentBufferIDExampleConstants) ]];
};

이는 아래와 같은 그림처럼 저장됩니다. 이때 texture의 기본 인덱스 값은 0, buffer는 100, constant는 200을 가지게 됩니다.

Encode Array Elements into an Argument Buffer

argument buffer에 대한 각 요소는 setTexture, setBuffer과 같은 함수로 정의 가능합니다.

for(uint32_t i = 0; i < AAPLNumTextureArguments; i++)
{
    argumentEncoder->setTexture(_texture[i], AAPLArgumentBufferIDExampleTextures + i);
    argumentEncoder->setBuffer(_dataBuffer[i], 0, AAPLArgumentBufferIDExampleBuffers + i);
    uint32_t *elementCountAddress = argumentEncoder->constantData(AAPLArgumentBufferIDExampleConstants + i);
    *elementCountAddress = (uint32_t)_dataBuffer[i].length / 4;
}

 

Access Array Elements in an Argument Buffer

함수 내에서, argument buffer로 인코딩된 배열의 요소들에 접근하는 것은 표준 배열의 요소들에 접근하는 것과 동일하다. 각 배열 요소는 [n] 첨자 구문을 사용하여 액세스되며, 여기서 n은 배열 내 요소의 인덱스입니다.

for(uint32_t textureToSample = 0; textureToSample < AAPLNumTextureArguments; textureToSample++)
{
    float4 textureValue = fragmentShaderArgs.exampleTextures[textureToSample].sample(textureSampler, in.texCoord);

    color += textureValue;
}

 

Combine Argument Buffers with Resource Heaps

지금까지 사용한 텍스처와 버퍼는 배열에 같이 존재하지만 자원에 대한 접근을 할때 개별적으로 검증하였습니다. 이를 해결하기 위해서 MTLHeap 개체에 자원을 할당하고, 이를 createHeap 메서드를 한번만 호출하여 힙의 전체 자원을 GPU에서 접근할 수 있도록 하였습니다. 이는 아래의 loadResources 메서드에서 구현됩니다. 또한, moveResourcesToHeap 메서드를 통해 영구적인 MTLTexture과 MTLBuffer 객체를 힙에 할당하고, MTLBlitCommandEncoder를 사용하여 자원 데이터를 임시 객체에서 영구 객체로 복사합니다.

void createHeap()
{
    MTLHeapDescriptor *heapDescriptor = MTL::HeapDescriptor::alloc()->init();
    heapDescriptor->setStorageMode(MTL::StorageMode::StorageModePrivate);
    heapDescriptor->setSize(0);

    // Build a descriptor for each texture and calculate the size required to store all textures in the heap
    for(uint32_t i = 0; i < AAPLNumTextureArguments; i++)
    {
        // Create a descriptor using the texture's properties
        MTL::TextureDescriptor* texture2DDescriptor(_texture[i]->pixelFormat(), _texture[i]->width(), _texture[i]->height(), _texture[i]->mipmapLevelCount ? true : false);

        // Determine the size required for the heap for the given descriptor
        MTL::SizeAndAlign sizeAndAlign = MTL::heapTextureSizeAndAlign(descriptor);
        
        // Align the size so that more resources will fit in the heap after this texture
        sizeAndAlign.size += (sizeAndAlign.size & (sizeAndAlign.align - 1)) + sizeAndAlign.align;

        // Accumulate the size required to store this texture in the heap
        heapDescriptor->setSize(heapDescriptor->Size() + sizeAndAlign.size);
    }

    // Calculate the size required to store all buffers in the heap
    for(uint32_t i = 0; i < AAPLNumBufferArguments; i++)
    {
        // Determine the size required for the heap for the given buffer size
        MTL::SizeAndAlign sizeAndAlign = MTL::heapBufferSizeAndAlign(_dataBuffer[i]->length(), MTL::ResourceStorageModePrivate);

        // Align the size so that more resources will fit in the heap after this buffer
        sizeAndAlign.size +=  (sizeAndAlign.size & (sizeAndAlign.align - 1)) + sizeAndAlign.align;

        // Accumulate the size required to store this buffer in the heap
        heapDescriptor->setSize(heapDescriptor->Size() + sizeAndAlign.size);
    }

    // Create a heap large enough to store all resources
    _heap = _device->newHeap(heapDescriptor);
}


void moveResourcesToHeap()
{
    // Create a command buffer and blit encoder to copy data from the existing resources to
    // the new resources created from the heap
    id <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
    commandBuffer.label = @"Heap Copy Command Buffer";

    id <MTLBlitCommandEncoder> blitEncoder = commandBuffer.blitCommandEncoder;
    blitEncoder.label = @"Heap Transfer Blit Encoder";

    // Create new textures from the heap and copy the contents of the existing textures to
    // the new textures
    for(uint32_t i = 0; i < AAPLNumTextureArguments; i++)
    {
        // Create a descriptor using the texture's properties
        MTLTextureDescriptor *descriptor = [AAPLRenderer newDescriptorFromTexture:_texture[i]
                                                                      storageMode:_heap.storageMode];

        // Create a texture from the heap
        id<MTLTexture> heapTexture = [_heap newTextureWithDescriptor:descriptor];

        heapTexture.label = _texture[i].label;

        [blitEncoder pushDebugGroup:[NSString stringWithFormat:@"%@ Blits", heapTexture.label]];

        // Blit every slice of every level from the existing texture to the new texture
        MTLRegion region = MTLRegionMake2D(0, 0, _texture[i].width, _texture[i].height);
        for(NSUInteger level = 0; level < _texture[i].mipmapLevelCount;  level++)
        {

            [blitEncoder pushDebugGroup:[NSString stringWithFormat:@"Level %lu Blit", level]];

            for(NSUInteger slice = 0; slice < _texture[i].arrayLength; slice++)
            {
                [blitEncoder copyFromTexture:_texture[i]
                                 sourceSlice:slice
                                 sourceLevel:level
                                sourceOrigin:region.origin
                                  sourceSize:region.size
                                   toTexture:heapTexture
                            destinationSlice:slice
                            destinationLevel:level
                           destinationOrigin:region.origin];
            }
            region.size.width /= 2;
            region.size.height /= 2;
            if(region.size.width == 0) region.size.width = 1;
            if(region.size.height == 0) region.size.height = 1;

            [blitEncoder popDebugGroup];
        }

        [blitEncoder popDebugGroup];

        // Replace the existing texture with the new texture
        _texture[i] = heapTexture;
    }

    // Create new buffers from the heap and copy the contents of existing buffers to the
    // new buffers
    for(uint32_t i = 0; i < AAPLNumBufferArguments; i++)
    {
        // Create a buffer from the heap
        id<MTLBuffer> heapBuffer = [_heap newBufferWithLength:_dataBuffer[i].length
                                                      options:MTLResourceStorageModePrivate];

        heapBuffer.label = _dataBuffer[i].label;

        // Blit contents of the original buffer to the new buffer
        [blitEncoder copyFromBuffer:_dataBuffer[i]
                       sourceOffset:0
                           toBuffer:heapBuffer
                  destinationOffset:0
                               size:heapBuffer.length];

        // Replace the existing buffer with the new buffer
        _dataBuffer[i] = heapBuffer;
    }

    [blitEncoder endEncoding];
    [commandBuffer commit];
}

 

Encode Data into Argument Buffers

먼저 초기화하는 동안 CPU에서 argument buffer은 다음과 같이 정의가 됩니다.

struct SourceTextureArguments {
    texture2d<float>    texture [[ id(AAPLArgumentBufferIDTexture) ]];
};

이는 source_texture 변수를 통해 접근되며, source_texture은 텍스처에 대한 참조를 지니는 구조체의 무한한 배열의 형태를 띕니다.

 

초기화 이후에, 각 프레임 마다 GPU로 InstanceArguments로 정의된 개별의 argument buffer에 데이터를 인코딩 합니다.

struct InstanceArguments {
    vector_float2    position;
    texture2d<float> left_texture;
    texture2d<float> right_texture;
};

이 argument buffer은 instance_params 변수를 통해 접근됩니다. instance_params는 computing 패스에 데이터가 채워진 다음 인스턴스 드로우 호출을 통해 렌더 패스에 액세스하는 구조의 배열입니다.

 

 

 

 

참조:

https://developer.apple.com/documentation/metal/buffers/managing_groups_of_resources_with_argument_buffers

https://developer.apple.com/documentation/metal/buffers/using_argument_buffers_with_resource_heaps

https://developer.apple.com/documentation/metal/buffers/encoding_argument_buffers_on_the_gpu

https://www.raywenderlich.com/books/metal-by-tutorials/v3.0/chapters/25-managing-resources

 

+ Recent posts