C++

instancing

3D object들을 렌더링 하는 애플리케이션의 최적화에 있어서 중요한 개념이다. Instancing이란 한 번의 render call을 사용하여 동일한 mesh data로 이루어진 많은 양의 object들을 생성하는 기술이다. Render call이란 CPU가 GPU에게 전달하는 명령(Command)으로, CPU와 GPU 사이의 통신은 device I/O이기 때문에 횟수가 적을수록 성능이 좋아진다.

example code

void Renderer::draw( MTK::View* pView )
{
    using simd::float4;
    using simd::float4x4;

    NS::AutoreleasePool* pPool = NS::AutoreleasePool::alloc()->init();

    _frame = (_frame + 1) % Renderer::kMaxFramesInFlight;
    MTL::Buffer* pInstanceDataBuffer = _pInstanceDataBuffer[ _frame ];

    MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer();
    dispatch_semaphore_wait( _semaphore, DISPATCH_TIME_FOREVER );
    Renderer* pRenderer = this;
    pCmd->addCompletedHandler( ^void( MTL::CommandBuffer* pCmd ){
        dispatch_semaphore_signal( pRenderer->_semaphore );
    });

    _angle += 0.01f;

    const float scl = 0.1f;
    shader_types::InstanceData* pInstanceData = reinterpret_cast< shader_types::InstanceData *>( pInstanceDataBuffer->contents() );
    for ( size_t i = 0; i < kNumInstances; ++i )
    {
        float iDivNumInstances = i / (float)kNumInstances;
        float xoff = (iDivNumInstances * 2.0f - 1.0f) + (1.f/kNumInstances);
        float yoff = sin( ( iDivNumInstances + _angle ) * 2.0f * M_PI);
        pInstanceData[ i ].instanceTransform = (float4x4){ (float4){ scl * sinf(_angle), scl * cosf(_angle), 0.f, 0.f },
                                                           (float4){ scl * cosf(_angle), scl * -sinf(_angle), 0.f, 0.f },
                                                           (float4){ 0.f, 0.f, scl, 0.f },
                                                           (float4){ xoff, yoff, 0.f, 1.f } };

        float r = iDivNumInstances;
        float g = 1.0f - r;
        float b = sinf( M_PI * 2.0f * iDivNumInstances );
        pInstanceData[ i ].instanceColor = (float4){ r, g, b, 1.0f };
    }
    pInstanceDataBuffer->didModifyRange( NS::Range::Make( 0, pInstanceDataBuffer->length() ) );


    MTL::RenderPassDescriptor* pRpd = pView->currentRenderPassDescriptor();
    MTL::RenderCommandEncoder* pEnc = pCmd->renderCommandEncoder( pRpd );

    pEnc->setRenderPipelineState( _pPSO );
    pEnc->setVertexBuffer( _pVertexDataBuffer, /* offset */ 0, /* index */ 0 );
    pEnc->setVertexBuffer( pInstanceDataBuffer, /* offset */ 0, /* index */ 1 );

    //
    // void drawIndexedPrimitives( PrimitiveType primitiveType, NS::UInteger indexCount, IndexType indexType,
    //                             const class Buffer* pIndexBuffer, NS::UInteger indexBufferOffset, NS::UInteger instanceCount );
    pEnc->drawIndexedPrimitives( MTL::PrimitiveType::PrimitiveTypeTriangle,
                                6, MTL::IndexType::IndexTypeUInt16,
                                _pIndexBuffer,
                                0,
                                kNumInstances );

    pEnc->endEncoding();
    pCmd->presentDrawable( pView->currentDrawable() );
    pCmd->commit();

    pPool->release();
}
  • kMaxFramesInFlight

    GPU에서 동시에 처리할수 있는 최대 프레임 수를 나타낸다. 일반적으로 애플리케이션은 각 프레임마다 GPU에 작업을 요청한다. 그리고 GPU는 이 작업을 병렬로 처리하여 가능한 한 많은 프레임을 한 번에 렌더링한다. 하지만 GPU에는 처리할수 있는 작업의 한계가 있으며, 이 한계를 초과하면 성능이 저하된다. 예를 들어 kMaxFramesInFlight = 3이라면 애플리케이션은 GPU에게 한 번에 3개의 프레임에 대해서만 작업을 요청할수 있다. 이후에는 이전에 요청한 작업이 완료될 때까지 기다린다.
    kMaxFramesInFlight의 값을 적절하게 사용하면 GPU의 성능을 최적화할수 있다. 값이 너무 작으면 GPU가 충분히 활용되지 않을 것이고, 너무 크다면 GPU에 부담이 된다.

  • MTL::Buffer* pInstancedDataBuffer = _pInstanceDataBuffe[_frame]

    GPU에 요청할 각 프레임에 필요한 데이터가 저장될 버퍼

  • MTL::CommandBuffer* pCmd = _pCommandQueue->commandBuffer GPU에 요청할 작업(command)이 저장될 버퍼. 명령 버퍼에 명령을 채우고 제출(commit)하면 해당 명령이 GPU에 전달되고 실행된다. 명령 버퍼는 비동기적으로 처리되며, 실행이 완료되기 전에 다른 명령 버퍼를 작성하고 제출할수 있다. 때문에 해당 명령들이 순서대로 처리되기를 원한다면 동기화를 해주어야 한다.

결과

reference

Categories:

Updated:

Comments