z Customizing Render Pass Setup :: C++, 그래픽 기술 블로그

Overview

렌더 패스는 텍스처 집합을 그리는 렌더링 명령의 집합입니다. 샘플 코드를 살펴가면서 이를 살펴볼 것인데, 먼저 첫 번째 패스는 텍스처에 이미지를 그려넣기 위하여 사용자 지정 Renderpass를 구성할 것이며 이는 일반적인 텍스처가 아닌 만들어지기에 offscreen render pass라 합니다. 두 번째는 MTKView 개체가 제공하는 RenderpassDescriptor를 사용하여 최종 이미지를 렌더링하고 표시합니다. 이때 텍스처는 offscreen render pass의 결과물을 사용합니다.
 offscreen render pass는 더 크고 복잡한 렌더러를 구성하기 위한 기본 구성 요소로 사용됩니다.

 

Create a Texture for the Offscreen Render Pass

MTKView 객체는 렌더링할 그리기 가능한 텍스처를 자동으로 생성하며, 이번에는 offscreen render pass를 수행하는 동안 그려낼 텍스처도 필요로 합니다.이러한 텍스처를 위하여 먼저 MTL::TextureDescriptor 개체를 만들고 속성을 구성합니다.

MTL::TextureDescriptor* texDescriptor = MTL::TextureDescriptor::alloc()->init();

texDescriptor->setTextureType( MTL::TextureType2D );
texDescriptor->setWidth( 512 );
texDescriptor->setHeight( 512 );
texDescriptor->setPixelFormat( MTL::PixelFormatRGBA8Unorm_sRGB );
texDescriptor->setUsage( MTL::TextureUsageRenderTarget | MTL::TextureUsageShaderRead );
MTL::Texture* _renderTargetTexture = m_pDevice->newTexture( texDescriptor );

...
pGBufferTextureDesc->release();

여기서 usage는 새로운 텍스처를 어떻게 사용할 지를 의미하며, 이는 텍스처에 이미지가 쓰여지고 나서 읽히는 과정을 거치기 때문에, RenderTarget과 ShaderRead 플래그를 사용하였습니다. 이는 용도에 맞는 텍스처만을 구성하여 성능을 향상시킵니다. 그리고 TextureDescriptor로 설정한 Texture를 실제로 _renderTargetTexture로 만들어내고 TextureDescriptor 객체는 메모리 해제시킵니다.

 

Create the Render Pipelines

렌더 파이프라인은 실행할 vertex function 및 fragment function을 포함한 drawing 명령을 어떻게 실행시키며 이들의 픽셀 포맷은 어떠한 지를 결정합니다. 아래의 코드는 offscreen render 파이프라인에 사용될 render pass를 만듭니다.

#define AAPLSTR( str ) reinterpret_cast< const NS::String* >(__builtin___CFStringMakeConstantString( str ))
NS::Error* pError = nullptr;

MTL::Library* pShaderLibrary = m_pDevice->newDefaultLibrary();
MTL::Function* pSimpleVertexFunction = pShaderLibrary->newFunction( AAPLSTR( "simpleVertexShader" ) );
MTL::Function* pSimpleFragmentFunction = pShaderLibrary->newFunction( AAPLSTR( "simpleFragmentShader" ) );

MTL::RenderPipelineDescriptor* pRenderPipelineDescriptor = MTL::RenderPipelineDescriptor::alloc()->init();
pRenderPipelineDescriptor->setVertexFunction( pSimpleVertexFunction );
pRenderPipelineDescriptor->setFragmentFunction( pSimpleFragmentFunction );
pRenderPipelineDescriptor->colorAttachments()->object(0)->setPixelFormat( _renderTargetTexture->pixelFormat() );
MTL::RenderPipelineState* _renderToTextureRenderPipeline = m_pDevice->newRenderPipelineState( pRenderPipelineDescriptor, &pError );

pRenderPipelineDescriptor->release();

 

이는 파이프 라인을 위해 vertex 및 fragment shader를 구성하고, 여기서 두 픽셀 형식이 일치하도록 _renderTargetTexture의 픽셀 형식을 사용합니다.

 

Set Up the Offscreen Render Pass Descriptor

offscreen 텍스처를 렌더링하기 위해서 새로운 render pass descriptor을 생성해야 합니다. 여기서는 단일 색상 텍스처로 렌더링되므로, colorAttachment[0].texture를 offscreen texture로 설정합니다.

