728x90

DirectCompute의 스레드 적용 모형

DirectCompute에 관해 가장 먼저 살펴볼 것은 그 스레드 적용 모형과 실행 모형이다. GPU가 많은 수의 처리 코어들로 이루어져 있기 때문에 병렬 알고리즘 처리에 아주 적합하다는 점은 앞에서 이미 언급했다. 작업을 수행할 처리기가 아주 많기 때문에, 특정 알고리즘을 다수의 스레드들에 효과적으로 대응시키기 위한 적당한 방법론을 갖출 필요가 있다. 

전통적인 CPU 기반 알고리즘에 쓰이는 전형적인 다중스레드는 스레드들이 각자 개별적으로 실행되되 공유 메모리 공간과 수동적인 동기화를 통해서 서로 의사소통하는 방법을 사용한다. 이는 다중 프로세서 시스템들에서 오랫동안 쓰였다.

그러나 이러한 모형은 수천 개의 스레드가 동시에 돌아가야 하는 처리 패러다임에 그리 잘 맞지 않는다. DirectCompute는 일반성과 사용 편의의 균형을 도모하는 또 다른 종류의 스레드 적용 및 실행 모형을 사용한다. 이 모형에서는 스레드들을 자료 요소에 사상(대응)시키기가 더 쉽다. 이 덕분에 하나의 처리 과제를 더 작은 여러 조각으로 분할해서 GPU에서 실행하는 것이 수월해진다.


핵(Kernel) 기반 처리

계산 셰이더 단계는 핵(Kernel) 기반 처리 시스템을 구현한다. 계산 셰이더 프로그램 자체는 하나의 함수이며, 그 함수는 일종의 핵함수(Kernel Function)처럼 작용한다. 작업의 단위는 알고리즘마다 다르지만, 현재 적재된 핵의 각 인스턴스가 각각의 단일한 입력 자료 집합을 처리하는 식으로 알고리즘이 실행된다는 점은 동일하다. 계산 셰이더에 주어지는 입력자료는 계산 셰이더 단계에 연결된 자원에서 온 것이다. 이러한 구조에서는 하나의 알고리즘이 GPU상의 수천 개의 스레드들에서 실행되도록 프로그래밍하기가 아주 쉽고 직관적이다. 모든 스레드가 같은 핵(Kernel)을 실행하므로, 스레드들의 행동을 일일이 동기화하는 데 힘을 빼는 대신 원하는 자료를 각자 독립적으로 처리될 자료 항목들로 분할하는 방법을 찾는 데 주력하면 된다.


작업의 배분

작업을 시작할 때 사용할 수 있는 메서드는 ID3D11DeviceContext::Dispatch() 또는 ID3D11DeviceContext::DispatchIndirect()이다.

이 함수들은 렌더링 파이프라인의 여러 그리기 메서드(Draw로 시작하는 함수)들에 비견할 수 있다. 

Dispatch메서드는 부호 없는 정수 매개변수 3개를 받는데 이 세 매개변수는 원하는 처리 핵의 실행을 '배분(dispatch)' 할 스레드들의 그룹의 개수를 결정한다. 좀 더 구체적으로 이 세 매개변수는 인스턴스화할 스레드 그룹들을 담는 3차원 배열의 x, y, z 차원 크기로, 유효한 범위는 셋 모두 1에서 65535이다. 예를 들어 응용 프로그램이 Dispatch(4, 6, 2)를 호출했다면 4 x 6 x 2 = 48개의 스레드 그룹들이 만들어진다. 그 배열 안에서 각 스레드 그룹은 세 차원 색인(범위는 0 ~ 차원 크기 - 1)의  고유한 조합으로 이루어진 식별자로 식별된다.

이러한 배분 메서드를 호출할 때 지정하는 것은 인스턴스화할 스레드 그룹의 개수이지 개별 스레드들의 개수가 아님을 주의하기 바란다. 한 그룹당 인스턴스화될 스레드들의 개수는 계산 셰이더 프로그램의 주 함수 앞에서 numthreads라는 HLSL 함수 특성으로 지정한다. 배분 메서드에서처럼 이 함수 특성도 3차원 배열의 세 차원의 크기를 지정하나, 그 배열은 스레드 그룹들이 아니라 실제 스레드들을 담는 것이다. 

