[번역] DX12를 사용 시 지향 그리고 지양할 점

2022. 5. 22. 21:09개인 공부 및 연구

 

 

- 2022/05/22 (최초 작성)

- 2022/05/24 (마지막 수정)

발 번역 주의보, Figures나 예제 코드는 빠져있을 수 있습니다.

개인 공부 및 연습용으로 번역된 글입니다. 의역/오역/오타가 많을 수 있으며. 원저작권자의 별도의 허가 없이 작성되었으므로, 언제든지 삭제될 수 있습니다.

원본 아티클


DX12를 사용시 지향 그리고 지양해야 할 점들 (Do's And Don'ts)

by NVIDIA

 

목차


소개

DX12 API는 역대 그 어떤 DirectX API 보다 프로그래머에게 자율을 보장해주는 동시에 그 에 따른 책임을 요구합니다. 이런 책임에는 리소스 상태 배리어에서부터 여러 커맨드 큐들을 동기화하기 위한 펜스들 까지 다양합니다. 또한 비정상적인 API 사용에도 DX 런타임이나 드라이버에 의해 감지되거나 보정되지 않습니다. 위에서 언급한 것들을 잘 유지시키기 위해서 DX12 개발자는 런타임 디버그를 활용하고 어떤 종류의 에러들이 보고되더라도 그 에러들에 주의를 기울여야 합니다. 이 뿐 아니라 DX12의 기능 명세들에 완전히 익숙해질 필요가 있습니다.


엔진 아키텍처/구조

지향 할 점

  • 병렬(parallel) 드로우 콜 제출을 위해 태스크 그래프 아키텍처를 선호하세요
    • 태스크 그래프 아키텍처는 의존성을 가지는 리소스나 커맨드 큐의 작업이 완료되기를 기다려야 하는 드로우 콜 제출을 효율적으로 병렬화 시킬 수 있도록 도와줍니다
  • 작업 제출을 위한 '주(Master) 렌더 스레드'를 두고, 커맨드 레코딩, 리소스 생성 그리고 PSO 컴파일을 위한 몇 개의 '워커 스레드'들을 두세요
    • 워커 스레드들이 커맨드 리스트를 생성 하도록 하고 주 렌더 스레드가 완료된 워커 스레드 중 하나를 골라서 커맨드들을 제출하도록 만드세요
  • 각 IHV(개별 하드웨어 벤더; Independent Hardware Vendor; 예시. NVIDIA, AMD, Intel)들의 최소 요구사항에 맞춰진 개별적인 렌더 패스들을 유지하세요
    • 애플리케이션은 주어진 하드웨어를 가장 효율적으로 동작시키는가에 따라 드라이버를 변경해야 합니다 (그래야 할 의무가 있음)

지양할 점

  • 어떤 Direct3D12 작업도 드라이버 스레드에서 병렬화 되지 않습니다. 그러니까 드라이버가 작업들을 병렬화 시켜 줄 것이라고 기대하지 마세요
    • DX11에서는에서는 가능한 경우 드라이버가 드라이버 워커 스레드에 비동기 작업들을 하도록 만들었지만 - DX12 에서는 이런 일은 더 이상 일어나지 않습니다
    • DX12에서의 총 작업 제출 비용은 줄어 들었지만, 드라이버 스레드가 더 이상 여러 작업들을 대신 처리해주지 않기 때문에, 애플리케이션의 스레드가 해야 할 일은 늘어났습니다.
      하지만 덕분에 병렬로 작업을 제출하는데 더 효과적으로 CPU의 병렬 하드웨어 코어들을 이용 함으로써, 드로우 콜 제출 성능 측면에서는 더 많은 이점들을 얻을 수 있을 것입니다.

작업 제출 - 커맨드 리스트와 번들

