Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

codingfarm

6. Direct3D의 그리기 연산 - 셰이더의 컴파일 본문

computer graphics/DX12 book

6. Direct3D의 그리기 연산 - 셰이더의 컴파일

scarecrow1992 2021. 6. 16. 23:40

Direct3D에서 shader program이 컴파일되는 단계는 다음과 같다.

  1. 이식성 있는 byte code로 컴파일
  2. 그래픽 드라이버가 byte code를 시스템의 GPU에 맞게 최적의 네이티브 명령들로 컴파일

 

Shader를 컴파일 하는 타이밍은 2가지로 나뉜다.

  1. 런타임 컴파일
  2. 오프라인 컴파일

 

1. 런타임 컴파일

런타임시 D3DCompileFromFile 함수를 통해 shader를 컴파일 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
HRESULT D3DCompileFromFile(
  LPCWSTR                pFileName,
  const D3D_SHADER_MACRO *pDefines,
  ID3DInclude            *pInclude,
  LPCSTR                 pEntrypoint,
  LPCSTR                 pTarget,
  UINT                   Flags1,
  UINT                   Flags2,
  ID3DBlob               **ppCode,
  ID3DBlob               **ppErrorMsgs
);
cs

HLSL코드를 컴파일 하는 함수

매개변수

1. LPCWSTR pFileName;

  • 컴파일할 HSL 소스코드를 담은 .hlsl 파일의 이름

 

2. const D3D_SHADER_MACRO *pDefines;

  • 고급옵션
  • 여기서는 항상 null pointer를 지정

 

3. ID3DInclude *pInclude;

  • 고급옵션
  • 여기서는 항상 null pointer를 지정

 

4. LPCSTR pEntrypoint;

  • shader program의 entry 함수의 이름
  • 하나의 .hlsl 파일에 여러개의 shader program으 있을 수 있으므로(예를들어 정점 셰이더 하나, 픽셀 셰이더 하나), 컴파일할 특정 shader의 진입점을 명시해야 한다

 

5. LPCSTR pTarget;

  • 사용할 셰이더 프로그램의 종류와 대상 버전을 나타내는 문자열
  • 이 책의 예제들은 5.0과 5.1을 사용한다
    • vs_5_0과 vs_5_1 : 정점 셰이더 5.0과 5.1
    • hs_5_0과 hs_5_1 : 덮개 셰이더 5.0과 5.1
    • ds_5_0과 ds_5_1 : 영역 셰이더 5.0과 5.1
    • gs_5_0과 gs_5_1 : 기하 셰이더 5.0과 5.1
    • ps_5_0과 ps_5_1 : 픽셀 셰이더 5.0과 5.1
    • cs_5_0과 cs_5_1 : 계산 셰이더 5.0과 5.1

 

6. UINT Flags1

  • 셰이더 코드의 세부적인 컴파일 방식을 제어하는 플래그들
  • SDK 문서화에는 많은 플래그가 나오지만 여기서는 다음 두가지만 사용함
    • D3DCOMPILE_DEBUG : 셰이더를 디버그 모드에서 컴파일
    • D3DCOMPILE_SKIP_OPTIMOZATION : 최적화를 생략한다(디버깅에 유용함)

 

7. UINT Flag2

  • 효과(effect)의 컴파일에 관한 고급옵션
  • 이책에서는 안씀 (0 전달)

 

8. ID3DBlob **ppCode

  • 컴파일된 shader object bytecode를 담을 ID3DBlob 구조체의 포인터를 돌려줄 parameter

 

9. ID3DBlob **ppErrorMsgs

  • 컴파일 오류가 발생한 경우 오류 메시지와 경고 메시지 문자열을 담은 ID3DBlob 구조체의 포인터를 이 parameter를 통해서 돌려줌

 

 

 

가령 vertex 및 pixel shader를 compile 하기 위해 D3DCompileFromFile 함수를 호출하는 방법은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ComPtr<ID3DBlob> mvsByteCode = nullptr;
ComPtr<ID3DBlob> mpsByteCode = nullptr;
ComPtr<ID3DBlob> errors[2];
 
HRESULT hr[2];
 
UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)
    compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
 
