ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • HLSL 2021
    개인 프로젝트 2024. 12. 15. 18:40

    목차

    1. 개요
    2. 컴파일 방법
      1. DirectX Shader Compiler 다운로드
      2. 코드를 통한 컴파일러 사용법
    3. 주요 변경점
      1. 사용자 정의 데이터형에 대한 엄격한 캐스팅
      2. 논리 연산자 단락 평가
      3. C++ for문 범위 규칙
      4. 템플릿 함수와 데이터 타입
      5. 멤버 연산자 오버로딩
      6. 데이터형의 비트 필드 멤버
    4. 마치며

    개요

    이 글에서는 HLSL의 최신 언어 사양인 HLSL 2021에 대하여 알아보겠습니다. 이름과 같이 2021년도에 발표된 HLSL의 최신 업데이트로 특정 셰이더에 대한 문법 및 기능 추가를 위한 셰이더 모델 업데이트와 다르게 HLSL의 전반에 영향을 미치는 기능입니다.

    HLSL 2021를 소개하는 Microsoft의 개발자 블로그에서는 일정 시간 후 HLSL 2021를 기본 언어 버전으로 만들 계획이라고 소개했는데 3년이 지난 2024년 현재 최신 DXC 버전으로 테스트해 보면 HLSL 2021이 기본 언어 버전으로 설정된 것으로 확인됩니다. 따라서 새로운 HLSL의 주요 변경점에 대해서 살펴볼 만한 시점이 되었다고 생각합니다.

    컴파일 방법

    DirectX Shader Compiler 다운로드

    우선 HLSL 2021로 셰이더 코드를 컴파일하는 방법을 알아보겠습니다. 셰이더 코드 컴파일에는 우선 DirectX Shader Compiler(이하 DXC)가 필요합니다. DXC는 다음 Github 저장소에서 다운로드할 수 있습니다.

    https://github.com/microsoft/DirectXShaderCompiler

    최신 Releases 압축파일을 다운로드하여 압축을 풀어보면 다음과 같은 구조로 되어 있습니다.

    bin에는 dll과 커맨드 라인에서 사용할 수 있는 dxc.exe 실행 파일이 들어 있습니다. inc에는 header 파일이 들어 있으며 lib에는 라이브러리 파일이 들어 있습니다.

    코드를 통한 컴파일러 사용법

    dxc.exe를 통해서 커맨드 라인으로 셰이더를 컴파일할 수도 있지만 여기서는 더 나아가 코드에 통합하여 셰이더를 컴파일하는 방법을 알아볼 것입니다. 따라서 DXC 폴더의 구조를 참고하여 본인의 빌드 환경에 맞게 포함 경로와 라이브러리 경로를 설정할 필요가 있습니다. 샘플 코드에서는 cmake를 통해 빌드 환경을 구축하고 있으므로 다음과 같이 설정하였습니다.

    set (SOURCE
    	"./Source/Private/main.cpp"
    )
    
    add_executable(HLSL2021 ${SOURCE})
    
    # 포함 경로 설정
    target_include_directories(HLSL2021
    PUBLIC
    	Source/Public
    PRIVATE
    	./../../ThirdParty/DXC/inc)
    
    # 라이브러리 경로 설정
    target_link_directories(HLSL2021
    PRIVATE
    	./../../ThirdParty/DXC/lib/x64)
    	
    # 라이브러리 설정
    target_link_libraries(HLSL2021
    PRIVATE
    	dxcompiler.lib
    )
    
    # dxcompiler.dll을 실행 파일 경로로 복사
    add_custom_command(TARGET HLSL2021 POST_BUILD
    	COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/ThirdParty/DXC/bin/x64/dxcompiler.dll" "${CMAKE_BINARY_DIR}/HLSL2021/\\$\\(Configuration\\)/dxcompiler.dll")
    
    set_target_properties(HLSL2021 PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/HLSL2021")
    

    이제 셰이더를 컴파일을을 위한 코드를 작성할 준비가 되었습니다. 이제 예제 코드를 통해 컴파일 과정을 살펴보겠습니다. 소스 코드 파일은 \HLSL2021\Source\Private\main.cpp입니다.

    먼저 컴파일러 Com 객체를 생성할 필요가 있습니다.

    ComPtr<IDxcCompiler3> compiler;
    
    // Create directX shader compiler
    {
    	HRESULT hr = DxcCreateInstance( CLSID_DxcCompiler, IID_PPV_ARGS( compiler.GetAddressOf() ) );
    	if ( FAILED( hr ) )
    	{
    		std::cout << "DirectX Shader Compiler creation failed";
    		return 1;
    	}
    }
    

    IDxcCompiler 객체의 생성에 성공하면 컴파일을 위한 인자를 설정할 필요가 있습니다. DXC 컴파일 인자는 여러 가지가 있으나 여기서는 필요한 최소한으로 필요한 인자만 설정하고 있습니다. 추가적인 인자에 대해 알고 싶으시다면 Reference 1. 을 참고 바랍니다.

    // Setup compile arguments
    std::vector<const wchar_t*> args;
    
    // Entry point
    args.emplace_back( L"-E" );
    args.emplace_back( L"main" );
    
    // HLSL version(2016, 2017, 2018, 2021). Default is 2021
    // 2024년 현재 최신 DXC에서 기본 설정이 2021이므로 생략 가능
    // args.emplace_back( L"-HV" );
    // args.emplace_back( L"2021" );
    
    // Target profile
    args.emplace_back( L"-T" );
    // Assuming all shader files are compute shaders in this sample
    args.emplace_back( L"cs_6_8" );
    

    가장 중요한 부분은 -HV로 여기에 2021을 설정하는 것으로 HLSL 2021을 사용하게 됩니다.

    이제 준비한 인자와 셰이더 파일을 가지고 IDxcCompiler의 Compile 함수를 호출하면 셰이더 파일을 컴파일할 수 있습니다.

    DxcBuffer buffer = {
    	.Ptr = shaderFileBinary.data(), // 파일에서 읽어 메모리에 저장해 놓은 셰이더 파일 내용의 시작
    	.Size = shaderFileBinary.size(), // 셰이더 파일 내용의 길이
    	.Encoding = DXC_CP_ACP
    };
    
    ComPtr<IDxcResult> results;
    compiler->Compile( &buffer
    	, args.data()
    	, static_cast<UINT32>( args.size() )
    	, nullptr
    	, IID_PPV_ARGS( results.GetAddressOf() ) );
    
    HRESULT hr = S_OK;
    results->GetStatus( &hr ); // 성공 여부 가져오기
    
    if ( FAILED( hr ) )
    {
    	std::cout << "Failed to compile shader file - " << shaderFilePath << std::endl;
    	return;
    }
    
    ComPtr<IDxcBlob> compiledBinary = nullptr;
    ComPtr<IDxcBlobUtf16> shaderName = nullptr;
    // 셰이더 바이트 코드로 출력
    results->GetOutput( DXC_OUT_OBJECT, IID_PPV_ARGS( compiledBinary.GetAddressOf() ), shaderName.GetAddressOf() );
    

    주요 변경점

    이제 HLSL 2021 셰이더 파일을 컴파일할 모든 준비가 끝났습니다. 이제 HLSL 2021에서는 이전과 어떤 부분이 달라졌는지 살펴보도록 하겠습니다.

    사용자 정의 데이터형에 대한 엄격한 캐스팅

    HLSL 2021 이전에는 동일한 멤버 레이아웃을 가진 사용자 정의 데이터형의 경우 동일한 타입으로 취급되어 서로 간의 자유로운 캐스팅이 가능했습니다. 따라서 다음과 같은 코드에서 GetLuma4에 대한 호출은 LinearRGB와 LinearYCoCg 간의 자유로운 캐스팅이 가능하기 때문에 어떤 함수를 호출해야 할지 모호해집니다.

    struct LinearRGB
    {
        float3 RGB;
    };
    
    struct LinearYCoCg
    {
        float3 YCoCg;
    };
    
    float GetLuma4(LinearRGB In)
    {
        return In.RGB.g * 2.0 + In.RGB.r + In.RGB.b;
    }
    
    float GetLuma4(LinearYCoCg In)
    {
        return In.YCoCg.x;
    }
    
    [numthreads(1, 1, 1)]
    void main()
    {
        LinearYCoCg v = { { 0.0, 0.0, 0.0 } };
        GetLuma4(v);
    }
    

    HLSL 2021에서는 사용자 정의 데이터형에 대한 엄격한 캐스팅을 통해 모호함을 해결하기 때문에 셰이더 코드는 정상적으로 컴파일됩니다.

    논리 연산자 단락 평가(Logical Operator Short Circuiting)

    HLSL 2021에서는 C와 동일한 논리 연산자 단락 평가가 도입됩니다. 이 말인즉슨 && 연산자의 첫 번째 피연산자가 false라면 두 번째 피연산자는 평가되지 않습니다. 마찬가지로 || 연산자의 첫 번째 피연산자가 true라면 두 번째 피연산자는 평가되지 않습니다. 이에 따라서 두 번째 피연산자가 사이드 이펙트를 일으키는 경우 이전과 동작이 달라질 수 있습니다.

    struct Doggo {
      bool isWagging;
    
      bool wag() {
        isWagging = !isWagging;
        return !isWagging;
      }
    
      void bark() {
        // woof!
      }
    };
    
    [numthreads(1,1,1)]
    void main() {
      Doggo Fido = {false};
      for (int i = 0; i < 10; ++i) {
        if (Fido.isWagging && Fido.wag())
          Fido.bark();
      }
    }
    

    HLSL 2021에선 Fido의 wag와 bark함수는 호출되지 않지만 HLSL 2021 이전에는 매 루프마다 wag함수가 호출되고 두 번째 루프부터 bark함수가 호출됩니다.

    논리 연산자 단락 평가와 더불어 HLSL 2021에서는 스칼라 타입만 논리 연산자에 사용할 수 있습니다. 따라서 다음과 같은 셰이더 코드는 컴파일이 실패합니다.

    [numthreads(1, 1, 1)]
    void main()
    {
        int3 X = { 1, 1, 1 };
        int3 Y = { 0, 0, 0 };
        
        bool3 Cond = X && Y;
        bool3 Cond2 = X || Y;
        int3 Z = X ? 1 : 0;
    }
    

    HLSL 2021에서는 and와 or, select를 사용하여 이전 동작을 대체할 수 있습니다.

    bool3 Cond = and(X, Y);
    bool3 Cond2 = or(X, Y);
    int3 Z = select(X, 1, 0);
    

    C++ for문 범위 규칙

    HLSL 2021부터 for문 안에서 선언한 변수는 for문이 포함된 범위에 귀속됩니다. 따라서 다음과 같은 코드는 HLSL 2021부터 컴파일되지 않습니다.

    [numthreads(1, 1, 1)]
    void main()
    {
        for (int i = 0; i < 10; ++i)
        {
        }
        i = 100;
    }
    

    템플릿 함수와 데이터 타입

    HLSL 2021에서는 구조체와 클래스에 대해서 C++과 유사한 템플릿을 지원합니다. HLSL 2021의 템플릿은 완전 특수화 및 템플릿 매개변수 추론(가능한 경우)을 지원합니다.

    C++과 마찬가지로 template 키워드를 사용해서 템플릿을 사용할 수 있습니다.

    template<typename T>
    void increment(inout T X)
    {
        X += 1;
    }
    

    또한 다음과 같이 명시적인 완전 특수화를 지원합니다.

    template<>
    void increment(inout float3 X) {
      X.x += 1.0;
    }
    

    그리고 다음과 같이 부분 특수화도 지원합니다.

    template<typename T, typename U>
    struct Partial
    {
        T first;
        U second;
    };
    
    template<typename T>
    struct Partial<T, int>
    {
        T first;
    };
    
    [numthreads(1, 1, 1)]
    void main()
    {
        Partial<float, float> P1;
        P1.first = 0.f;
        P1.second = 0.f;
    
        Partial<float, int> P2;
        P2.first = 0.f;
        /* 부분 특수화가 지원되기 때문에 아래 주석을 풀면 컴파일 에러 */
        // P2.second = 0;
    }

    템플릿 함수는 인수 유형이 템플릿 매개변수를 유추할 수 있는 경우 일반 함수 호출 구문을 사용하여 호출할 수 있습니다.

    [numthreads(1, 1, 1)]
    void main()
    {
        int X = 0;
        int3 Y = { 0, 0, 0 };
        float3 Z = { 0.0, 0.0, 0.0 };
        increment(X);
        increment(Y);
        increment(Z);
    }
    

    만약 템플릿 매개변수를 유추할 수 없는 경우 C++처럼 필요한 매개변수를 호출할 때 전달할 수 있습니다.

    template<typename V, typename T>
    V cast(T X) {
      return (V)X;
    }
    
    [numthreads(1,1,1)]
    void main() {
      int X = 1;
      uint Y = cast<uint>(X);
    }
    

    멤버 연산자 오버로딩

    HLSL에서는 멤버 연산자 오버로딩을 지원합니다. 대부분 C++과 유사하나 포인터와 참조형을 지원하지 않는 HLSL의 특징으로 인해 몇 가지 지원되지 않는 연산자(*, &, ->, =, +=)가 있습니다.

    추가로 HLSL에서는 동적 할당을 지원하지 않기 때문에 new, delete에 대한 오버로딩도 지원하지 않습니다.

    struct Pupper
    {
        int Fur;
    
        Pupper operator+(int MoarFur)
        {
            Pupper Val = { Fur + MoarFur };
            return Val;
        }
    
        bool operator<=(int y)
        {
            return Fur <= y;
        }
    
        operator bool() 
        {
            return Fur > 50;
        }
    };
    
    [numthreads(1, 1, 1)]
    void main(uint tidx : SV_DispatchThreadId)
    {
        Pupper y = { 0 };
        for (Pupper x = y; x <= 100; x = x + 1)
        {
            if ((bool) x)
                y = y + 1;
        }
    }
    

    HLSL 2021에서 재정의할 수 있는 연산자 전체 목록은 다음과 같습니다.

    산술 연산자  +, –, *, /, %
    비트 연산자 &, |, ^ 
    논리, 비교 연산자 &&, ||, !=, ==, <=, >=, <, >
    추가 연산자 함수 호출 (), 배열 접근 [], 캐스팅 <type>

    데이터형의 비트 필드 멤버

    CPU 코드에서 데이터 구조를 더 쉽게 활용하고 더 유연한 정수 크기를 제공하기 위해 HLSL 2021에서는 구조체 멤버에 대한 비트 필드를 사용할 수 있습니다. 이를 통해 구조체 내부의 정숫값에 사용할 비트 수를 임의의 수로 지정할 수 있습니다.

    struct ColorRGBA
    {
        uint R : 8;
        uint G : 8;
        uint B : 8;
        uint A : 8;
    };
    
    [numthreads(1, 1, 1)]
    void main()
    {
        ColorRGBA RGBA = (ColorRGBA)0;
        RGBA.R = 255;
    }
    

    비트 필드는 기본 정수 유형이어야 하지만 지정된 유형의 크기보다 적은 수의 비트도 가능합니다.

    마치며

    준비한 내용은 여기까지입니다. 이 글에서 다룬 모든 코드 샘플은 아래의 깃 저장소에 있으니 세부 코드 및 실제 컴파일 동작을 확인하고 싶으신 분은 참고 바랍니다.

    https://github.com/xtozero/HLSL2021

    Reference

    1. https://simoncoenen.com/blog/programming/graphics/DxcCompiling
    2. https://github.com/microsoft/DirectXShaderCompiler/wiki/HLSL-2021
    3. https://devblogs.microsoft.com/directx/announcing-hlsl-2021/
    4. https://devblogs.microsoft.com/directx/hlsl-2021-migration-guide/

    '개인 프로젝트' 카테고리의 다른 글

    GPU Query  (0) 2024.09.19
    오브젝트 피킹 (Object Picking)  (0) 2024.07.13
    Specular IBL  (0) 2024.05.24
    Bindless Resource  (0) 2024.03.20
    빛 전파 볼륨 (Light Propagation Volume)  (0) 2024.01.13
Designed by Tistory.