지향할 점

  • 여러분이 GPU/CPU 병렬화를 달성하고 이를 제어해야 할 책임이 있음을 받아들이세요
    • 커맨드 리스트에 작업을 제출하는 것 만으로는 GPU에선 아무런 일도 일어나지 않습니다
    • 최종적으로 ExecuteCommandList()를 호출하여야지만 GPU에서 작업이 시작됩니다
  • 여러 스레드/코어들에 걸쳐서 다중 커맨드 리스트들에 균등하고 병렬적으로 작업을 제출하세요
    • 커맨드를 기록하는 것은 CPU 집약적인 연산이기 때문에 드라이버 스레드가 아무런 도움도 줄 수 없습니다
    • 커맨드 리스트는 스레드로부터 독립적이지 않기 때문에 병렬적으로 작업을 제출한다는 것은 여러 커맨드 리스트들에 제출하는 것을 의미합니다 (워커 스레드마다). 
  • 커맨드 리스트의 준비와 초기화에 비용이 따른 다는 사실을 인지하세요
    • 여전히 여러분은 효율적인 병렬 작업 제출을 위한 합리적인 수의 커맨드 리스트들이 필요합니다
    • 펜스들은 다양한 이유들(다중 커맨드 큐, 쿼리 결과 가져오기)로 인해 커맨드 리스트를 쪼갭니다(splitting) 
  • 15-30개 이하의 범위에서 합리적인 수의 커맨드 리스트를 사용하는 것을 목표로 해서. 커맨드 리스트들을 (bundle) 프레임 당 5-10번의 ExecuteCommandLists() 호출로 묶을 수 있도록 하세요.
  • 커맨드 리스트의 묶음에서 기록된 프래그먼트들을 가능하다면 재사용하세요
    • 이렇게 하면 CPU 시간을 한번 더 쓸 필요가 없습니다
  • 드문드문 번들(bundle) 리소스 바인딩 상속을 사용하세요
    • 이를 통해 완벽하게 준비된 번들을 쉽고 오버헤드가 적게 재 사용할 수 있도록 합니다
    • 만약 개별적인 컴퓨트 커맨드 큐들을 사용할 때 주의 깊게 검사를 한다면 이는 이점이 됩니다
  • 이론적으로 컴퓨트 작업이 그래픽스 작업들과 병렬적으로 실행될 수 있더라도, 실제 GPU 상에서의 병렬 작업 스케쥴링 세부사항에 따라서 여러분이 의도한 대로 결과가 나오지 않을 수도 있습니다
  • 어떤 비동기 컴퓨트 그리고 그래픽스 워크로드라도 동시에 스케쥴링될 수 있다는 점에 유의하세요. 올바른 작업 순서를 짝지어 주기 위해선 펜스를 사용하세요
  • 만약 여러분이 비동기 컴퓨트와 그래픽스 워크로드들을 병렬적으로 실행하는 것에 초점을 맞추고 있다면 모든 프레임들에 대해 반드시 단 하나의 CBV/SRV/UAV 디스크립터 힙을 하나의 링-버퍼로써 사용하세요.

지양할 점

  • 번들을 몇 개 이상의 드로우 콜들을 기록하는 데 사용하지 마세요 (예. ~12개 이하의 드로우 콜은 괜찮습니다)
    • 다르게 말하자면 일반적으로 번들의 재사용성을 제한하여야 합니다
  • 3D 큐에서의 컴퓨트 작업을 독립적인 비동기 컴퓨트 큐에 있는 컴퓨트 작업과 겹치게 하지 마세요
    • 이런 행동은 비동기 컴퓨트 큐에 버블을 발생시킬 수 있습니다
    • 이런 경우 가능하다면 컴퓨트 워크로드를 그래픽스 워크로드로 변경하세요
  • 극단적으로 작은 커맨드 리스트를 제출하지 마세요
    • 작은 크기의 커맨드 리스트는 가끔 CPU에서의 OS 스케쥴러가 새로운 커맨드 리스트를 제출하는 것보다 더 빠르게 완료될 수도 있습니다. 이런 경우 유휴 GPU 사이클들을 낭비하는 결과로 이어지게 됩니다.
    • OS는 이전 ExecuteCommandLists 콜 다음에 커맨드 리스트를 스케쥴링하는데 50-80 마이크로초를 소요합니다. 만약 호출 내의 하나 또는 모든 커맨드 리스트가 이보다 더 빠르게 실행된다면, 하드웨어 큐에 버블이 발생할 수 있습니다.
    • GPUView를 통해 버블이 발생하는지 확인하세요
  • 큰 장면뿐만 아니라 모든 것을 단 몇 개의 커맨드 리스트에 기록하려고 하지 마세요
    • 이러한 행동은 여러분이 가진 CPU 코어들을 완전히 활용할 능력을 제한하게 됩니다
    • 또한 적은 수의 커다란 커맨드 리스트를 만든다는 것은 잠재적으로 GPU를 유휴 상태로 유지하기가 더 어려워질 수 있다는 것을 의미합니다.
  • 모든 것을 기록한 후, 즉 프레임의 끝에서만 제출하지 마세요
    • 다른 커맨드 리스트를 기록하는 동안 병렬적으로 GPU가 동작할 수 있도록 할 기회를 낭비하게 됩니다.
  • 대부분의 리스트가 재사용될 것이라는 기대를 하지 마세요
    • 보통 오브젝트 가시성과 같이 프레임마다 바뀌는 부분이 많이 있습니다
    • 단, 후처리(post-processing)의 경우엔 예외일 수도 있습니다
  • 스레드든 커맨드 리스트든 너무 과하게 많이 만들지는 마세요
    • 너무 많은 스레드를 만들면 CPU 자원을 독점하게 될 수 있고, 너무 많은 커맨드 리스트를 만드는 경우엔 너무 큰 오버헤드가 누적되는 원인이 될 수 있습니다.

