원문 : https://developer.samsung.com/game/usage


주의 : 번역이 개판이므로 이상하면 원문을 참고하세요.


주의 : 허락받고 번역한 것이 아니므로 언제든 내려갈 수 있습니다.



Introduction


Vulkan 용례 가이드는 독자가 이미 API 와 친숙하지만 여러 종류의 Galaxy 디바이스에서 효율적으로 사용하는 방법에 대해서 알고자 한다고 가정합니다. 또한 일반적으로 모바일 디바이스들에서 찾아볼 수 있는 타일 기반 렌더링( Tile Based Rendering, TBR ) GPU 아키텍쳐의 기초에도 익숙하다고 가정합니다. Vulkan 초심자라면 API 개념을 코드 예제와 함께 소개하는 추천 SDK 들부터 시작하실 수 있습니다. GPU 아키텍쳐에 대해서는 여기에서 학습하실 수도 있습니다.


이 문서를 읽기 전에, Game Asset Optimization Recommendations 에도 익숙해지기를 권장합니다.


Understand your target



고성능 애플리케이션을 개발할 때, 여러분이 대상으로 하는 하드웨어 및 API 의 기능( capabilities )과 성능 특성( characteristics )을 이해하는 것이 필수적입니다.


Vulkan 의 경우, 다음을 포함합니다:


  • 최대 그래픽스 API 버전.
  • 기능들
    • 예를 들어 최대 텍스쳐 해상도.
  • 확장( extension )들.


다른 성능 집중 API 와 마찬가지로, Vulkan 에는 API 사용자가 고려하지 않으면 정의되지 않는 동작을 발생시킬 수 있는 상황들이 존재합니다. 크로노스( Khronos ) Vulkan Validation LayersGraphics API debugging tools 는 API 의 오용을 식별하는 데 도움을 줍니다. 또한 개발 초기 단계에 버그를 식별하기 위해 애플리케이션을 광범위한 디바이스, 칩셋( chipset ), GPU 아키텍쳐에 대해서 테스트하는 것이 매우 중요합니다.


  • GPU 아키텍쳐의 차이를 고려하십시오.
  • 런타임에 기능들과 확장들을 검사하십시오. 대안( fall-back )들을 구현하십시오.
  • 애플리케이션을 여러 디바이스에서 테스트하십시오.


Asset optimization


Game Asset Optimization Recommendations 을 확인하세요.


Shader Precision


SPIR-V 는 정밀도 한정자( precision qualifier )들을 지원합니다( OpDecorate RelaxedPrecision ). 정밀도 힌트( hint )는 ALU 연산들의 성능을 개선하기 위해서, 개발자가 감소된( reduced ) 정밀도를 사용할 위치를 컴파일러에게 알려줄 수 있도록 하며, 결국 GPU 의 전력 소비를 줄이게 됩니다.


변수에 대해 요청된 정밀도를 컴파일러가 받아들이는 것은 유효합니다. 예를 들어 개발자가 RelaxedPrecision 을 지정할 때는 32 비트 부동소수점 정밀도를 사용합니다. 컴파일러는 정밀도 변환을 위해 도입된 명령어가 완전한( full ) 정밀도로 계산을 실행하는 것보다 더 많은 부하를 발생시키는 경우에 이 방법을 사용하는 경향이 있습니다.


  • 컴파일러가 정밀도를 높이는( promote ) 것에 주의하십시오.  A 디바이스( promoted precision )에서 완벽하게 동작하는 셰이더가  B 디바이스( honors precision qualifier )에서 아티팩트( artifact )를 발생시킬 수 있습니다.
  • 감소된 정밀도로 인해 일부 디바이스들에서 렌더링 에러들이 감춰져 있는 상황에 주의하십시오.


Recommendations


  • 성능을 향상시키고 전력 소비를 줄이기 위해서 감소된 정밀도를 사용하십시오.
  • 컴파일러가 정밀도를 높이는 것에 주의하십시오. 셰이더 정밀도 아티팩트들을 초기에 발견하려면 여러 디바이스에서 테스트하십시오.
  • 감소된 정밀도로 인해 일부 디바이스들에서 렌더링 에러들이 감춰져 있는 상황에 주의하십시오.


Pipeline management


파이프라인을 드로( draw ) 시점에 생성하는 것은 성능 끊김( stutter )을 유발합니다. 파이프라인을 애플리케이션을 실행할 때 가능한 한 초기에 생성하는 것을 권장합니다. 파이프라인을 드로 시점 전에 생성하도록 렌더링 엔진을 재구성하는 것이 불가능하다면, 파이프라인을 한 번만 생성하고 그것들을 맵에 넣어서 연속되는 드로 호출들에서 해싱된( hashed ) 스테이트( state )들을 통해 검색할 수 있도록 하는 것을 권장합니다.


파이프라인 캐시는 새로운 파이프라인이 생성될 때 드라이버가 캐싱된 파이프라인들로부터 스테이트들을 재사용할 수 있도록 해 줍니다. 이는 셰이더 컴파일같은 반복되는 비싼 연산들 대신에 이미 구워진 스테이트를 재사용함으로써 성능을 상당히 개선할 수 있습니다. 단일 파이프라인 캐시를 사용해서 드라이버가 이전에 생성되었던 모든 스테이트들을 재사용할 수 있도록 보장하는 것을 권장합니다. 또한 파이프라인 캐시를 파일에 써서 다음 번에 애플리케이션이 실행될 때 재사용될 수 있도록 하기를 권장합니다.


파이프라인 상속( derivative )은 애플리케이션이 "자식" 파이프라인들을 유사한 "부모" 파이프라인으로부터의 증분 스테이트 변경( incremental state changes )으로서 표현할 수 있도록 해 줍니다; 일부 아키텍쳐들에서 이는 유사한 스테이트들 간의 전환 비용을 줄여줄 수 있습니다. 많은 모바일 GPU 들은 주로 파이프라인 캐시를 통해 성능을 개선합니다. 그래서 파이프라인 상속은 종종 휴대용( portable ) 모바일 애플리케이션에 대해서는 이점을 제공하지 않습니다.


Recommendations


  • 애플리케이션 실행 초기에 파이프라인들을 생성하십시오. 파이프라인 생성이 드로 시점에 발생하는 것을 피하십시오.
  • 모든 파이프라인 생성에 대해서 단일 파이프라인 캐시를 사용하십시오.
  • 파이프라인 캐시를 애플리케이션 실행되는 사이에 파일에 쓰십시오.
  • 파이프라인 상속을 피하십시오.