각 차원의 크기는 셰이더 모형에 따라 다른데, cs_5_0의 경우 x와 y가 1이상이어야 하고 z는 반드시 1 ~ 64 범위 이어야 한다. 또한 그룹의 전체 스레드 개수(X * Y * Z)가 1024를 넘으면 안된다.


[numthreads(10, 10, 2)]

이 예에서는 각 스레드 그룹마다 총 10 x 10 x 2 = 200개의 스레드가 인스턴스화된다. 이 스레드들의 개별 스레드 역시 차원 색인 ( 0 ~ 차원 크기 - 1) 세 개의 고유한 조합으로 이루어진 식별자로 식별된다. 앞에서처럼 Dispatch(4, 6, 2)라고 호출한 경우, 계산 셰이더 단계 전체적으로 인스턴스화되는 스레드는 48 x 200 = 6400개이다.


스레드 식별 체계

앞에서 처럼 총 6400개의 스레드가 실행된다면 이 스레드들은 자신이 처리할 자료를 어떻게 알게 될까? 모든 인스턴스가 같은 핵(셰이더 프로그램)을 실행하므로, 셰이더 프로그램 소스 코드 자체에서 자료를 지정할 수는 없다. 따라서 처리할 자료 집합을 스레드에게 알려주는 다른 어떤 메커니즘이 필요하다.

앞에서 이야기한 기하학적 스레드 조직화 방식이 바로 그러한 메커니즘의 기반이 된다. 스레드들을 그렇게 조직화하는 데에는 다 이유가 있었던 것이다. 앞에서 보았듯이, 하나의 배분 호출에 관여하는 스레드 그룹들 중 특정 스레드 그룹을 3차원 좌표 형태로 지정하는 것은 아주 간단한 문제이며, 특정 스레드 그룹의 특정 스레드를 지정하는 것 역시 마찬가지로 간단하다.

결과적으로, 한 배분 호출의 모든 스레드를 각각 고유한 식별자로 간단하게 식별할 수 있다.

셰이더 프로그램을 작성할 때 개발자는 셰이더 함수의 입력 매개변수로 쓰일 일단의 시스템 값 의미소들을 지정할 수 있는데, 그런 시스템 값 의미소들 중에는 현재 실행되는 셰이더 프로그램 인스턴스(스레드)를 식별하는 데 사용할 수 있는 것들이 있다. 


SV_GroupID : 이 배분 호출의 스레드 그룹들 중 현재 스레드가 속한 그룹의 3차원 식별자(uint3)

SV_GroupThreadID : 그 스레드 그룹 안에서의 현재 스레드의 3차원 식별자(uint3)

SV_DispatchThreadID : 전체 배분 안에서의 현재 스레드의 3차원 식별자(uint3)

SV_GroupIndex : 현재 스레드가 속한 스레드 그룹의 3차원 식별자를 1차원으로 직렬화한 색인(uint)


각 스레드의 실행마다 이러한 식별 정보가 주어지므로, 개발자는 이들을 이용해서 입력 자료 중 현재 스레드가 처리할 부분을 결정하도록 셰이더 프로그램을 작성하면 된다. 



이 코드가 하는 일은 1차원 버퍼의 자원의 값을 두 배로 증가 시키는 계산 셰이더이다.

이 코드는 스레드 그룹의 크기를 numthreads 함수 특성으로 선언한다. 그 다음 부분은 계산 셰이더의 처리 핵에 해당하는 주 함수이다. 이 함수의 매개 변수는 앞에서 말한 스레드 식별용 시스템 값 의미소 특성들 중 하나이다. 


728x90
728x90

앞에서 대략적인 렌더링 파이프라인에 대해 보았지만, DirectX11의 파이프라인에는 좀 더 광범위한 분야에 적용할 수 있는 유연한 계산을 위한 파이프라인 단계가 있다. 그 단계를 이용하면 GPU를 광선 추적이나 물리 시뮬레이션 같은 다양한 응용 분야에서 사용할 수 있고, 인공지능 계산에도 사용할 수 있다. 그것은 계산 셰이더(Compute Shader) 단계로 흔히 DirectCompute라고 부르는 기술이 바로 이 단계에서 구현된다.