파이프라인 상태 오브젝트 (PSOs)

지향할 점

  • PSO들을 워커 스레드에서 비동기적으로 생성하게요
    • PSO 생성은 쉐이더 컴파일 및 관련된 스톨(stall)들이 일어나게 되는 곳입니다
    • 더 일반적인(general) PSO를 먼저 사용하세요 (빠르게 컴파일되는 제네릭 쉐이더 들과 함께) 그리고 특수화된 PSO를 나중에 생성하세요
      • 심지어 가장 선택적인 PSO나 쉐이더를 실행하지 않았음에도 빠르게 실행됩니다
      • 쉐이더의 분화(specializations)를 생성하는 것은 여러분이 해야 할 일입니다 - 드라이버는 여러분 대신 최적화된 쉐이더 변이(variants)를 생성해주지 않습니다.
    • 런타임 PSO 컴파일은 대부분의 경우 스톨을 야기할 수 있으니 하지 마세요
      • 드라이버가 관리하는 쉐이더 디스크 캐시가 도움이 될 수 있습니다
    • 가능하다면 PSO 간의 상태 변화를 최소화하세요
      • PSO가 GPU의 원자적 상태 변화에 반드시 맵핑되지는 않습니다
    • 가능하다면 신경 쓰지 않아도 되는 필드 어디든지 동일한 합리적인 기본 값을 사용하세요
      • 그렇게 함으로 써 PSO를 재사용할 수 있는 가능성이 더 커집니다
    • 가능한한 all_resources_bound/D3DCOMPILE_ALLRESOURCES_BOUND 컴파일 플래그를 사용하세요
      • 그렇게 하면 컴파일러가 더욱 텍스처 접근들을 잘 최적화할 수 있게 됩니다. 저희는 이 플래그를 활성화시켰을 때 1% 미만의 프레임 레이트 향상을 얻었습니다.

지양할 점

  • 정말로 필요한 경우가 아니라면 같은 커맨드 큐상에서 컴퓨트와 그래픽스 사이를 왔다 갔다 하지 마세요.
    • 이런 행동은 여전히 무거운 스위치 작업입니다
  • 정말로 필요한 경우가 아니라면 테셀레이션을 껐다 켰다 하지 마세요
    • 이 경우도 여전히 무거운 작업입니다
  • PSO가 생성되는 곳이 쉐이더가 컴파일되고 스톨이 발생하기 시작하는 곳임을 잊지 마세요
    • PSO를 비동기적으로 그리고 사용되기 훨씬 전에 생성되는 것은 정말로 중요합니다
    •  PSO 컴파일 스레드를 위한 스레드의 우선순위를 조심해서 다루세요
      1. 서두를 필요가 없다면 게임 스레드들이 느려지는 것을 방지하기 위해서라도 유휴 우선순위를 사용하세요
      2. 서두를 필요가 있다면 일시적인 부스팅 우선순위의 사용을 고려해보세요

루트 시그니처

지향할

  • 상수와 CBV들을 배치하세요 (SRV와 UAV는 NVIDIA 하드웨어에서 가능한 경우에만 루트 시그니처에서 직접적으로 가지도록 하세요)
    • 픽셀 단계를 위한 항목에서부터 시작하세요
      • 루트 시그니처에 직접적으로 저장된 상수들은 NVIDIA 하드웨어에서 눈에 띄게 픽셀 쉐이더들을 빨라지게 할 수 있습니다 - 특별히 우버 쉐이더(uber-shader; 통합 쉐이더)의 토글 쉐이더 상수에 루트 상수의 사용을 고려해보세요
      • 루트 시그니처에 저장된 CBV 도한 NVIDIA 하드웨어에서 픽셀 쉐이더의 속도를 눈에 뛰게 개선해줍니다
    • 쉐이더 단계들의 실행 빈도를 계속 낮추세요
    • 루트 시그니처에 저장되는 CBV(루트 디스크립터)를 사용하면 CBV 디스크립터의 저장이나 버전화 항목들이나 추가 간접 접근을 위한 디스크립터 힙이 필요하지 않습니다 (=> CreateConstantBufferView()를 호출할 필요가 없습니다)
    • 루트 뷰들은 바운드 검사를 하지 않거나 다른 제한점을 가지지 않는다는 것을 기억하세요
  • 루트 상수, CBV, SRV 그리고 UAV의 현재 값을 CPU 메모리에 캐싱해두고 정말로 변화가 감지된 경우에만 루트 시그니처의 콘텐츠를 변경해주세요.
    • 저희의 경우 변화를 제대로 관리해줌으로써 눈에 띄는 성능 향상을 이끌어낼 수 있었습니다
  • CBV, SRV 그리고 UAV의 쉐이더 가시성(shader visibility)을 오직 필요한 단계에 대해서만 노출되도록 제한하세요
    • 각 단계에서 뷰를 봐야 할 때 드라이버나 GPU에서 오버헤드가 발생합니다
    • 명시적으로 리소스의 쉐이더 가시성을 제한하기 위해선 DENY_*_ACCESS 플래그를 사용하세요
  • 루트 시그니처가 변경되는 횟수를 최소화하세요
    • RS를 변경하는 것 자체는 문제가 되지 않지만 루트 시그니처 변경 이후에 발생하는 루트 시그니처 항목들의 초기화 비용이 존재합니다.
  • 우아하게 CBV, UAV, SRV 그리고 샘플러 디스크립터는 티어 1 그리고 CBV와 UAV 디스크립터는 티어 2 하드웨어에서 처리하세요
    • 이런 티어들의 경우, 애플리케이션은 커맨드 리스트가 실행되기 전까지 루트 시그니처 (그리고 사용된 디스크립터 테이블) 안에 정의되어있는 모든 디스크립터 내부를 채워야 합니다. 이는 모든 디스크립터들이 사용하는 쉐이더에서 참조하지 않는 경우에도 일어나야 합니다.
    • 티어 3은 사용하지 않게 된 디스크립터들의 바운드를 유지시키도록 하세요 - 이들을 언바인드 하는 것은 쉽게 상태 스래싱 병목을 발생시킬 수 있으니, 여기에 시간을 낭비하지 마세요.