MTL::RenderPassDescriptor* _renderToTextureRenderPassDescriptor = MTL::RenderPassDescriptor::alloc()->init();
_renderToTextureRenderPassDescriptor->colorAttachments()->object(0)->setTexture( m_pShadowMap );
_renderToTextureRenderPassDescriptor->colorAttachments()->object(0)->setLoadAction( MTL::LoadActionClear );
_renderToTextureRenderPassDescriptor->colorAttachments()->object(0)->setStoreAction( MTL::StoreActionStore );
_renderToTextureRenderPassDescriptor->colorAttachments()->object(0)->setClearColor( MTL::ClearColor::Make(1.0, 1.0, 1.0, 1.0) );

여기서 load action은 GPU가 drawing 명령을 수행하기 전에 렌더 패스가 시작될 떄 텍스처의 초기 내용을 결정하며, 마찬가지로 store action은 최종 이미지를 텍스처에 다시 덮어씌울지의 여부를 결정합니다. 위의 경우는 샘플로 렌더링 대상의 내용을 지우는 load action과 렌더링된 데이터를 텍스처에 다시 저장하는 store action을 취합니다. 이는 결과값을 텍스처에 저장하여 다음 render pass가 사용할 수 있도록 합니다.

 Metal은 다음과 같은 load 및 store 작업을 사용하여 GPU가 텍스처 데이터를 관리하는 방법을 최적화합니다. 큰 텍스처의 경우 많은 메모리를 소모하며 이를 전송하기 위해서는 큰 메모리 대역이 소모됩니다. 렌더 대상의 동작을 적절하게 설정하는 것으로 GPU가 텍스처에 접근할 때 사용하는 메모리 대역폭의 양을 줄일 수 있어, 성능과 배터리 수명을 향상시킬 수 있습니다.

 

 LoadAction과 StoreAction에 대해서 조금만 더 자세히 짚고 넘어가도록 하겠습니다. 위와 같이 texture에 대해서 속성을 정의할 수 있는데, 먼저 LoadAction에는 3가지 옵션이 존재합니다.

  • dontCare - 렌더 대상의 이전 데이터가 필요 없고 모든 픽셀에 대해서 작업을 수행할 때 사용될 수 있습니다. 이 작업은 비용이 들지 않으며, 픽셀 값은 초기에 미정의 상태로 시작합니다
  • clear - 이전의 값은 필요 없지만 픽셀 전체가 아닌 일부에 대해서만 작업을 수행할 때 사용될 수 있으며, 각 픽셀에 값을 새로 써넣는 비용이 발생합니다.
  • load - 이전의 렌더 대상의 내용이 필요하거나 일부의 픽셀만을 재렌더링이 필요한 경우 사용될 수 있습니다. 이는 각각의 픽셀에 대해 메모리에서 읽어오는 비용이 발생하기에, 위의 두 옵션보다는 속도가 느립니다. 

StoreAction에는 4가지 옵션이 존재합니다. 이때 보통 렌더 대상이 멀티샘플링된 텍스처로 존재하는데, 이를 멀티 샘플된 형태를 취할 것인지 아니면 따로 처리할 것인지(resolved data)

  • dontCare - 렌더 대상을 저장할 필요 없는 경우 사용될 수 있습니다. 이 작업은 비용이 들지 않으며, 픽셀 값은 초기에 미정의 상태로 시작합니다. 이는 보통 depth나 stencil에 대해서 수행합니다.
  • store - 렌더 대상을 저장할 필요가 있을 때 사용됩니다. 이는 저장하는 비용이 발생하는데, 렌더 대상이 다시 사용될 떄 수행됩니다. 
  • multisampleResolve - GPU가 멀티 샘플링된 데이터를 픽셀 당 하나의 샘플로 분해하고 분해된 텍스처에 데이터를 저장합니다. 나중에 버려지게 됩니다. 이는 렌더 패스 끝의 멀티 샘플링 내용은 확인해야 하지만, 이는 추후에 버려지기 떄문에 저장할 필요가 없는 경우에 사용됩니다.
  • storeAndmultisampleResolve - store과 multisampleResolve를 모두 수행합니다.