DirectCompute

DirectCompute는 GPU가 제공하는 대규모 병렬 계산 능력을 통상적인 래스터 기반 렌더링 이외의 분야에 활용하기 위한 새로운 처리 패러다임이다. GPU는 병렬로 작동하는 아주 많은 수의 소규모 처리기들로 이루어져 있기 때문에 크게 병렬화할 수 있는 계산 과제에 아주 적합하다.

DirectCompute는 Direct3D 11 처리 환경에 직접 내장되어 있으며, Direct3D 렌더링 API와 기존 프레임워크의 상당 부분을 공유한다. 덕분에 DirectX11에 익숙한 응용 프로그램 개발자라면 응용 프로그램의 설정과 실행에 필요한 정도의 API 사용법은 따로 배우지 않아도 될 정도이다.


계산 셰이더 단계

계산 셰이더의 전체적인 사용 방법은 개념적으로 다른 프로그램 가능 셰이더 단계와 동일하다.

자원들을 순서 없는 접근 뷰(UAV)를 이용해서 계산 셰이더 단계에 연결할 수도 있다. 그러나 계산 셰이더 단계는 다른 프로그램 가능 파이프라인 단계들과 근본적으로 다르다. 가장 큰 차이점은 다른 파이프라인 단계의 출력을 입력으로 받지 않고, 출력을 다른 파이프라인 단계에 넘겨주지도 않는다. 몇 가지 시스템 값 의미소들은 입력 매개변수로 받을 수 있지만, 이는 셰이더 프로그램에서 사용할 수 있는 특성 형태의 자료에만 국한된다. 그 외의 모든 자료 입 · 출력은 자원을 통해서만 일어난다.

이러한 구조 때문에 계산 셰이더에서는 하나의 프로그램 안에서 알고리즘을 완결적으로 구현해야 한다. 




728x90
728x90

자원 뷰 (Resource View)

자원 뷰는 자원을 파이프라인에 묶는 데 사용되는 객체이다. 


자원 뷰의 종류

자원 뷰는 4개의 종류가 있다. 


렌더 타겟 뷰 ( ID3D11RenderTargetView )

깊이/스텐실 뷰 ( ID3D11DepthStencilView )

셰이더 자원 뷰 ( ID3D11ShaderResourceView )

순서 없는 접근 뷰 ( ID3D11UnordereAccessView )


같은 자원이라도 어떤 자원 뷰로 연결되는가에 따라 자원의 용도가 달란진다. 


렌더 타겟 뷰 ( ID3D11RenderTargetView )는 렌더링 파이프라인의 출력을 받을 자원을 연결하는 데 쓰인다. 전통적으로 렌더 타겟은 2차원 텍스처이나, 다른 종류의 자원을 렌더 타게으로 연결하는 것도 가능하다. 렌더 타겟 뷰의 구성 옵션으로는 자원의 DXGI 형식이 있으며, 그 외에도 자원의 종류에 따라 다양한 구성 옵션들이 존재한다. 렌더 대상 뷰는 또한 자원의 일부분을 파이프라인에 노출시키는 다양한 메서드들도 제공한다.


깊이/스텐실 뷰 ( ID3D11DepthStencilView )도 렌더 타겟 뷰처럼 렌더링 파이프라인의 출력을 받는 자원을 위한 것이다. 차이는, 렌더 대상 뷰가 색상 값들을 담는 버퍼를 위한 것인 반면 깊이/스텐실 뷰는 깊이와 스텐실 값들을 담는 버퍼를 위한 것이라는 점이다. 깊이/스텐실 버퍼 자원은 빈번하게 쓰이는 렌더링 연산이 깊이 판정과 스텐실 판정을 수행하는 데 사용되며, 이 때문에 파이프라인의 효율성에 아주 중요한 요인이 된다.


셰이더 자원 뷰 ( ID3D11ShaderResourceView )는 파이프라인의 프로그램 가능 셰이더 단계가 자원을 읽을 수 있게 한다. 이 뷰는 예전에 픽셀 셰이더에서 텍스처가 하던 역할, 즉 셰이더 프로그램 안에서 읽고 사용할 수는 있지만 기록하지는 못하는 자료에 해당하는 것이다.