지양할

  • 서로 다른 업데이트 주기를 가지는 CBV들을 같은 CBV 디스크립터 테이블에 모으지 마세요.
    • 이상적으로 테이블 안의 모든 CBV들은 같은 타이밍에 업데이트되어야 합니다
  • 루트 시그니처와 디스크립터 테이블을 재사용하기 위해 크기를 부풀리지 마세요
    • 각 재질의 집합마다 최소한의 엔트리의 집합을 사용하는 것을 목표로 하세요
  • 루트 테이블 항목에서 같은 쉐이드 스테이지에 대한 visible 그리고 deny 플래그를 동시에 설정하지 마세요
    • 현재 드라이버에서는 deny 플래그가 D3D12_SHADER_VISIBILITY_ALL 플래그가 설정돼있을 때만 작동합니다
  • 아주 높은 빈도로 드로우/디스패치 콜에서 사용하지 않는 한 직접적으로 SRV와 UAV를(루트 파라미터로 써) 루트 시그니처에 포함시키지 마세요
  • 루트 시그니처가 변경된 이후에 리소스 바인딩을 미정의 상태로 나 두지 마세요
    • 루트 시그니처를 변경할 때 모든 이전 루트 시그니처에서 사용된 리소스 바인딩은 삭제되거나/청소됩니다.

할당자와 리스트

지향할

  • 비슷한 크기의 드로우 콜 시퀀스에 할당자를 재사용하세요
    • 리스트가 미리 캐싱돼있으면(pre-warmed) 할당이 아주 빠릅니다
  • 최소 2*T + N개의 할당자들을 사용하세요
    • 2* - 집합 중 하나는 여전히 GPU에 의해 사용되고 있는 지난 프레임의 리스트/할당 자이고 그다음 두 번째 집합은 현재 프레임에서 사용되거나 만들어지기 시작하는 리스트/할당자들의 집합입니다.
    • T = 커맨드 리스트를 만드는 스레드의 수 - 할당자들이 스레드로부터 자유롭지 않다는 것을 주의하세요!
    • N = 번들을 위한 여분의 풀(pool)
  • 다른 프레임에서 할당자를 다시 사용할 때 Allocator::Reset을 호출하세요
    • 다른 말로 말하자면 할당자는 여러분의 메모리를 다 써버리기 전까지 계속 크기를 키워나간 다는 것입니다

지양할

  • 할당자와 리스트가 GPU 메모리를 소비한다는 점을 잊지 마세요
    • 너무 큰 할당자는 여러분의 GPU 작업을 예상치 못한 방향으로 제한시킬 수 있습니다
  • 할당자를 새로 생성하거나 있던 할당자를 파괴하는 대신에 이미 만들어진 할당자를 재활용하세요
    • 할당자를 생성하고 파괴하는데 들어가는 오버헤드를 줄이세요
  • 다른 크기의 드로우 콜 시퀀스에 재사용 하진 마세요
    • 이는 최악의 크기의 할당자가 될 수 있습니다
  • 커맨드 리스트의 집합을 재설정할 때 연관된 할당자를 초기화하는 것을 잊지 마세요
    • 할당자를 재설정하지 않는 것은 메모리 누수를 의미합니다!
  • 활성화돼있는 커맨드 리스트에 의해 사용 중에 있는 할당자를 해제하거나 재사용하지 마세요
    • 이는 좋지 않은 행동이고 여전히 커맨드 리스트가 사용하고 있는 메모리 공간을 덮어 씌우거나 해제해버릴 수도 있습니다.