hr[0= D3DCompileFromFile(
    L"Shaders\\color.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, 
    "VS""vs_5_0", compileFlags, 0, mvsByteCode, errors[0]);
hr[1= D3DCompileFromFile(
    L"Shaders\\color.hlsl", nullptr, D3D_COMPILE_STANDARD_FILE_INCLUDE, 
    "PS""ps_5_0", compileFlags, 0, mvsByteCode, errors[1]);
cs

hr[0]과 hr[1]에 vertex shader와 pixel shader를 컴파일한 shader object bytecode가 담기게된다.

 

ID3DBlob

  • 범용 메모리 버퍼를 나타내는 형식
  • 이 포스티에서는 shader object bytecode를 담기위해 쓰임
  • 다음 두 메서드를 제공함
    • LPVOID GetBufferPointer
      • 버퍼를 가리키는 void* 포인터를 돌려준다.
      • 그 블록에 담긴 객체를 실제로 사용하려면 먼저 적절한 형식으로 캐스팅 해야 함
    • SIZE_T GetBufferSize
      • 버퍼의 크기(바이트 갯수)를 돌려준다

 

 

예제 프레임워크는 실행 시점에서 shader 프로그램을 손쉽게 컴파일하기 위해 아래와 같은 보조함수를 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ComPtr<ID3DBlob> d3dUtil::CompileShader(
    const std::wstring& filename,
    const D3D_SHADER_MACRO* defines,
    const std::string& entrypoint,
    const std::string& target)
{
    UINT compileFlags = 0;
#if defined(DEBUG) || defined(_DEBUG)  
    compileFlags = D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION;
#endif
 
    HRESULT hr = S_OK;
 
    ComPtr<ID3DBlob> byteCode = nullptr;
    ComPtr<ID3DBlob> errors;
    hr = D3DCompileFromFile(filename.c_str(), defines, D3D_COMPILE_STANDARD_FILE_INCLUDE,
        entrypoint.c_str(), target.c_str(), compileFlags, 0&byteCode, &errors);
 
    if(errors != nullptr)
        OutputDebugStringA((char*)errors->GetBufferPointer());
 
    ThrowIfFailed(hr);
 
    return byteCode;
}
cs

위 함수를 호출하는 방법은 아래와 같다.

1
2
3
4
5
ComPtr<ID3DBlob> mvsByteCode = nullptr;
ComPtr<ID3DBlob> mpsByteCode = nullptr;
 
mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS""vs_5_0");
mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS""ps_5_0");
cs

위 코드의 효과는 이전에 설명했던 VS와 PS를 컴파일 하는 예제 코드와 효과가 동일하다.

 

 

주의

shader를 compile 한다고 해서 shader가 rendering pipeline에 묶이지는 않는다.

shader를 pipeline에 binding하는 방법은 추후 나온다.

 

 

2. 오프라인 컴파일

runtime이 아닌 오프라인에서 개별적인 단계로 컴파일할수도 있다.

대표적으로 빌드 과정의 한 단계 또는 자산 내용 pipeline 공정의 일부에서 컴파일이 이루어진다.

 

오프라인 컴파일이 필요한 이유

  1. compile 시간이 오래걸리는 shader는 오프라인단계에 미리 함으로써 게임의 적재(loading) 시간을 줄일 수 있다.
  2. 셰이더 컴파일 오류들은 실행시점이 아니라 빌드 과정에서 점검하는 것이 효율적이다.
  3. Windows 8 스토어 앱은 반드시 오프라인 컴파일을 사용해야 한다.
  4. 컴파일 실패시 콘솔화면을 통해 오류메시지를 직접 확인할 수 있다.

 

컴파일된 셰이더를 담는 파일의 확장자는 .cso(compiled shader object)를 사용하는 것이 관례이다.

오프라인 컴파일시에는 DirectX에 포함된 FXC 도구를 사용한다. 이것은 명령줄 도구이다.

fxc는 일반적으로 DirectX SDK의 설치 경로 아래에 있으며 내컴퓨터의 경우 아래 경로에 존재한다.

C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Utilities\bin\x64

이 fxc를 명령창에서 편리하게 실행하기 위해선 C:\Windows\System32 경로에 넣거나 아니면 위 경로를 환경변수에 저장하는 방법이 있다. 적절한 방법을 선택해서 세팅해준다.

활용법은 아래와 같다.

예를들어 color.hlsl에 담긴, entry point가 VS인 vertex shader와 entry point가 PS인 pixel shader를 디버그 모드로 각각 컴파일 하려면 콘솔창에서 다음과 같은 명령들을 실행하면 된다.

fxc "color.hlsl" /Od /Zi /T vs_5_0 /E "VS" /Fo "color_vs.cso" /Fc "color_vs.asm"
fxc "color.hlsl" /Od /Zi /T ps_5_0 /E "PS" /Fo "color_ps.cso" /Fc "color_ps.asm"
  • /Fo 옵션 : compiled shader object 파일을 생성한다.
  • /Fc 옵션: 이식성 있는 어셈블리 코드(asm)를 생성한다.

.cso 파일이 생성된것을 볼 수 있다.

 

로드 하기

오프라인에서 컴파일할 경우 실행시점에서는 컴파일할 필요가 없는 대신, .cso 파일에 담긴 shader bytecode를 응용 프로그램으로 적재해야한다.

예제 프레임 워크는 컴파일된 셰이더 바이트코드를 C++ 표준 파일 입출력 라이브러리를 이용해서 적재하는 다음과 같은 함수가 제공된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ComPtr<ID3DBlob> d3dUtil::LoadBinary(const std::wstring& filename) {
    std::ifstream fin(filename, std::ios::binary);
 
    fin.seekg(0std::ios_base::end);
    std::ifstream::pos_type size = (int)fin.tellg();
    fin.seekg(0std::ios_base::beg);
 
    ComPtr<ID3DBlob> blob;
    ThrowIfFailed(D3DCreateBlob(size, blob.GetAddressOf()));
 
    fin.read((char*)blob->GetBufferPointer(), size);
    fin.close();
 
    return blob;
}
 
...
 
ComPtrID3DBlob> mvsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_vs.cso");
ComPtrID3DBlob> mpsByteCode = d3dUtil::LoadBinary(L"Shaders\\color_vps.cso");
cs

 

 

어셈블리 코드 생성

fxc 실행시 /Fc 옵션을 주면 이식성 있는 어셈블리 코드를 생성한다.

종종 셰이더의 어셈블리 코드를 출력해서 셰이더 명령 갯수를 확인하거나 어떤 종류의 코드가 생성되었는지 살펴보면 도움이 된다.

어셈블리 코드는 HLSL 코드와 다른 모습을 보이기도 하는데 대표적으로 평탄화(flattening) 작업을 통해 if 분기 명령이 제거되기도 하기 때문이다.

초창기의 GPU는 shader에서 분기명령을 사용하는 비용이 높았기에 아래와 같은 방법으로 분기명령을 제거하기도 했다. 아래 두 코드는 같은 답을 낸다.

1
2
3
4
5
6
7
8
// 조건문을 포함한 코드
floag x = 0;
 
// s ==1 (참) 또는 s == 0 (거짓)
if(s)
    x = sqrt(y);
else
    x = 2 * y;
cs
1
2
3
4
5
6
7
// 평평하게 만든 코드
float a = 2 * y;
float b = sqrt(y);
float x = a + s * (b - a);
 
// s == 1 : x = a + b - a = b = sqrt(y)
// s == 0 : x = a + 0(b - a) = a = 2 * y
cs

종종 어셈블리 코드를 살펴보면서 코드가 어떻게 생성되는지 확인할 수 있다.

 

3. Visual Studio를 이용한 오프라인 셰이더 컴파일

직접 FXC 명령어를 쓰는것이 아닌, visual studio를 통한 오프라인 셰이더 컴파일도 가능하다.

.hlsl 파일을 프로젝트에 추가하면 visual studio(VS)는 그것이 shader 파일임을 인식하고 아래 그림과 같은 속성 페이지를 통해서 컴파일 옵션들을 제공한다. 아래 속성 페이지에서 설정한 값은 FXC 실행시 명령줄 옵션들로 쓰인다.

HLSL 파일을 VS 프로젝트에 추가하면 그 파일은 빌드 공정의 일부가 되며, 프로젝트를 빌드하면 FXC 가 셰이더 파일을컴파일 한다.

 

VS 내장 HLSL 지원 기능의 단점

1. 파일당 셰이더 프로그램을 하나만 둘 수 있다.
즉, 하나의 파일에 정점 셰이더와 픽셀 셰이더를 함께 둘 수 없다.

2. 같은 셰이더 프로그램을 전처리 지시자들을 다르게 해서 컴파일함으로써 여러 변형들을 얻고싶은 경우가 종종 있는데, VS의 내장 HLSL 지원 기능에서는 .hlsl 파일당 하나의 .cso 목적 파일만 가능하므로 이러한 응용이 불가능 하다.

 

 

Comments