순서 없는 접근 뷰 ( ID3D11UnordereAccessView )는 셰이더 프로그램안에서 자원을 읽음과 동시에 쓰기도 할 수 있다. 게다가, 출력 장소가 미리 정해져 있지 않기 때문에 셰이더 프로그램 안에서 자원 안의 임의의 위치에 scatter 연산을 수행하는 것도 가능하다. 이 자원 뷰는 픽셀 셰이더 단계와 계산 셰이더 단계에만 사용할 수 있다.


자원 뷰의 생성

자원 뷰의 생성에도 ID3D11Device가 사용된다. 이 인터페이스는 각 자원 종류별 생성 메서드를 제공하는데, 이 생성 메서드들은 모두 같은 패턴을 따른다. 자원 뷰 생성 메서드들은 모두 세 개의 매개변수를 받는데, 첫 매개변수는 자원 뷰를 적용할 자원(예를 들면 Texture)을 가리키는 포인터이다. 둘째 매개변수는 자원 뷰를 서술하는 구조체를 가리키는 포인터로, 그 구조체는 해당 종류의 자원 뷰를 위한 모든 옵션을 담는다. 셋째이자 마지막 매개변수는 해당 종류의 자원 뷰 객체에 대한 포인터로, 호출이 성공한 경우 그 포인터가 가리키는 곳에 자원 뷰 객체가 만들어진다.


자원 뷰의 사용

자원 뷰의 구조체는 사용 용도가 다르기 때문에 정의된 구조체도 모두 다르다. 

각 해당 자원에 대해 자원 뷰를 이용하여 접근하면 된다.


자원 뷰의 해제

자원과 마찬가지로 COM 인터페이스를 따라가므로, 참조를 해제해야 한다.

728x90
728x90

자원의 개요 

자원은 앞에서 말햇듯이 크게 두 가지의 종류로 나뉘는데, 버퍼(Buffer)텍스처(Texture)이다.




위의 그림은 자원 클래스들이 모두 ID3DResource라는 단일 공통 기반 클래스로부터 파생된 것임을 보여준다.


이러한 구조는 자원이라는 것이 결국은 파이프라인에 부착(연결)할 수 있고, 입력 또는 출력에 쓰이는 메모리 블록이라는 점을 생각하면 당연한 것이다. 다른 말로 하면 자원은 GPU가 사용하고 조작하도록 마련된 메모리 블록일 뿐이라는 것이다.


자원의 생성

모든 메모리 자원의 생성은 ID3D11Device 인터페이스가 책임진다. 생성된 자원을 파이프라인에 직접 연결할 수도 있고 자원 뷰를 통해서 부착할 수도 있다. 일단 연결이되면 이후 파이프라인 실행 과정 안에서 자원이 실제로 사용된다. 


자원의 생성에 쓰이는 ID3D11Device의 메서드는 자원의 종류마다 다르지만, 모두 동일한 일반 패턴을 따른다.


모든 자원 생성 메서드는 세 개의 매개변수를 받는다. 

첫 매개변수는 자원 생성에 관한 모든 옵션을 지정하는 구조체이다. 이를 자원 서술(Resource Description)이라고 부른다. 자원 종류마다 자원 서술 구조체가 다르다. 그러나 이 구조체들은 모두 동일한 목적(생성된 자원의 원하는 특성들을 정의)으로 사용된다.


둘째 매개변수는 D3D11_SUBRESOURCE_DATA 구조체를 가리키는 포인터인데, 이 구조체는 자원에 적재할 초기 자료를 제공하는 데 쓰인다. 예를 들어 정적 정점 자료를 담을 버퍼 자원을 만드는 경우 이 구조체를 이용해서 모형의 정점 자료를 버퍼 자원에 채워 넣을 수 있다. 


세번째 매개변수는 자원 종류에 맞는 자원 인터페이스를 가리키는 포인터의 포인터로, 자원 생성이 성공하면 해당 자원을 가리키는 포인터가 이 매개변수에 설정된다.




자원 용도 플래그