리소스

지향할

  • 비디오 메모리의 과도한 사용을 피하세요
    • 사용 가능한 비디오 메모리에 관한 정확한 정보를 얻기 위해선 IDXGIAdapter3::QueryVideoMemoryInfo()를 사용하세요
    • 전위(foreground) 애플리케이션에 무조건 높은 % 의 비디오 메모리가 할당되는 것은 아닙니다
    • OS으로부터의 예산 변경에 대응하세요
      • IDXGIAdapter3::RegisterVideoMemoryBudgetChangeNotificationEvent의 사용을 고려하세요
      • 사용 가능한 메모리에 기반하여 그래픽 설정을 제한하세요
    • 시스템 메모리에 오버플로우 힙을 만들어 넣고 비디오 메모리 힙에서 넘치는 리소스들을 이동시키세요
    • DX12는 DX11 드라이버를 넘는 애플리케이션 메모리 관리 이점을 제공합니다
      • 커맨드 리스트들을 분해함으로 써 각각의 참조 되는 메모리의 양을 비디오 메모리에 맞춥니다
      • 커맨드 리스트당 어떤 것이 사용되고 있는지 추적합니다
      • 비디오 메모리 예산을 초과해야 하는 경우에는 커맨드 리스트 실행 전/후로 MakeResident/Evict를 사용하는 것을 고려하세요
    • 드라이버가 더 많은 정보를 가질 수 있는 경우에는 커밋된 리소스를 사용하세요
      • 이는 드라이버가 GPU 메모리를 더 잘 관리할 수 있도록 해줍니다
      • 배정된 리소스의 좋은 사용 사례는 예를 들어 스트리밍에 사용되거나 전체 수명 동안 서로 다른 읽기-전용 텍스처들의 집합을 가지고 있는 리소스 힙의 경우가 있습니다.
    • MakeResident 호출들을 배치(Batch) 하세요 (페이지 테이블 업데이트를 위한 CPU 그리고 GPU 비용을 예측해서)
      • 이는 GPU와 드라이버 내부의 오버헤드를 줄여줍니다
    • MakeResident/MakeUnresident를 사용해서 주어진 메모리 예산으로 작업하기 위해서
      • 필요한 만큼 타일화된 리소스의 밉 레벨을 드롭시키세요
      • MakeResident가 실패했을 경우를 처리해야 합니다
    • 특정 리소스 타입들이 힙 내부에서 다른 정렬(alignment) 규칙들을 가지고 있다는 사실을 인지하세요
    • 꼭 디바이스 피처 레벨 내부의 다양한 리소스 바인딩 티어들을 처리하는 방법을 강구하세요
      • UAV는 모든 단계(쉐이더)에 걸쳐 8개 또는 64개로 제한됩니다
      • CBV는 단계마다 14개로 제한됩니다
      • 샘플러는 단계마다 16개로 제한됩니다
    • 힙을 위한 앨리어싱 규칙을 잘 알아두세요
      • 타 일화 된 리소스 명세를 보세요
    • 리소스, SRV, DSV 기타 등등을 위한 서로 다른 힙 타입이 존재한다는 사실을 알고 있으세요
      • 몇몇 힙 티어에서는 다른 티어보다 더 많은 제한들이 존재할 수 있습니다
      • 리소스 힙 티어 호환성을 검사하세요
    • 깊이 스텐실 텍스처를 복사하기 위해 CopyTextureRegion()을 사용할 대는 주의를 기울여서 D3D12_TEXTURE_COPY_LOCATION을 채워 넣으세요.
      • 리소스의 깊이 파트만 복사하면 느린 경로가 사용될 수 있습니다

지양할

  • 깊이 스텐실 그리고 렌더 타깃 리소스를 위해 배정된 리소스의 재사용 횟수를 초과하지 마십시오
    • 리소스를 렌더링 하기 전에 해당 리소스를 삭제해야 하는 필요성 외에도 이런 전환을 오래 걸리게 만드는 다른 하드웨어 종속적인 장부(북키핑) 연산이 있을 수도 있습니다
  • 타일화된 리소스의 유효성에 의존하지 마십시오
    • 여전히 다른 DX12 하드웨어 클래스들에 대해 생각해야 합니다
  • 한 번에 모든 GPU 메모리를 할당할 수 있는 것에 의존하지 마십시오
    • GPU 아키텍처에 따라 메모리는 분할될 수도 있고 되지 않을 수도 있습니다
  • MakeUnresident 호출의 즉각적인 비용을 예측하지 마십시오
    • 다른 MakeResident 호출이 메모리를 활성화시킬 때까지 비용이 지연될 수 있습니다
      • 지연된 페이징 요청을 찾기 위해선 GPUView 분석을 사용하십시오
  • 피할 수 있다면 리소스의 생성과 파괴를 하지 마세요
    • 가능하다면 MakeUnresident와 MakeResident를 사용하는 것이 더 낫습니다
    • 리소스의 생성과 파괴로 인한 오버헤드를 피하세요