Descriptor set management


디스크립터 셋은 드로를 위한 리소스 바인딩을 정의합니다. 이상적으로 볼 때, 디스크립터 셋들은 빌드 시점에 생성되어야 하고 런타임 실행을 위해 캐싱되어야 합니다. 그것이 불가능하면, 디스크립터 셋들을 가능한 한 애플리케이션 실행 초기에 생성해야 합니다( 애플리케이션 로드나 레벨 로드 ).


Vulkan 의 대부분의 리소스들과 마찬가지로, API 사용자는 디스크립터 셋 갱신( update )을 동기화( synchronizing )할 책임이 있습니다. 그래야 대기중인 디바이스 읽기가 존재할 때 호스트에서 변경이 발생하지 않는 것이 보장됩니다. 리소스 동기화를 단순화하기 위해서 스왑 인덱스( swap index ) 별 디스크립터 풀( pool )을 사용하고 같은 바인딩을 사용하는 드로 사이에서 디스크립터 공유( sharing )를 이용( facilitate )하는 것을 권장합니다. 렌더링 엔진을 재구성하는 것이 불가능하고 드로 시점에 디스크립터 셋을 갱신할 필요가 있다면, 사용중인( in-flight ) 디스크립터를 변경하는 것을 피하기 위해, 디스크립터 셋과 버퍼를 관리하는 전략이 매우 주의깊게 고려되어야 합니다. 아래의 Buffer management 에서도 기술하듯이, 엔진이 최소 VkPhysicsDeviceLimits::maxUniformBufferRange 값에 대처하도록 하는 것이 중요합니다. 왜냐햐면 디스크립터들 사이에서 버퍼를 공유할 때 그 제한을 쉽게 초과할 수 있기 때문입니다.


유니폼( uniform ) 버퍼나 스토리지( storage ) 버퍼의 옵셋( offset )이 자주 변경된다면( 예를 들어 드로 때마다 ), VK_DESCRIPTOR_TYPE_*_BUFFER_DYNAMIC 을 사용해 버퍼를 동적으로 바인딩하고 vkCmdBindDescriptorSets() 가 호출될 때 옵셋을 pDynamicOffsets 로 설정하는 것을 권장합니다. 이는 그 옵셋들이 드로 실행 전에 디스크립터 셋을 수정하지 않고 변경될 수 있도록 해 줍니다.


Recommendations


  • 디스크립터 셋을 가능한 한 초기에 빌드하십시오( 이상적으로는 빌드 시점에 ).
  • 드로에 의해 참조되지 않는 리소스들을 바인딩하지 마십시오.
  • 버퍼 옵셋이 자주 변경될 필요가 있다면( 예를 들어 드로 사이에서 ), 디스크립터를 바인딩할 때 pDynamicOffset 를 사용하십시오.
  • 높은 빈도로 디스크립터 셋을 수정하지 마십시오. 동적 업데이트가 요구된다면, 읽기와 쓰기에 대한 동기화를 보장해야 합니다.


Buffer management


Game Asset Optimizations: Interleaved vertex attributes 에서 기술했듯이, position 애트리뷰트들은 다른 애트리뷰트들과는 다른 버퍼에 저장되어야 합니다. 이는 현대 모바일 GPU 들이 버텍스( vertex ) 셰이딩을 더 효율적으로 실행할 수 있게 해 줍니다. 서로 다른 빈도로 업데이트를 하지 않는 한에는, 다른 모든 애트리뷰트들은 단일 버퍼에 삽입되어야 합니다.


유니폼 버퍼들은 애플리케이션 실행시에 가능한 한 초기에 할당되어야 하며, 프레임 당 할당은 피해야 합니다( 이전의 할당을 재사용하는 것이 훨씬 빠릅니다 ). 실행중인 렌더러들이 현재 프레임의 API 호출에 의해 변경되는 버퍼 영역에 접근하는 것을 끝마치도록 보장하기 위해 펜스( fence )를 사용해야 합니다.


유니폼 버퍼는 서로 다른 빈도로 업데이트되는 데이터를 포함해서는 안 됩니다. 예를 들어, 드로가 정적인 데이터와 프레임 당 설정되는 데이터( 예를 들어 변환 행렬 )에 의존한다면, 두 개의 버퍼가 사용되어야 합니다. 다중 드로에 대해 공유되는 유니폼 데이터는 단일 버퍼에 저장되어야 합니다.


중복 전송( redundant transfer )들을 막기 위해서, 유니폼 데이터 메모리는 VK_MEMORY_PEROPERTY_HOST_VISIBLE_BIT 셋으로 할당되어야 합니다. 이 플래그는 vkMapMemory() 가 호스트로부터 효율적인 수정을 위해 사용될 수 있도록 합니다( 스테이징( staging) 버퍼에 비하면 복사 한 번이 더 적습니다 ). 잦은 Map/Unmap 호출은 피해야 합니다. 가능하다면 버퍼는 VkMemoryProeprtyFlagBits::VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 를 설정함으로써 영구적으로 매핑될 수 있습니다. 주의 : 영구적인 매핑은 API 캡쳐 도구들이 버퍼 수정을 트래킹하는 것을 더 어렵게 만듭니다. 이러한 이유로, 전용 GPU 메모리를 사용하는 플랫폼( platform )을 위해 영구적으로 매핑되지 않는 대안( non-persistently mapped fallback ) 경로를 구현하여 디버깅을 단순화하는 것을 권장합니다.


버퍼는 VkPhysicalDeviceLimits::min*Alignment 제한으로 정렬( align )되어야 합니다. 디스크립터 셋 바인딩과 버퍼 할당의 최대 크기는 다음과 같이 질의될 수 있습니다:


 Limit

 Desriptions

 VkPhysicalDeviceLimits::maxUniformBufferRange

 최대 유니폼 버퍼 메모리 범위.

 VkPhysicalDeviceLimits::maxDescriptorSetUniformBuffers

 디스크립터 셋에 바인딩될 수 있는 유니폼 버퍼의 최대 개수.

 VkPhysicalDeviceLimits::maxDescriptorSetUniformBuffersDynamic

 디스크립터 셋에 바인딩될 수 있는 동적 유니폼 버퍼의 최대 개수.

 VkPhysicalDeviceLimits::maxPerStageDescriptorUniformBuffers

 단일 셰이더 스테이지( stage )에서 접근할 수 있는 유니폼 버퍼의 최대 개수.