자원이 존재하는 '메모리'는 비디오 카드일 수도 있고, 시스템의 주 메모리일 수도 있다. 또한 Direct3D 실행 모듈이 자원을 비디오 카드 메모리에서 시스템 메모리로 또는 그 반대 방향으로 이동할 수도 있다. 때문에 응용 프로그램은 반드시 자원의 사용 방식에 대한 자신의 '의도'를 용도 명세 필드를 통해서 명시적으로 밝혀야 한다. 


enum D3D11_USAGE

{

D3D11_USAGE_DEFAULT,

D3D11_USAGE_IMMUTABLE,

D3D11_USAGE_DYNAMIC,

D3D11_USAGE_STAGING

}


 자원 용도

DEFAULT 

DYNAMIC 

 IMMUTABLE

 STAGING

 GPU 읽기 

 YES 

YES 

YES 

YES

GPU 쓰기

YES

 

 

 YES 

 CPU 쓰기 

 

 

 

YES

CPU 읽기

 

YES 

 

YSE


D3D11_USAGE_DEFAULT(기본 용도) : 표에서 보듯이 기본 용도 자원은 GPU의 읽기 쓰기만 가능하고 CPU의 접근은 모두 거부한다. 이 용도는 렌더 타겟 텍스처나 스트림 출력 정점 버퍼가 있다.


D3D11_USAGE_IMMUTABLE(불변 용도) : 가장 간단한 사용 패턴으로 오직 GPU가 읽을 수만 있다. GPU와 CPU의 쓰기를 모두 거부한다. 이 용도는 정적인 상수나, 정점, 색인 버퍼를 들 수 있다.


D3D11_USAGE_DYNAMIC(동적 용도) : CPU의 쓰기가 가능한 두 가지 용도 중 하나이다. GPU는 읽기만 가능하다. 이 사용 용도는 CPU가 자원의 내용을 생산하고 GPU가 자원을 소비한다. 

이 용도는 상수 버퍼에서 사용하면 좋다.


D3D11_USAGE_STAGING(예비 용도) : 이 예비 용도는 특별한 종류의 사용 패턴을 제공한다. 앞에서 말한 세 가지 용도는 렌더링 수행을 위한 전형적인 자원 사용 시나리오들에 해당하는 것이다. 그러나 GPU에서 자료를 계산, 조작하고 그것을 저장이나 추가 조사를 위해 CPU로 읽어 들어야 하는 경우도 있다. 그런 목적의 응용 프로그램에서는 다른 사용 패턴들에 CPU의 읽기 접근을 강제로 허용하는 대신 이 예비 용도의 자원을 중간 계산을 위한 장소로 활용하면 된다.


CPU 접근 플래그

자원의 용도를 지정했다면, 다음으로는 자원에 대한 CPU의 접근 방식을 지정해야 한다. 이 플래그는 CPU에만 국한 된다. 이 열거형의 두 값을 비트 단위 OR로 결합해서 사용하면 읽기와 쓰기를 모두 허용 할 수도 있다. 하지만 자원 용도 플래그에 맞추어서 설정해야만 한다.


enum D3D11_CPU_ACCESS_FLAG

{

D3D11_CPU_ACCESS_WRITE,

D3D11_CPU_ACCESS_READ

}


연결 플래그

자원 서술 구조체의 또 다른 공통 필드로 연결 플래그(Bind Flag)가 있다. 이 필드는 자원을 파이프라인의 어디에 연결할 것인지를 나타낸다. 이 값들을 비트 단위 OR로 결합함으로써 연결 가능 장소를 여러개 지정하는 것도 가능하다. 이 플래그를 제대로 지정하지 않고 자원을 생성하면 나중에 응용 프로그램이 자원을 파이프라인에 연결하려 할 때 오류가 발생한다.


enum D3D11_BIND_FLAG 

{

D3D11_BIND_VERTEX_BUFFER,

D3D11_BIND_INDEX_BUFFER,

D3D11_BIND_CONSTANT_BUFFER,

D3D11_BIND_SHADER_RESOURCE,

D3D11_BIND_STREAM_OUTPUT,

D3D11_BIND_RENDER_TARGET,

D3D11_BIND_DEPTH_STENCIL,

D3D11_BIND_UNORDERED_ACCESS,

}