배리어, 펜스 그리고 하자드 (Hazard)

지향할

  • 배리어와 펜스를 최소한으로 사용하세요
    • DX11에서 DX12로 포팅할 때 가장 큰 퍼포먼스 문제는 불필요한 배리어의 사용으로 인한 유후 작업을 기다리는 것과 관련이 있음을 알아냈습니다
      • DX11의 드라이버는 배리어를 줄여주지만, DX12에서는 프로그래머 스스로가 이런 일을 해주어야 합니다
    • 그 어떤 배리어나 펜스의 사용은 병렬성을 제한하는 결과로 이어질 수 있습니다
  • 언제나 리소스 사용 플래그의 집합을 최소화해서 사용하세요
    • 정말로 D3D12_RESOURCE_USAGE_GENERIC_READ에 내포되어 있는 플래그 조합을 사용하기 위해서 사용하는 것이 아니라면 사용을 피하세요
    • 불필요한 플래그는 불필요한 플러시나 스톨을 일으켜 의도치 않게 여러분이 만든 게임의 퍼포먼스를 떨어트릴 수 있습니다
    • 다시 말씀드립니다: 불필요하게 지나친 보수적인 배리어의 플래그들과 이와 관련된 유휴 연산을 위해 기다리는 일은 DX11에서 DX12로의 포팅에 있어서 가장 큰 퍼포먼스 문제를 야기시킵니다.
  • ID3D12CommandList::ResourceBarrier에 최소한의 타깃 집합을 명시하세요
    • 거짓(false) 의존성을 추가하는 건 불필요한 중복을 야기합니다
  • 여러 개의 배리어를 모아서 한 번의 ID3D12CommandList::ResourceBarrier를 통해 제출하세요
    • 모든 배리어를 순차적으로 실행하는 대신 이 방법을 사용하면 최악의 경우를 피할 수 있습니다(불필요한 배리어의 사용/중복)
  • 가능한 경우에 분리된 배리어를 사용하세요
    • _BEGIN_ONLY/END_ONLY 플래그를 사용하세요
    • 이렇게 하면 드라이버가 좀 더 효율적으로 동작할 수 있습니다
  • 시그날 이벤트/진전을 ExecuteCommandLists 쪽에 알리기 위해 펜스를 사용하세요

지양할

  • 불필요한 배리어를 두지 마세요
    • 이런 행동은 병렬성을 크게 해칠 수 있습니다
    • D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE에서 D3D12_RESOURCE_STATE_RENDER_TARGET으로 전환 한 다음에 그 사이에서 아무런 드로우 콜을 하지 않는 것은 불필요한 전환을 한 것입니다
    • 읽기에서 읽기로 상태를 전환하는 배리어를 두지 마세요
      • 리소스를 모든 후속 읽기 연산에 대한 적절한 상태로 두세요
  • 합당한 이유가 없다면 D3D12_RESOURCE_USAGE_GENERIC_READ를 사용하지 마세요
    • 쓰기에서 읽기 상태로 전환할 때는, 전환 대상이 다음 쓰기 상태로의 전환 전에 요구될 모든 읽기 상태들을 포함하도록 만드세요. 이는 읽기 상태 플래그들을 합침으로써 API단에서 가능하고 후속 ResourceBarrier 호출에서 읽기에서 읽기 상태로의 전환을 통한 전환보다 선호되는 방법입니다.
  • 하나의 배리어에 대해 ID3D12CommandList::ResourceBarrier를 순차적으로 호출하지 마세요
    • 이는 드라이버가 배리어 집합에서 최악의 경우를 골라낼 수 없도록 만듭니다
  • 펜스가 ExecuteCommandLists 호출 한번 보다 더 세부적으로 신호와/전달을 트리거할 것이라고 기대하지 마세요

다중 GPU

지향할

  • 여러분의 시스템에 얼마나 많은 GPU가 있는지 알아낼 때는 DX12 표준 검사를 사용하세요
    • 더 이상 벤더의 특정 API들을 사용할 필요가 없습니다
    • 꼭 CROSS_NODE_SHARING 티어를 검사하세요
  • 어떤 표면(surface)이 동기화가 일어나야 하고 어떤 것이 일어나지 말아 나는지를 완전히 제어 하에 두세요
    • 리소스에 대한 명시적인 제어를 최대한 활용하세요
    • 각 노드에서 동기화될 필요가 있는 리소스를 만드세요
      • 적절한 CreationNodeMask를 사용하세요
      • 접근을 하고자 하는 노드가 리소스를 볼 수 있도록 하세요
    • 필요하다면 현재 노드로 리소스를 복사해오세요
  • 동기화를 최소화하세요
  • 장치가 티어 2의 노드 간 공유를 지원한다면
    • 항상 티어 1 타입의 구현과 성능을 비교하세요
  • 노드 간 복사 연산을 할 때는 지정된 복사 큐들을 사용하세요