Descriptor set management 에서 기술했듯이, 디스크립터 셋 갱신을 최소화하기 위해서 동적 버퍼들과 동적 옵셋들을 사용해야 합니다.


셰이더 입력의 경우에는 항상 스토리지 버퍼보다는 유니폼 버퍼를 선호( prefer )해야 합니다.


Recommendations


  • 애트리뷰트 데이터를 두 개의 버퍼에 저장하십시오 - 하나는 버텍스 position 을 위해, 다른 하나는 다른 모든 애트리뷰트들을 위해.
  • 유니폼 버퍼들의 옵셋을 VkPhysicalDeviceLimits::minUniformBufferOffsetAlignment 로 정렬하십시오.
  • 통합( unified ) 메모리를 사용하는 디바이스 상에서는, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 를 사용하고, 플러싱( flush )들과 잦은 map/unmap 호출들에 대해 영구적인 버퍼 매핑을 선호하십시오.


Shader inputs


Vulkan 은 셰이더 상수들을 설정하기 위한 다양한 메커니즘을 제공합니다; 유니폼 버퍼, 특수화( specialization ) 상수, 푸시( push ) 상수.


특수화 상수들은 정적인 값들인데, 파이프라인 생성 시점에 설정될 수 있습니다( SPIR-V 바이너리가 컴파일될 때 ).


푸시 상수들은 버퍼 오브젝트가 아니라 Vulkan 커맨드를 통해서 셰이더 상수 입력 데이터를 설정하기 위한 메커니즘을 제공합니다. 푸시 상수 저장 공간은 제한적입니다. 푸시 상수 저장소를 위한 최대 바이트 수는 VkPhysicalDeviceLimits::maxPushConstantsSize 를 사용해 질의될 수 있습니다. 가능하다면 동적 유니폼 퍼버 옵셋과 같은 특수화 옵셋 메커니즘을 푸시 상수보다는 더 선호해야 합니다. 렌더링 엔진이 모든 드로들을 푸시 상수 데이터( 128 바이트 )를 위해 최소 maxPushConstantsSize 바이트 미만으로 사용하는 것을 보장할 수가 없다면, 유니폼 버퍼 기반 대안을 구현해 둘 필요가 있습니다. 이는 가장 작은 크기의 푸시 상수 바이트만을 지원하는 Vulkan 구현에서 렌더링 엔진이 작동하는 것을 보장합니다. 동적 유니폼 버퍼 옵셋을 사용하는 경우, vkCmdBindDescriptorSets() 를 호출해 드로 당 옵셋을 설정할 수 있습니다.