자원을 파이프라인에 연결할 수 있는 지점은 총 여덟 가지이다. 처음 둘은 정점 버퍼와 색인 버퍼에 해당하는 것으로, 파이프라인의 기하구조 자료를 공급하기 위한 입력 조립기 단계에 부착할 자원에 쓰인다. 반면 여섯 번째와 일곱번째의 렌더 타겟 플래그와 깊이/스탠실 버퍼 플래그는 파이프라인의 렌더링 결과를 받는 출력 병합기 단계에 연결할 자원을 위한 것이다. 다섯 번째의 스트림 출력 플래그 역시 파이프라인으로부터의 출력을 위한 것이나, 래스터화된 이미지 자료가 아니라 기하구조 자료를 받는다는 점이 다르다. 반면 D3D11_BIND_CONSTANT_BUFFER, D3D11_BIND_SHADER_RESOURCE, 

D3D11_BIND_UNORDERED_ACCESS는 셰이더 프로그램 안에서 사용한다.


위의 그림에서 연결 플래그가 연결되는 부분을 확인 할 수 있다.


자원 해제

자원들은 모두 COM 인터페이스를 구현하는 객체의 형태이며, 따라서 해당객체의 계통 구조 안에는 IUnknown 인터페이스가 존재한다. 이는 자원들이 참조 계수 방식으로 관리된다는 뜻이며, 따라서 응용 프로그램이 자원을 다 사용하고 난 후에는 반드시 참조를 해제해 주어야 한다.


728x90
728x90

DirectX11을 사용할 때 반드시 이해해야 하는 두 가지 주된 인터페이스로 장치(Device)장치 문맥(Device Context)가 있다.


쉽게 말하면 파이프라인이 사용할 자원들을 생성할 때는 장치(Device) 인터페이스가 사용되고, 생성된 자원들을 실제로 사용하거나, 파이프라인 자체를 조작할 때에는 장치 문맥(Device Context) 인터페이스가 사용된다.


DirectX11의 장치 인터페이스(ID3D11Device)는 셰이더 프로그램 객체, 자원, 상태 객체, 질의 객체 등의 생성을 위한 여러 메서드들을 제공한다. 또한 여러 하드웨어 기능들의 사용 가능 여부를 점검하는 메서드들과 진단 및 디버깅 관련 메서드들도 제공한다.


장치 문맥(ID3D11DeviceContext)은 장치로 생성한 자원이나 상태 객체를 파이프라인에 묶는 데 쓰인다. 또한 장치 문맥은 렌더링 파이프라인과 계산 파이프라인의 실행을 제어하는 수단을 제공하며, 장치로 생성한 자원을 조작하는 수단도 제공한다. 응용 프로그램은 인터페이스(ID3DDeviceContext)를 통해서 장치 문맥에 접근한다. DirectX11은 다중 스레드 렌더링을 좀 더 잘 지원하기 위해서 두 종류의 장치 문맥을 제공하는데, 하나는 즉시 문맥(Immediate Context)과 또 하나는 지연 문맥(Deferred Context)이다.


즉시 문맥(Immediate Context)은 파이프라인에 직접 연결되는 통로라고 생각할 수 있다. 이 문맥에서 어떤 메서드를 호출하면 그 호출은 DirectX 11 실행시점 모듈에 '즉시' 제출되어서 해당 명령이 구동기에서 실행된다. 하나의 응용 프로그램은 단 하나의 즉시 문맥만 사용할 수 있으며, 이 즉시 문맥은 장치가 생성될 때 함께 만들어진다. 이 문맥을 파이프라인의 모든 구성요소와 직접 상호작용하기 위한 인터페이스라고 생각하면 된다. 이 문맥은 반드시 주 렌더링 스레드에서, GPU에 대한 1차적인 인터페이스로서 사용해야 한다.