이떄 store을 수행하는 두 옵션의 경우 memoryless한 렌더 대상에 대해서는 수행 불가능 합니다. 여기서의 memorylesss는 GPU 내의 tile memory와 같이 일시적으로 생겨났다가 CPU로 되돌아가지 않고서 사라지는 메모리를 의미하며, ResourceStorageMode 중 하나로, 나머지인 public은 CPU와 GPU 둘 다에서 모두 접근 가능하고, private은 GPU에서만 접근 가능한 경우를 얘기합니다. 또한 이러한 store 옵션은 뒤로 늦출 수 있는데 unknown으로 설정하면 됩니다. 이는 다른 옵션을 조기에 저장하는 것으로 인한 잠재적인 비용을 피할 수 있지만, render pass 인코딩을 완료하기 전에 올바른 저장 작업을 지정하지 않으면 오류가 발생할 수 있습니다.

Render to the Offscreen Texture

render pass를 인코딩하기 전에 Metal이 어떻게 GPU 상의 명령을 스케쥴링하는 지를 이해할 필요가 있습니다. 프로그램이 명령 대기열에 명령 버퍼를 넘기면 Metal은 명령을 순차적으로 실행하는 것처럼 행동해야 합니다. 하지만 높은 성능과 GPU 활용도를 위해서는, Metal은 순차적으로 수행하는 것과 다른 결과를 내지 않는 한도 내에서 동시적으로 명령을 실행시킵니다. 이를 사전에 공지하기 위해, render pass가 자원에 적혀 이를 같이 읽어내어 Metal이 종속성을 확인하게 됩니다. 이번 코드의 경우는 첫번째 render pass가 종료되기 전까지 두 번째 render pass의 실행을 자동적으로 지연시킵니다. 따라서, CPU와 GPU 작업을 명시적으로 동기화하는 것과 같은 특별한 작업은 필요로 되지 않으며, 단순히 두 개의 패스를 순차적으로 인코딩하고, 이를 수행하도록 만듭니다. 이를 위해서 두 render pass를 하나의 command buffer로 순서에 맞게 인코딩하면됩니다. 먼저, 이전에 만든 offscreen render pass descriptor를 사용하여 render command encoder를 생성합니다.

MTL::RenderCommandEncoder* pRenderEncoder = pCommandBuffer->renderCommandEncoder(_renderToTextureRenderPassDescriptor);
pRenderEncoder->setLabel( AAPLSTR( "Offscreen Render Pass" ) );

...

pRenderEncoder->endEncoding()

이후의 작업은 일반적인 렌더링 작업과 동일합니다. metal-cpp 1을 참조하면 됩니다. 이후에 endEncoding()을 명시해주어 끝을 알리면 됩니다. 여러개의 render pass는 명령 버퍼로 순차적으로 인코딩되어야 하므로, 다음 렌더링 패스를 시작하기 전에 이전의 렌더 패스의 인코딩을 완료해야 합니다.

Render to the Drawable Texture

두 번째 render pass는 최종 이미지를 렌더링 해야 합니다. draw 가능한 렌더 파이프라인의 fragment 셰이더는 텍스처의 데이터를 샘플링하고 해당 샘플을 최종 색상으로 반환해야 합니다. 따라서 다음과 같은 fragmentShader 코드로 구성이 되며, 이로 두 번째 Render pass를 생성하고, drawing 명령을 인코딩하여 텍스처 사각형을 렌더링합니다.  

// Fragment shader that samples a texture and outputs the sampled color.
fragment float4 textureFragmentShader(TexturePipelineRasterizerData in      [[stage_in]],
                                      texture2d<float>              texture [[texture(AAPLTextureInputIndexColor)]])
{
    sampler simpleSampler;

    // Sample data from the texture.
    float4 colorSample = texture.sample(simpleSampler, in.texcoord);

    // Return the color sample as the final color.
    return colorSample;
}

이는 명령에 필요한 offscreen texture를 인자로서 지정하는 코드입니다.

 

// Set the offscreen texture as the source texture.
pRenderEncoder->setFragmentTexture(_renderTargetTexture, AAPLTextureInputIndexColor);

명령 버퍼를 보내면, Metal은 두 개의 render pass를 순차적으로 실행합니다. 이 경우 Metal은 첫 번째 렌더 패스가 offscreen texture에 그리고 두 번째 렌더 패스에서 읽는 것을 감지합니다. Metal이 다음과 같은 종속성을 발견하는 경우, GPU가 첫 번째 패스가 끝나고 나서야 두 번째 패스가 실행되도록 합니다.

+ Recent posts