아래 목록들은 셰이더에서 상수값을 설정하는 것과 관련한 권장 사향들을 정리한 것입니다:


  • 정적 브랜칭( static branching ): SPIR-V 생성 시점에 실행 경로들을 지정할 수 있다면, 실행 경로를 위해 전용 SPIR-V 를 생성하십시오( 예를 들어 GLSL 의 #define 을 사용 ). 그렇지 않으면, SPIR-V 파이프라인 생성 시점에 특수화 상수를 사용하십시오.
  • 파이프라인 생성 시점 상수들: 파이프라인 생성 시점에 상수값을 설정할 수 있다면, 특수화 상수들을 사용하십시오.
  • 유니폼 버퍼에 대한 잦은 갱신들: vkCmdBindDescriptorSets() 호출 후에 유니폼 데이터 갱신이 여러 드로들에 적용되면, 동적 유니폼 버퍼를 사용하고 vkCmdBindDescriptorSet() 이 호출될 때 동적 옵셋들을 사용하십시오. 그래야 디스크립터 셋에 대한 변경을 피할 수 있습니다( 자세한 내용을 알고자 한다면 Descriptor set management 섹션을 참조하세요 ). 유니폼 데이터가 자주 갱신된다면, 예를 들어 드로마다 갱신된다면, 동적 유니폼 버퍼 옵셋 대신에 푸시 상수를 사용하는 것을 고려해 보십시오.


Recommendations


  • 생성 시점 정적 브랜칭을 위해 전용 SPIR-V 를 생성하십시오( 예를 들어 셰이더 코드에서의 #define ).
  • SPIR-V 컴파일 시점 정적 브랜칭을 위해 특수화 상수를 사용하십시오.
  • SPIR-V 컴파일 시점 상수 값들을 위해 특수화 상수를 사용하십시오.
  • 유니폼 데이터 갱신이 vkCmdBindDescriptorSets() 호출 후에 여러 드로에 적용된다면, 동적 옵셋 유니폼 퍼버를 사용하십시오.
  • 드로 호출마다 유니폼 데이터가 갱신되다면 푸시 상수를 고려해 보십시오.
  • 여러분의 엔진이 높은 빈도로 128 바이트보다 작은 데이터를 사용하고 있다면 푸시 상수들을 사용하십시오. 이것이 보장되지 않는다면, 동적 유니폼 버퍼 대안 경로를 구현하십시오.


Beware: No Implicit uniform type conversions


OpenGL ES 와는 다르게, Vulkan 은 묵시적으로 유니폼 타입 변환을 수행하지 않습니다. 개발자들은 버퍼 바인딩의 내용이 그것들이 바인딩하려고 하는 셰이더 유니폼 버퍼들과 일치하도록 보장할 책임이 있습니다.


Recommendations


  • 유니폼 버퍼 데이터 타입들이 셰이더 유니폼 변수들과 일치하도록 보장하십시오.


View frustum culling


드라이버와 GPU 가 처리하는 가장 싼 드로는 제출되지 않는 드로입니다. 드라이버와 GPU 의 중복 처리를 피하기 위해, 일반적인 최적화된 렌더링 엔진은 뷰 프러스텀 경계내에 존재하거나 겹치는 것들만을 그래픽스 API 에 제출합니다. 뷰 프러스텀 컬링은 보통 CPU 상에서 싸게 수행되며, 복잡한 3D 씬들을 렌더링할 때는 항상 고려되어야 합니다.


Recommendations


  • 드라이버와 GPU 의 중복 처리를 막기 위해 뷰 프러스텀 컬링을 항상 시도하십시오.


Command buffer binding


Vulkan 은 커맨드 버퍼들이 다중 스레드에서 빌드되는 것을 허용하도록 설계되었으며, 이는 이 비싼 작업들이 다중 CPU 코어들에서 수행될 수 있도록 해 줍니다. 또한, 세컨더리( secondary ) 커맨드 버퍼들이 생성될 수 있으며, 그것은 작업을 더 작은 청크( chunk )로 쪼개는 것을 쉽게 해 줍니다. 세컨더리 커맨드 버퍼는 생성 후에 프라이머리( primary ) 커맨드 버퍼에 커밋( commit )되어야 합니다. 하지만, 일부 구현에서는, GPU 가 렌더 패스 내의 모든 커맨드 버퍼가 단일한 연속된 메모리 블락에 속해 있는 것을 요구합니다 -- 이런 GPU 들을 위한 Vulkan 드라이버들은 커맨드 버퍼가 실행되기 전에 세컨더리 커맨드 버퍼를 프라이머리 커맨드 버퍼로 memcpy() 할 필요가 있습니다. 이러한 부하때문에, 세컨더리 커맨드 버퍼들보다는 프라이머리 커맨드 버퍼들을 선호할 것을 권장합니다. 렌더링을 다중 CPU 코어들 사이에서 다중 스레드로 처리하는 것을 선호한다면, 세컨더리 커맨드 버퍼들을 고려하기 보다는 프라이머리 커맨드 버퍼들을 병렬적으로 빌드하는 것을 권장합니다.


세컨더리 커맨드 버퍼를 사용하기로 결정했다면, 분할 계획( partitioning scheme )을 주의깊게 고려하십시오. 씬이 청크들로 빌드될 때, 여러분의 엔진이 드로 호출 제출 순서를 최적화하고 스테이트( state ) 변경을 최소화하도록 만드는 것이 더 어렵습니다. 세컨더리 커맨드 버퍼 빌드 경로가 구현되었다면, 런타임( run-time )에 그 경로가 요구되는지 혹은 프라이머리 커맨드 버퍼 빌드가 더 빠른지 결정해야 합니다.


Recommendations


  • 세컨더리 커맨드 버퍼들을 사용하기 보다는 프라이머리 커맨드 버퍼들에 서밋( submit )하는 것을 선호하십시오.
  • GPU 제한이 있는 디바이스들에서는 세컨더리 커맨드 버퍼를 사용하는 것을 피하십시오.
  • 세컨더리 커맨드 버퍼들을 고려하기 보다는 병렬적으로 프라이머리 커맨드 버퍼들을 빌드하는 것을 고려하십시오.
  • 세컨더리 커맨드 버퍼를 사용한다면, 분할 계획을 주의깊게 고려하십시오.


Instanced draws


모든 Vulkan CmdDraw* 함수들은 instanceCount 파라미터를 받습니다. 인스턴스 당 데이터는 VkVertexInputBindingDescription::inputRate 를 VK_VERTEX_INPUT_RATE_INSTANCE 로 설정된 버퍼를 바인딩함으로써 제공됩니다.


Recommendations


  • 항상 단일 호출 및 인스턴스 당 데이터를 사용하여 인스턴스화된 오브젝트를 렌더링하십시오.


Clearing framebuffer attachments


Vulkan 에는 프레임버퍼 어태치먼트를 클리어하기 위한 세 가지 메커니즘이 존재합니다 :


  • 렌더 패스 로드 연산( VK_ATTACHMENT_LOAD_OP_CLEAR ).
  • vkCmdClearAttachments() 함수.
  • vkCmdClearColorImage() 함수 및 vkCmdClearDepthStencilImage() 함수.


연산들이 효율적으로 수행되도록 하기 위해서는, 주어진 시나리오를 위해 사용되는 메커니즘을 올바르게 선택하는 것이 중요합니다:


Recommendations


  • 렌더 패스의 시작부에서 어태치먼트들을 클리어할 때는, VK_ATTACHMENT_LOAD_OP_CLEAR 를 사용하시오.
  • 서브 패스 내에서 어태치먼트들을 클리어할 때는 VkCmdClearAttachements() 를 사용하십시오.
  • vkCmdClearColorImage() 와 vkClearDepthStencilImage() 는 렌더 패스 외부에서 클리어하는 데 사용될 수 있습니다. 이 함수들은 타일 기반 GPU 아키텍쳐들에서 가장 비효율적인 메커니즘들입니다.


Efficient render pass upscaling


고화질( high-fidelity ) 3D 게임들에서 일반적인 병목은 프래그먼트( fragement ) 셰이딩 실행 시간입니다. 프레임당 프래그먼트 셰이딩 비용을 줄이기 위해, 게임 씬이 줄어든 해상도로 렌더링되고 나서 디바이스의 원래 해상도로 사용자 인터페이스를 렌더링하기 전에 크게 스케일링될 수 있습니다.


  • 렌더 패스 A( 줄어든 해상도 )
    • 게임 씬 렌더링.
  • 렌더 패스 B( 원래 해상도 )
    • 게임 씬 이미지를 업스케일링.
    • UI 를 렌더링.


업스케일링은 두 가지 방식으로 수행될 수 있습니다:


  1. vkCmdBlitImage() 호출.
    1. 소스 이미지의 영역들을 대상 이미지로 복사하는데, 잠재적으로 포맷 변환, 임의의 스케일링 및 필터링을 수행합니다.
    2. 구현에 따라, 이 연산은 전용 블리팅( blitting ) 하드웨어에 의해 GPU 나 GPU 에서 수행될 수도 있습니다.
  2. 전체 화면 사각형 렌더링.
    1. 렌더 패스 B 를 이미지를 샘플링하는 전체 화면 드로 호출로 시작합니다.


vkCmdBlitImage() 가 최적의 선택인 것으로 보이겠지만, 모바일 GPU 들에서는 전체 화면 사각형을 렌더링하는 것보다 덜 효율적인 경향이 있습니다. 그 이유는 그것이 하나의 VkImage 로부터 다른 VkImage 로의 명시적인 복사를 요구하기 때문입니다. 블리트 연산을 위해서 GPU 를 사용하는 구현들 상에서는, 이것이 렌더 패스 A 와 B 사이에서 추가적으로 구현되어야 할 것입니다 -- 낭비될 수도 있는 메모리 대역폭과 GPU 사이클을 소비하게 됩니다. 반면에, 전체 화면 사각형 접근법은 단지 한 VkImage 에 대한 레이아웃 트랜지션( transition )만을 요구합니다. 트랜지션의 유형에 따라 이것은 "무료" 로 수행될 수 있도록 구현될 수 있습니다.


Recommendations


  • 프래그먼트 셰이더 제약이 있을 때 게임 씬 업스케일링만을 고려하십시오. 업스케일링의 대역폭 비용에 주의하십시오.
  • vkCmdBlitImage() 보다는 전체 화면 사각형 업스케일링을 선호하십시오.


Subpasses


모바일 디바이스들은 제한된 메모리 대역폭을 가집니다. 게다가 메모리 대역폭 데이터 전송( transfer )들은 이를 위해 전력을 소비하게 되므로, 가능한 한 적게 사용하는 것이 최상입니다.


3D 그래픽스 렌더링에서, 프레임버퍼는 하나 이상의 어태치먼트들을 필요로 할 것이고 일부 어태치먼트 데이터들은 보존( preserve )될 필요가 있습니다 -- 다른 모든 어태치먼트 데이터는 임시적( temporary )입니다. 예를 들어 컬러 버퍼는 렌더링된 이미지를 위해 요구될 것이고, 뎁스 버퍼는 프리미티브들이 의도된 순서로 렌더링되는 것을 보장하기 위해서 요구될 것입니다. 이 시나리오에서, 뎁스 데이터는 보존될 필요가 없습니다 .그러므로 GPU 메모리에서 시스템 메모리로 그것을 쓰는 것은 대역폭을 낭비하게 됩니다. 또한 N - 1 프레임에 있는 컬러 및 뎁스 버퍼의 내용은 N 프레임에서 요구되지 않습니다. 이 데이터를 업로드하는 것은 메모리 대역폭을 중복으로 사용하기 때문에, 드라이버에게 그런 연산들이 요구되지 않음을 전달하고 싶을 것입니다.


Attachment load op


각 어태치먼트들의 VkAttachmentLoadOp 속성은 어태치먼트들이 서브패스 시작할 때 초기화되어야 하는 방법을 지정합니다.


  • VK_ATTACHMENT_LOAD_OP_DONT_CARE
    • 이는 가낭 효율적인 선택입니다. 어태치먼트가 초기화될 필요가 없을 때 사용되어야 합니다. 예를 들어 모든 픽셀이 스카이 박스나 어떤 드로에 의해서 칠해지는 경우입니다.
  • VK_ATTACHMENT_LOAD_OP_CLEAR
    • 이 연산은 타일러( tiler ) 매우 효율적입니다. Clearing framebuffer attachements 섹션에서 기술했듯이, 이것은 렌더 패스를 시작할 때 어태치먼트를 클리어하는 가장 효율적인 방법입니다.
  • VK_ATTACHMENT_LOAD_OP_LOAD
    • 이것은 가장 비용이 드는 연산입니다. 타일 기반 렌더러에서, 타일( on-tile ) 메모리는 어태치먼트의 보존된 데이터를 시스템 메모리로부터 읽어들임으로써 초기화됩니다.


Attachment store op


각 어태치먼트들의 VkAttachmentStoreOp 속성은 어태치먼트가 서브패스의 끝에서 저장되는 방법을 지정합니다.


  • VK_ATTACHMENT_STORE_OP_DONT_CARE
    • 이는 가장 효율적인 선택입니다. 어태치먼트가 보존될 필요가 없을 때 사용되어야 합니다. 예를 들어 뎁스와 스텐실 데이터가 이 패스를 렌더링하기 위해서만 사용되는 경우입니다.
  • VK_ATTACHMENT_STORE_OP_STORE
    • 이는 가장 비용이 드는 연산입니다. 타일 기반 렌더러에서, 타일 메모리는 어태치먼트 이미지를 시스템 메모리로 저장함으로써 보존됩니다.


어태치먼트 이미지가 결코 로드되거나 저장되지 않는다면, VkImage 는 VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT 를 사용해 생성되어야 하며, VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 속성을 사용해 메모리에 바운딩되어야 합니다. 이는 드라이버가 그 이미지를 임시적( transient )으로 처리할 수 있도록 해 줍니다. 그런 메모리는 나중에 할당될 수 있습니다.


Advanced subpass usage


디퍼드 라이팅( deferred lighting ) 시스템을 위한 같은 멀티 패스 렌더링같은 발전된 서브패스 주제들은 Introduction to Vulkan Render Passes 기사에서 다루고 있습니다.


Recommendations


  • VK_ATTACHMENT_LOAD_OP_DONT_CARE 를 기본으로 두고, 어태치먼트가 클리어되어야 할 필요가 있다면 VK_ATTACHMENT_LOAD_OP_CLEAR 를, 이전의 어태치먼트 데이터가 보존될 필요가 있다면 VK_ATTACHMENT_LOAD_OP_LOAD 를 사용하십시오.
  • 모든 어태치먼트들이 보존될 필요가 없다면 VK_ATTACHMENT_STORE_OP_DONT_CARE 를 사용하십시오.
  • 어태치먼트 이미지가 결코 로드되거나 저장되지 않는다면, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT 를 사용해 할당하고 VK_MEMORY_LAZILY_ALLOCATED_BIT 속성을 가지고 할당된 메모리를 사용하십시오.


Synchronization


Vulkan 에서 동기화는 복잡하며, 이를 지원하는 게임에들에서 일반적인 버그들의 온상입니다. 다음 챕터들은 동기화 권장 사항들에 대해서 설명합니다.


Terminology


동기화 개체( primitive )들에 대해서 이해하려고 하기 전에, 이 챕터들을 통해서 사용되는 Vulkan 의 용어들에 대해서 이해하는 것이 중요합니다:


  • 동기화 스코우프( Synchronization scope ): 연산들의 집합입니다. 대부분의 동기화 커맨드들에서, 소스( source ) 파이프라인 스테이지 마스크와 대상( destination ) 파이프라인 스테이지 마스크들에 의해 동기화 스코우프들이 정의됩니다.
  • 실행 종속성( Execution dependency ): 대부분의 동기화 커맨드들은 실행 종속성을 정의합니다. 첫 번째 동기화 스코우프에서 정의된 모든 연산들은 두 번째 동기화 스코우프 내에서 정의된 모든 연산들보다 먼저 실행되어야 합니다.
  • ( 역주 : 접근 ) 가능성 연산( Availability operation ): 이는 다음 메모리 접근이 발생하기 전에 완료되어야 하는 메모리 연산들을 지정합니다. 예를 들어, 이미지 메모리 배리어( barrier )는 VkImage 의 컬러 어태치먼트 쓰기가 다음 접근들 전에 완료되어야 함을 지정합니다.
  • ( 역주 : 읽기 ) 가시성 연산( Visibility operation ): 이는 주어진 가능성 연산이 발생하기 전에 한 번만 발생해야 하는 메모리 연산을 지정합니다. 예를 들어, 이미지 메모리 배리어는 지정된 가능성 연산이 완료되기 전에 셰이더가 어태치먼트를 한 번 읽어들이게 됨을 지정할 수 있습니다.
  • 리소스 트랜지션( Resource transition ): 어떤 VkImage 리소스들은 가능성 연산과 가시성 연산 사이에서 한 레이아웃에서 다른 레이아웃으로 전이( transition )될 필요가 있습니다. 이미지 메모리 배리어의 경우에, 리소스 트랜지션은 oldLayout 과 newLayout 으로 정의됩니다.
  • 메모리 종속성( Memory dependency ): 이는 가시성 연산들의 집합 이전에 완료되어야만 하는 가능성 연산들의 집합을 정의합니다. 메모리 종속성은 리소스 트랜지션을 정의하기도 합니다.


Semaphores


세마포어는 다중 큐 사이에서 리소스 접근을 제어하는 데 사용될 수 있습니다. 가장 일반적인 세마포어의 용례는 그래픽스 큐와 프리젠테이션 큐를 동기화하는 것입니다.


예제: 그래픽스 큐 <--> 프리젠테이션 큐 동기화


코드 블락 1 : Per-Frame



더 많은 정보를 원한다면 이 페이지를 참고하세요.


Fences


펜스는 큐에서 호스트와 통신하기 위해서 사용될 수 있습니다. 가장 일반적인 용례는 그래픽스 렌더링이 완료되었을 때를 시그널( signal )링하는 것입니다. 그러므로 그 리소스들이 다음 프레임을 위해 재사용될 수 있습니다. 최적의 성능을 위해서는 프리젠트할 이미지들과 리소스들의 개수들 사이를 1:1 로 매핑되도록 하는 것을 추천합니다.


프레임 루프에서 vkWaitForFences() 호출을 피할 것을 추천합니다. 왜냐하면 이것은 실행을 중지( stall )시키고 성능을 감소시키는 결과를 낳기 때문입니다( 우리가 프로우파일링한 게임에서 1-3 fps 정도가 떨어지는 것을 발견했습니다 ). 대신에 vkGetFenceStatus() 를 호출해서 프리젠트할 수 있는 이미지가 이용가능한지를 확인하는 것을 권장합니다.


Example: graphics queue --> host synchronization


코드 블록 2 : Initialization



코드 블록 3 : Per-frame rendering loop content


Recommendations


  • 그래픽스 큐를 호스트와 동기화하려면 항상 펜스를 사용하십시오.
  • 원형( circular ) 버퍼에 있는 동적 리소스들에 대한 참조를 유지하고 각 리소스들이 재사용될 수 있는지 확인하기 위해서 펜스를 사용하십시오.
  • vkWaitForFences() 를 프레임 루프에서 호출하는 것을 지양하고, 대신에 vkGetFenceStatus() 를 사용하십시오.


Barriers


Vulkan 배리어는 API 사용자가 같은 큐 혹은 같은 서브패스 내의 커맨드들 사이에 종속성을 삽입할 수 있도록 해 줍니다. 실행 종속성은 파이프라인 스테이지 동기화 스코우프에 의해 정의됩니다. 실행 종속성과 함게 vkCmdPipelineBarrier() 호출은 세 가지 유형의 메모리 접근 배리어 -- 전역( global ), 버퍼( buffer ), 이미지( image ) -- 들을 받아들일 수 있습니다. 메모리 배리어들은 API 사용자에게 첫 번째 동기화 스코우프가 완료되는 동안( 혹은 전에 ) 두 번째 동기화 스코우프 내의 읽기 연산전에 쓰기 연산을 보장해 줍니다. 이름이 제안하고 있듯이, 전역 메모리 배리어들은 특정 리소스를 지정하는 것이 아니라 모든 메모리 접근들을 동기화하는 데 사용됩니다. 더 자세한 동기화를 위해서, 버퍼 메모리 배리어와 이미지 메모리 배리어를 사용할 수 있습니다:


 Host

 Transfer

 Compute

 Graphics

 TOP_OF_PIPE_BIT

 HOST_BIT

 

 

 

 

 TRANSFER_BIT

 

 COMPUTE_SHADER_BIT

 DRAW_INDIRECT_BIT

 DRAW_INDIRECT_BIT

 

 VERTEX_INPUT_BIT

 VERTEX_SHADER_BIT

 TESSELLATION_CONTROL_SHADER_BIT

 TESSELLATION_EVAULATION_SHADER_BIT

 GEOMETRY_SHADER_BIT

 EARLY_FRAGEMENT_TESTS_BIT

 FRAGMENT_SHADER_BIT

 LATE_FRAGMENT_TESTS_BIT

 COLOR_ATTACHMENT_OUTPUT_BIT

 BOTTOM_OF_PIPE_BIT


위의 표의 네 열은 Vulkan 파이프라인들입니다. TOP_OF_PIPE_BIT 와 BOTTOM_OF_PIPE_BIT 는 모든 파이프라인에 대해 공통입니다. 상대적으로, 그것들은 실행을 시작하고 실행을 종료하는 첫 번째 커맨드를 위한 일반 파이프라인 스테이지들을 표시합니다.


파이프라인 버블( bubble )을 막기 위해서는, API 사용자가 배리어들의 실행 종속성을 매우 주의깊게 고려하는 것이 중요합니다. 배리어 호출을 타일 기반 GPU 아키텍쳐의 서브패스 내에서 하고 있을 때는 특히 중요합니다. 예를 들어 첫 번째 동기화 스코우프에서 BOTTOM_OF_PIPE_BIT 이고 두 번째 동기화 스코우프에서 TOP_OF_PIPE_BIT 인 서브패스 내에서 배리어가 설정되었다면, 배리어 앞에 있는 모든 GPU 커맨드들이 플러싱( flush )되고 배리어 뒤에 있는 커맨드들이 실행을 시작하기 전에 플러싱이 종료되는 것을 위해 대기해야만 할 것입니다.


이러한 버블을 피하려면, 배리어의 첫 번째 동기화 스코우프가 파이프라인의 가능한 한 앞쪽으로 설정되고 두 번째 동기화 스코우프가 가능한 한 가장 마지막의 파이프라인 스테이지로 지정되어야 합니다. scrStageMask 를 TOP_OF_PIPE_BIT 로 설정하는 것은 결코 배리어를 블락킹하지 않으며, 이는 첫 번째 동기화 스코우프가 비어 있는 것처럼 동작합니다. 이와 유사하게 dstStageMask 를 BOTTOM_OF_PIPE_BIT 로 설정하는 것은 두 번째 동기화 스코우프가 비어 있는 것을 의미하게 됩니다. 이러한 동작이 기대되는 경우들이 존재합니다 -- 다른 동기화( 예를 들어 세마포어 )가 이미 요청된 종속성들을 강제하고 있는 경우 -- 하지만 이런 옵션들은 주의깊게 사용되어야 합니다.


자세한 버퍼 동기화가 요구되지 않는 한은 전역 메모리 배리어들이 버퍼 메모리 배리어보다 선호됩니다. -- 예를 들어 버퍼의 특정 영역을 쓰는 동기화입니다. 같은 동기화 스코우프에 의존하는 이미지 메모리 배리어와 버퍼 메모리 배리어는 단일 vkCmdPipelineBarrier() 호출 내부에 배칭( batch )해야 합니다.


Recommendations


  • 소스 종속성을 파이프라인 내에서 가능한 한 앞쪽으로 설정하십시오. 그리고 대상 종속성을 가능한 한 늦게 설정하십시오.
  • 타일 기반 아키텍쳐 상에서 서브패스 내에서 설정되는 배리어들에 의해 만들어질 수 있는 파이프라인 버블에 조심하십시오.
  • 버퍼 메모리 배리어보다는 전역 메모리 배리어를 선호하십시오.
  • 이미지 메모리 배리어와 버퍼 메모리 배리어가 같은 스코우프를 사용하고 있다면 그것들은 단일 vkCmdPipelineBarrier() 호출 내에 배칭하십시오.
  • 용례에 기반한 권장 사항에 대해서 알고자 한다면 크로노스 그룹의 동기화 예제를 찾아 보십시오.


Events


이벤트들은 자세한 동기화 메커니즘을 제공합니다:


  • 호스트에서 그래픽스 큐로의 동기화.
  • 렌더 패스 내부에서의 커맨드 종속성들.


vkCmdWaitEvents() 는 vkCmdPipelineBarrier() 와 유사한 인자들을 취합니다. 추가적인 인자들은 eventCount 와 pEvents 입니다. pEvents 와 srcStageMask 에 의해 정의되는 동기화 스코우프는 vkCmdWaitEvents() 와 dstStageMask 뒤쪽의 커맨드들이 실행되기 전에 종료되어야 합니다.


호스트에서 그래픽스 큐로의 이벤트 동기화는 커맨드 버퍼에 쓰여진 리소스들에 대한 종속성이 있는 커맨드들 뒤쪽에서 리소스 쓰기가 발생할 필요가 있을 때 유용합니다. 예를 들어, 사용자 입력과 GPU 실행 사이의 지연을 줄이기 위해서, VR 합성기( compositor )는 타임 워프 합성이 수행되기 전에 유니폼 버퍼에 씬이 렌더링되므로 머리 방향 델타( delta )를 표현하는 행렬을 쓸 수 있습니다. vkCmdWaitEvents() 는 이 이벤트가 시그널링될 때까지 실행을 블로킹합니다. 하지만 너무 많은 시간이 걸리는 GPU 제출( submission )은 시스템에 의해 무시( kill )될 수 있다는 점을 기억하십시오. 그래서 이러한 접근법을 사용하려면 극도의 주의가 필요합니다.


Recommendations


  • 이벤트보다는 배리어를 사용하세요.


Wait idle


아이들 대기는 동기화에서 매우 무거운 형식입니다. vkQueueWaitIdle() 은 모든 큐 연산들이 종료될 때까지 대기하며 기능적으로는 펜스를 기다리는 것과 동일합니다. vkDeviceWaitIdle() 은 모든 디바이스 연산이 종료될 때까지 대기합니다. 아이들 대기 함수들은 중복이 없음이 보장하며, and should only be used for rendering engine tear down.


Recommendations


  • Only use WaitIdle functions for rendering engine tear down.


SwapChains


스왑체인을 생성할 때, VK_PRESENT_MODE_FIFO_KRH 프리젠테이션 모드를 사용하고 minImageCount 를 3 으로 설정하는 것을 권장합니다.


VK_PRESENT_MODE_MAILBOX_KHR 프리젠테이션 모드는 잠재적으로 프레임율을 안정화시켜 줍니다; 하지만 이는 렌더링된 전체 프레임을 날림( throwing )으로써 수행됩니다. 이는 GPU 가 필요 이상으로 무거워지도록 만듭니다( 그리고 결국 더 많은 전력을 소비합니다 ). FIFO 를 사용하여 프로우파일링하고 최적화할 것을 강력히 권장합니다. 그리고 MAILBOX 는 최소 지연이 절대적으로 요구될 때만 사용하십시오.


스왑체인 이미지의 개수를 결정하는 데 대한 주요 고려사항은 메모리 사용과 부드러움 사이의 균형입니다. 안드로이드는 2 개의 이미지만을 가진 스왑체인을 생성하는 것을 지원합니다. 이는 요구되는 메모리를 줄여주지만, 프레임이 v-sync 를 위한 시간 내에 렌더링되지 못한다면 렌더링 파이프라인에서 버블을 만듭니다. 3 개 보다 많은 스왑체인 이미지를 요청할 수 있기는 하지만, 이는 이러한 이점과 추가적인 메모리 소비량 사이에서 주의깊게 고민해 봐야 합니다.


그래픽스 큐와 프리젠테이션 큐를 동기화하기 위해서 세마포어를 사용해야 합니다. 펜스는 그래픽스 큐와 호스트를 동기화하기 위해 사용되어야 합니다. 더 많은 정보를 원한다면 다음 예제들을 참고하십시오:


  • 예제 : 위의 코드블락 1, 2.


Recommendations


  • VK_PRESENT_MODE_FIFO_KHR 프리젠테이션 모드를 사용하십시오.
  • 스왑체인의 minImageCount 를 3 으로 설정하십시오.
  • 그래픽스 큐와 프리젠테이션 큐를 동기화하기 위해 세마포어를 사용하십시오.
  • 그래픽스 큐와 호스트를 동기화하기 위해 펜스를 사용하십시오.


Minimizing overdraw


프래그먼트들은 프리미티브가 그래픽스 큐에 제출된 순서대로 래스터화( rasterize )됩니다. 여러 개의 프리미티브들이 겹쳐 있으면, 결과 프래그먼트들이 최종 이미지의 다른 것들에 의해 가려지는 상황이라고 할지라도 모든 프래그먼트들이 렌더링됩니다. 이후의 프래그먼트들에 의해 겹쳐지게 되는 값을 가진 프래그먼트들을 렌더링하는 것은 오우버드로( overdraw )라 알려져 있습니다.


일부 아키텍쳐들만 나중에 무시될 프래그먼트에 대한 셰이딩 부하를 줄이는 최적화를 포함하고 있습니다. 그럼에도 불구하고, 최상의 휴대용 최적화를 획득하기 위해서는, 초기 뎁스/스텐실 테스트를 사용하고 불투명 드로 호출들을 깊이 순서대로( 일반적으로 앞에서 뒤로, 뎁스 테스트 모드에 따라 다름 ) 제출하는 것을 권장합니다. 이는 GPU 가 셰이딩을 위한 가시적인 우선순위를 가지고 있는 프리미티브들이 어떤 것인지 판단할 수 있도록 해 줍니다. 투명 프리미티브들은 불투명 프리미티브들 후에 블렌딩 동작을 유지할 수 있는 순서로 렌더링되어야 합니다.


Recommendations


  • 불투명 드로를 앞에서 뒤로 정렬하고 나서 투명 프리미티브들을 렌더링하십시오. 순서 결정은 오우버드로 감소를 위해 완벽할 필요는 없습니다. CPU 정렬 부하와 오우버드로 감소 사이의 좋은 균형을 제공하는 알고리즘을 찾으십시오.


Avoid redundant API calls


렌더링없이 반복적으로 스테이트를 설정하는 것과 같은 Vulkan 중복 호출들은 병목을 유발하지 않습니다만, 여전히 비용이 듭니다.


Recommendations


  • API 를 중복호출하는 것을 피하십시오.


Robust Buffer Access


Vulkan 드라이버들은 API 가 그것을 호출하는 애플리케이션 내에서 올바르게 사용되고 있다고 가정합니다. 이러한 가정은 드라이버가 런타임 유효성 검사를 피할 수 있도록 해 줍니다. 애플리케이션들은 robustBufferAccess 기능을 활성화함으로써 더 강력한 신뢰성 보장을 요구할 수 있습니다. 신뢰성있는 버퍼 접근의 주요 목적은 버퍼 경계를 넘어서는 것과 관련한 검사들을 제공하는 것입니다. Vulkan 명세는 robustBufferAcess 가 물리 디바이스 상에서 이용가능한 기능임을 보장하지만, 그것을 활성화하는 것은 일부 아키텍쳐에서 심각한 성능 페널티를 발생시킬 수 있습니다.


Recommendations


  • robustBufferAccess 를 개발 단계에서 사용하여 버그를 잡으십시오. 릴리스 빌드에서는 비활성화하십시오.


Validation Layers


API 오용을 식별하기 위해서 개발 단계 동안 주기적으로 크로노스 그룹의 Vulkan 유효성 레이어들을 사용해야 합니다. 최상의 결과를 위해서, 최신 버전의 유효성 레이어들을 사용하는 것을 권장합니다. 유효성 레이어들의 소스 코드는 깃허브에서 호스팅되고 있으며 그것을 빌드하는 것은 직관적입니다. 각 레이어의 릴리스들에는 sdk-* 라는 태그가 붙어 있습니다.


이용가능한 레이어들의 하위 집합에 의존해서는 안 됩니다. 모든 잠재적인 문제들을 발견하기 위해서는 크로노스에서 제공하는 모든 레이어들을 주기적으로 사용할 것을 권장합니다.


렌더링 엔진은 서로 다른 디바이스 상에서 서로 다른 Vulkan 기능들( 예를 들어 텍스쳐 포맷 )을 사용할 것입니다. 그래서 여러 Galaxy 디바이스를 사용해 유효성 레이어들을 실행하는 것을 권장합니다. 게임을 릴리스하기 전에 모든 유효성 레이어 메시지들을 해결해야 합니다.


에러코드는 메시지에 특정되지 않습니다.그것들은 문제의 유형을 분류하며 하나 이상의 메시지들에 이해 재사용될 것입니다. 레이어 출력을 파싱하는 것을 권장하지는 않습니다. 왜냐하면 에러 코드와 메시지 텍스트는 유효성 레이어 릴리스마다 다양하기 때문입니다.


레이어에 의해 보고된 메시지의 의미가 명확하지 않다면, 원인을 더 잘 이해하기 위해서 유효성 소스 코드를 검색해 볼 것을 권장합니다.


잘못된 긍정( positive )이나 잘못된 API 용례가 발견되지 않는 시나리오들을 인지했다면, 깃허브의 레이어에 대한 문제에다가 보고해 주십시오.


크로노스 유효성 레이어들에 대해 더 알고자 한다면, LunarG 의 Vulkan Validation Layers Deep Dive 슬라이드를 읽어보실 것을 권장합니다.


Recommendations


  • 크로노스 유효성 레이어들을 개발 단계에서 주기적으로 사용하십시오.
  • 항상 최신 버전의 레이어 릴리스를 사용하십시오.
  • 다중의 디바이스 상에서 레이어들을 사용해 디바이스-지정 메시지들을 수집하십시오.
  • 주의 : 에러 코드들은 문제를 분류합니다 -- 그것들은 주어진 메시지에 대한 유일한 식별자가 아닙니다.
  • 메시지가 불명확하다면, 레이어 소스 코드를 참고하십시오.


+ Recent posts