지연 문맥(Deferred Context)는 2차적인 장치 문맥으로 주 렌더링 스레드 이외에 2차적인 스레드들이 보낸 일련의 명령들을 스레드에 안전하게 기록하는 메커니즘을 제공한다. 이 문맥으로 소위 '명령 목록(Command List)' 객체를 생성하고, 나중에 주 스레드에서 그 명령 목록을 '재생'하는 것이 가능하다. 또한 DirectX 11은 여러 스레드들에서 자원들을 비동기적으로 생성할 수 있다.


그림 - Immediate Context와 Deferred Context의 관계


728x90
728x90

컴퓨터 프로그램을 사용하여 Model 또는 Scene으로 부터 영상을 만들어 내는 과정을 Rendering이라고 하고, 한 데이터 처리 단계의 출력이 다음 단계의 입력으로 이어지는 형태로 연결된 구조를 Pipeline이라고 한다.


그림 - Pipeline


즉, Rendering Pipeline 이라는 것은 메모리 자원들을 GPU로 처리해서 하나의 렌더링된 이미지를 만들어 내는 데 쓰이는 메커니즘을 뜻한다. 


DirectX11의 Rendering Pipeline을 대략적으로 보면 다음과 같다.

그림 - 대략적인 Rendering Pipeline


대략적인 Rendering Pipeline의 그림을 보면 우선 Rendering 과정이 초록색과 빨간색으로 나뉘어져 있다.

초록색은 Fixed Function(고정 기능 단계)로 미리 정해진 특정한 연산들만 수행이 가능하고, 실행 시점에서 기능은 바꿀 수 없지만, 상태 객체라는 개념을 이용하여 설정들의 변경이 가능하며, 이 Fixed Function의 단계는 임의로 실행을 하지 않는 방법은 없다.

빨간색은 Programmable(프로그래밍 가능 단계)로 HLSL(High Level Shading Language, 고수준 셰이딩 언어)로 셰이더 프로그래밍이 가능한 단계이다. 물론 특정 Programmable 단계를 비활성화 하는 것도 가능하다.


Rendering Pipeline의 진입점은 입력 조립기(Input Assembler)이다. 이 단계는 자원들로 부터 입력 자료를 읽어 들여서 파이프라인의 이후 단계들이 사용할 정점들을 긁어 모으는 작업을 한다. 또한 입력 자원들에 기초해서 정점들 사이의 연결성을 파악하고 바람직한 렌더링 구성을 결정한다. 이 단계는 취합한 정점들과 기본도형(Primitive) 연결성 정보를 다음 단계인 정점 셰이더로 넘겨준다.


다음 단계인 정점 셰이더(Vertex Shader) 단계는 입력 조립기가 넘겨준 정점 자료의 정점들을 한 번에 하나 씩 처리한다. 각 입력 정점마다 현재 지정된 정점 셰이더 프로그램이 적용된다. 이 정점 셰이더에서 하는 일은 저의 경험으로 비추어 볼때 대체로 변환 행렬을 적용하거나, 정점 별 조명계산 수행 등 Pixel Shader에서의 불필요한 연산들을 미리 할 때 사용한다.


다음 세 단계는 하드웨어의 테셀레이션(Tessellation) 기능을 활용하기 위해서 최근에 파이프라인에 추가된 것으로 반드시 세 단계 (Hull Shader, Tessellatior, Domain Shader)을 함께 사용하거나, 사용하지 않아야 한다.


테셀레이션(Tessellation)의 첫 번째 단계인 덮개 셰이더(Hull Shader)는 정점 셰이더가 넘겨준 기본 도형들을 받아서 두 가지 작업을 수행한다. 첫 작업은 각 기본 도형마다 실행되는 것으로, 여기서 덮개 셰이더는 기본도형에 대한 일단의 테셀레이션 계수들을 결정한다. 이 계수들은 이후 과정에서 해당 기본 도형을 얼마나 세밀하게 분할해야 하는지를 파악하는데 쓰인다. 두 번째 작업은 바람직한 출력 제어 패치 구성의 각 제어점마다 실행되는 것으로, 여기서 덮개 셰이더는 이후 영역 셰이더 단계(Domain Shader)에서 기본도형을 실제로 분할하는 데 사용할 제어 점들을 만들어낸다.