지양할

  • 암묵적인 MGPU 스케일링으로부터 이득을 얻으려고 하지 마세요
    • 어떤 표면도 자동으로 동기화될 것이라고 믿지 마세요
    • 암묵적인 MGPU 스케일링이 필요할 때 일어나는 동기화 들을 완전히 통제하세요

스왑 체인

지향할

  • Flip 모드 스왑 체인을 사용하세요
  • 정확한 즉시 독립 플립 모드로의 전환을 위해 (경계가 없는) 풀 스크린 윈도에 대해 SetFullScreenState(TRUE)와 non-windowed 플립 모델 스왑 체인을 사용하세요
    • Microsoft에 따르면 현재 이 모드가 D3D12에서 Present(0,0)을 호출할 때 티어링을 동반하는 무제한 프레임 레이트를 사용할 수 있는 유일한 모드입니다.
    • 다른 모드는 티어링을 유발하는 무제한 프레임 레이트를 허용하지 않습니다
  • 의식적으로 DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH 플래그를 사용하세요
    • 만약 윈도의 사이즈가 현재 화면의 해상도와 일치한다면 무제한 프레임 레이트를 위해 플래그를 설정할 필요가 없습니다
    • 만약 이 플래그가 설정돼 있을 때, SetFullScreenState(TRUE)를 호출하기 전에 ResizeTarget()을 호출하여서 해상도를 변경하게 된다 해도 제대로 동작할 것이며 제한되지 않은 FPS를 얻을 수 있습니다.
      만약 이 플래그가 설정되어 있지 않은 상태에서, SetFullScreenState(TRUE) 호출 전에 ResizeTarget()을 통해 해상도를 변경하게 되면 디스플레이의 해상도에 변화가 없이 대상이 현재 해상도로 늘려지고(stretched) FPS 또한 제한이 걸리게 됩니다.
  • 만약 풀 스크린 상태에 있지 않다면 (정화한 즉시 독립 플립 모드) 원하는 FPS와 지연에 따라 스왑-체인에서의 지연과 버퍼의 개수를 조심해서 컨트롤하세요.
    • 원하는 지연시간을 설정하기 위해선 IDXGISwapChain2::SetMaximumFrameLatency(MaxLatency)를 사용하세요
      • 이게 제대로 동작하도록 하기 위해선 DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT 플래그가 설정된 상태로 스왑 체인을 생성하세요
        li> 동기화 주기가 0이란 것은 "현재 출력하고 있는 버퍼가 다음 구성이 일어날 때 가장 최신의 버퍼이다"를 의미하고 이전에 표시한 것들을 모두 버립니다. 하지만, 출력은 구성이 시작되기 전까지 일어나지 않습니다, 즉 VSync가 일어날 때만 일어납니다.
      • DXGI는 MaxLatency-1 번 만큼 화면이 출력되고 난 다음에는 진행을 막습니다
        • 기본 지연이 3이란 것은 FPS가 2 * RefreshRate 보다 높아질 수 없음을 의미합니다. 즉 60HZ이 모니터를 사용하면 FPS는 120 FPS 이상이 될 수 없다는 것입니다.
      • 큐 프레임으로 생각하고 있는 것보다 약 1개에서 2개의 스왑 체인 버퍼를 더 사용하세요 (커맨드 할당자들과 동적 데이터 그리고 연관된 프레임 펜스의 입장에서) 그리고 "최대 프레임 지연"을 스왑 체인 버퍼의 개수로 설정하세요.
  • 만약 풀 스크린 상태가 아니라면 (true immediate independent flip mode) 더 높은 FPS를 만들어 내기 위해서 WaitForSingleObjectEx()를 사용하는 대기 가능한 객체 스왑 체인(waitable object swap chain)의 사용을 고려하세요
    • 이 방법은 가끔 부분적으로도 절대 표시되지 않는 프레임을 만들어 낼 수도 있지만, 벤치마킹을 위한 애플리케이션을 만들 때는 좋은 선택일 수 있습니다.
    • 대기 가능한 객체 스왑 체인과 GetFrameLatencyWaitableObject()를 사용해서 버퍼가 렌더링 되기 전이나 출력되기 전에 유효한지 테스트할 수 있습니다 - 유효한 옵션들은 다음과 같습니다:
      1. 추가적인 오프-스크린 표면을 사용한다
        • 오프-스크린 서페이스에 렌더링 합니다. 버퍼가 유효한지 검사하기 위해서 대기 가능 객체를 timeout 0로 테스트합니다. 만약 유효하다면 스왑-체인 백 버퍼로 복사하고 Present()를 호출 합니다. 만약 유요한 버퍼가 없다면 프레임을 처음부터 다시 시작합니다.
          프레임이 시작될 때, 대기 가능 객체를 테스트 합니다. 만약 성공하면, 유효한 스왑 체인 버퍼에 렌더링 합니다. 만약 실패한다면, 오프 스크린 표면에 렌더링 합니다.
      2. 3이나 4 버퍼 스왑 체인을 사용한다
        • 백 버퍼에 직접 렌더링 합니다. Present()를 부르기 전에, 대기 가능 객체를 테스트합니다. 만약 성공하면, Present()를 호출하고, 실패하면 처음부터 다시 시작합니다.

지양할

  • DXGI가 Present()의 동작을 멈추기 전까지 각 스왑 체인당 3개의 대기 중인 프레임 제한이 있다는 것을 잊지 마세요
    • 이 기본 값을 변경하기 위해선 스왑 체인을 생성할 때 DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT 플래그를 설정하고 IDXGISwapChain2::SetMaximumFrameLatency를 사용하세요.
    • SetFullScreenState(TRUE)를 사용해서 정확한 즉시 독립 플립 모드로 전환한 후에 ResizeBuffers()를 호출하는 것을 잊지 마세요.

SetStablePowerState

절대 게임 엔진 코드에서 SetStablePowerState(TRUE)를 호출하지 마세요.

 

더 낮은 퍼포먼스의 비용에 비해 매우 안정적인 결과가 필요하든 말든 SetStablePowerState의 사용을 심사숙고하세요. 우리 블로그에서 이에 대한 논의를 확인하세요.

 

정말로 안정적인 결과를 원한다면, 스탠드얼론 애플리케이션에서 별도로 SetStablePowerState를 호출하도록 하세요.

 

혼동을 피하려면, 함수의 실행 여보를 명확히 해야 합니다. (성능 결과와 함께 클락(clock)을 기록하는 것이 좋습니다. 실제로 우리는 이 방법을 자주 사용합니다. 우리의 블로그에 NVIDIA에서 어떻게 GPU 클락을 질의하는지에 대한 코드 조각을 제공하고 있습니다.)

 

DX12 API와 다른 API들을 테스트할 때 클락을 안정화시키는 우리의 스탠드얼론 프로그램을 사용하세요.

 

이에 대한 더 많은 논의는 별도의 블로그 포스트에서 확인해볼 수 있습니다:

SetStablePowerState.exe: NVIDIA GPU들에서의 더 결정론적인(deterministic) 타임 스탬프 질의를 위해 Windows 10에서 GPU 부스트를 비활성화 하기


DirectX 12 하드웨어 기능들과 다른 맥스웰 기능들

지향할

  • 최대 속도의 보수적인 래스터화를 위해선 하드웨어 기반 보수적인 래스터화를 사용하세요
  • 다른 Maxwell 기능들을 사용하기 위해선 NvAPI (가능하다면)을 사용하세요
    • 고급 래스터화 기능들
      • 쿼드 기반 지오메트리를 위한 바운딩 박스 래스터화 모드
      • Post depth coverage mask와 서브 샘플들로의 데이터 라우팅을 위한 coverage mask 오버라이딩과 같은 새로운 MSAA 기능들
      • 프로그래밍 가능한 MSAA 샘플 위치
    • 빠른 지오메트리 쉐이더 기능들
      • 지오메트리 증폭 없이 하나의 지오메트리 패스에서 큐브 맵에 렌더링 하기
      • 지오메트리 증폭없이 여러 뷰 포드에 렌더링 하기
      • 픽셀 쉐이더에서 삼각형 당 데이터가 필요한 기법들을 위한 빠른 전달 지오메트리 쉐이더 사용
    • 새로운 상호 잠금(interlocked) 연산들
    • 개선된 블렌딩 연산들
    • 새로운 텍스처 필터링 연산들

지양할

  • 래스터 오더 뷰 (ROV) 기술 남용하지 말기
    • 순서를 보장하는 것은 공짜로 이루어지지 않습니다
    • 항상 고급 블렌딩 연산과 아토믹과 같은 대체 가능한 방법들과 비교하세요

엔비디아 DirectX 12 하드웨어 기능 테이블

NVIDIA GeForce Kepler Maxwell Pascal
기능 레벨 (Feature Level) 12_0 12_1 12_1
리소스 바인딩 Tier 2 Tier 2 Tier 2
타일화된 리소스 Tier 1 Tier 3 Tier 3
타입화된 UAV 로드 미지원 지원 지원
보수적인 래스터화 미지원 Tier 1 Tier 2
래스터라이저 정렬 뷰(ROV) 미지원 지원 지원
스텐실 참조 출력 미지원 미지원 미지원
UAV 슬롯 수 64 64 64
리소스 힙 Tier 1 Tier 1 Tier 1