테셀레이션의 두 번째 단계인 테셀레이터(Tessellator)는 현재 기본 도형 종류에 적합한 표본 추출 패턴(sampling pattern)을 결정한다. 테셀레이터 단계는 테셀레이션 계수들과 자신만의 구성을 이용해서, 현재 기본 도형의 정점들 중 기본 도형을 더 작은 조각으로 분할하기 위한 표본으로 사용할 정점들을 결정한다. 그 정점들로부터 산출한 일단의 무게 중심 좌표들을 다음 단계인 영역 셰이더(Domain Shader)에게 넘긴다.


테셀레이션의 세 번째 단계인 영역 셰이더(Domain Shader)는 무게 중심 좌표(barycentric coordinates)들과 덮개 셰이더가 생성한 제어점들을 입력으로 받아서 새 정점들을 생성한다. 이 단계에서 현재 기본 도형에 대해 생성된 제어점들 전체와 텍스처, 절차적 알고리즘 등을 이용해서, 테셀레이션된 각 점마다 무게중심 '위치'들을 출력 기하구조(geometry)로 변환해서 다음 단계로 넘겨준다. 테셀레이션 단계에서 증폭된 정점 자료들로부터 출력 기하구조를 생성하는 부분의 유연성 덕분에 파이프라인 안에서 좀 더 다양한 테셀레이션 알고리즘을 구현할 수 있는 여지가 생긴다.


다음 단계인 기하 셰이더(Geometry Shader)는 완성된 형태의 기본 도형(다각형)들을 처리하거나 생성한다. 이 단계에서는 파이프라인에 새로운 자료 요소를 추가하거나 제거할 수 있으며, 덕분에 전통적인 렌더링 파이프라인에서는 불가능했던 흥미로운 응용이 가능해진다.


이제 래스터화기 단계(Rasterizer Stage)에서는 주어진 기하 구조가 렌더 대상의 어떤 픽셀들을 덮는지 파악해서 그 픽셀들에 대한 단편 자료를 산출한다. 각 단편은 모든 정점별(per-vertex) 특성을 해당 픽셀 위치에 맞게 보간한 값들을 가진다. 이렇게 생성된 단편은 픽셀 셰이더로 넘어간다.


픽셀 셰이더(Pixel Shader)는 연결된 각 렌더 대상을 위한 색상 값을 출력한다.


마지막 단계인 출력 병합기(Output Merger)는 픽셀 셰이더 출력을 파이프라인에 연결된 깊이/스텐실 자원 및 렌더 대상 자원에 제대로 '합치는' 작업을 담당한다. 이 과정에서 출력 병합기는 깊이 판정과 스텐실 판정, 혼합 함수 적용 등을 수행하며, 최종적으로는 출력을 해당 자원에 실제로 기록한다.



또 다른 셰이더가 한개 더 존재한다. 그것은 계산 셰이더(Compute Shader)이다. 이 단계는 통상적인 렌더링 패러다임에서 벗어나는 계산을 수행하기 위해 만들어진 것으로, 그런 만큼 통상적인 렌더링 파이프라인과 개별적으로 수행된다고 생각하면 된다. 계산 셰이더 단계는 전적으로 범용 계산에만 쓰이는 하나의 독립적인 파이프라인이라고 할 수 있다. 이전에는 셰이더 프로그램의 호출(실행)이 특정 단계에서 입력이 처리되는 방식에 제약을 받았다. 그리고 스레드가 쓰이는 방식을 개발자가 직접 제어하는 것이 불가능했다. 하지만 계산 셰이더 단계에서는 더 이상 그렇지 않다. 또 다른 기능은 '그룹 공유 메모리' 블록이다. 한 스레드 그룹에 속하는 모든 스레드는 그 그룹의 공유 메모리에 접근할 수 있다. 실행 도중 스레드들은 이 메모리 블록을 통해서 서로 자료를 주고받을 수 있는데, 전통적인 렌더링 파이프라인에서는 이러한 소통이 불가능했다. 그런 소통이 가능해지면, 적재한 자료나 중간 계산 결과를 공유함으로써 효율성을 더욱 개선할 여지가 생긴다.


마지막으로 계산 셰이더 단계에서는 자원들에 대한 임의 읽기 접근과 쓰기 접근이 동시에 가능하다.



728x90

+ Recent posts