DX12 Descriptor Heap과 Root Signature
Resource가 HLSL Register에 연결되는 경로 이해하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
ID3D12Resource 하나를 만들었다고 Shader가 그 메모리를 자동으로 찾지는 않는다. Resource를 어떤 Format과 범위로 볼지 Descriptor에 기록하고, Descriptor Table과 Root Signature을 통해 HLSL Register에 연결해야 한다.
Resource와 View는 다르다
Texture Resource 하나를 여러 방식으로 볼 수 있다.
ID3D12Resource: 실제 메모리와 크기, 차원, 생성 속성
RTV: Render Target으로 쓰기
DSV: Depth/Stencil로 쓰기
SRV: Shader에서 읽기
UAV: Shader에서 순서 보장 없이 읽고 쓰기
CBV: Constant Buffer로 읽기
View는 선택한 Mip, Array Slice, Format, Buffer 범위를 설명한다. Resource의 생성 Format과 View Format은 호환되어야 한다. Typeless Resource를 만들어도 SRV와 RTV/DSV에는 호환되는 Typed Format을 지정한다.
Descriptor Heap은 Descriptor의 배열이다
D3D12_DESCRIPTOR_HEAP_DESC desc{};
desc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
desc.NumDescriptors = 1024;
desc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
ThrowIfFailed(device->CreateDescriptorHeap(
&desc, IID_PPV_ARGS(&resourceHeap)));
const UINT descriptorStride =
device->GetDescriptorHandleIncrementSize(desc.Type);
Heap의 네 종류는 섞을 수 없다.
CBV_SRV_UAVSAMPLERRTVDSV
Shader-visible Flag은 CBV/SRV/UAV Heap과 Sampler Heap에만 의미가 있다. RTV와 DSV는 Shader가 Descriptor Table로 직접 읽는 객체가 아니다.
descriptorStride는 현재 Device에서 Descriptor 한 칸을 이동할 때 필요한 증분이다. Handle을 CPU 메모리 포인터로 캐스팅해 역참조하면 안 된다. Descriptor의 내부 배치는 GPU와 Driver에 따라 다른 불투명 형식이다.
SRV를 Heap Slot에 작성한다
D3D12_SHADER_RESOURCE_VIEW_DESC srv{};
srv.Format = DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
srv.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srv.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srv.Texture2D.MostDetailedMip = 0;
srv.Texture2D.MipLevels = texture->GetDesc().MipLevels;
srv.Texture2D.ResourceMinLODClamp = 0.0f;
auto cpuHandle = resourceHeap->GetCPUDescriptorHandleForHeapStart();
cpuHandle.ptr += std::size_t(textureSlot) * descriptorStride;
device->CreateShaderResourceView(texture.Get(), &srv, cpuHandle);
CreateShaderResourceView는 새 COM 객체를 반환하지 않고 지정한 Heap Slot에 Descriptor 내용을 쓴다. 따라서 Slot Allocator와 수명 관리는 앱의 책임이다.
GPU가 아직 해당 Slot을 참조하는 중인데 CPU가 다른 Texture Descriptor로 덮어쓰면 경쟁이 생긴다. Descriptor도 Resource처럼 Fence 완료 전에 재사용하면 안 된다.
Root Signature는 Shader 인자 계약이다
Root Signature은 Shader가 어떤 자원을 받는지를 정의한다. 실제 Texture나 Buffer를 담는 것이 아니라 인자의 형식과 Register 배치를 정한다.
Root parameter 0: root CBV, b0
Root parameter 1: descriptor table, t0..t3
Static sampler 0: s0
Root Parameter는 세 가지다.
- Root Constants: 작은 32-bit 값을 Root Argument에 직접 넣는다.
- Root Descriptor: CBV나 일부 Buffer SRV/UAV의 GPU Virtual Address를 직접 넣는다.
- Descriptor Table: Shader-visible Heap의 연속 범위를 가리킨다.
Texture2D SRV는 Root Descriptor로 넣을 수 없으므로 Descriptor Table을 사용한다. Root Signature은 크기 제한이 있고 변경 비용도 있으므로 모든 것을 Root Constants나 Root Descriptor로 올리지 않는다.
HLSL Register와 일치시킨다
cbuffer FrameConstants : register(b0)
{
float4x4 ViewProjection;
};
Texture2D AlbedoTexture : register(t0);
SamplerState LinearWrapSampler : register(s0);
HLSL b0 ↔ Root parameter 0 CBV
HLSL t0 ↔ Root parameter 1 descriptor range SRV(t0)
HLSL s0 ↔ Static sampler 0
HLSL Register 번호, Root Parameter 인덱스, Heap Slot 인덱스는 서로 다른 숫자다. 예를 들어 Root Parameter 1에 Heap Slot 57에서 시작하는 Table을 바인딩하면, Shader의 t0이 그 Table의 첫 Descriptor를 본다.
Draw 전에 연결 경로를 완성한다
ID3D12DescriptorHeap* heaps[] = { resourceHeap.Get() };
commandList->SetDescriptorHeaps(1, heaps);
commandList->SetGraphicsRootSignature(rootSignature.Get());
commandList->SetGraphicsRootConstantBufferView(
0, frameConstantBufferAddress);
auto gpuHandle = resourceHeap->GetGPUDescriptorHandleForHeapStart();
gpuHandle.ptr += std::uint64_t(textureSlot) * descriptorStride;
commandList->SetGraphicsRootDescriptorTable(1, gpuHandle);
commandList->DrawIndexedInstanced(indexCount, 1, 0, 0, 0);
Shader-visible CBV/SRV/UAV Heap은 한 시점에 하나, Sampler Heap도 하나만 바인딩할 수 있다. Heap을 자주 교체하는 설계는 GPU Idle이나 Driver 부담을 만들 수 있다. 보통은 큰 Shader-visible Heap을 두고 안에서 Persistent/Transient 영역을 관리한다.
Static Sampler와 Dynamic Sampler를 선택한다
Linear Wrap, Point Clamp처럼 프로그램 전체에서 고정된 Sampler는 Root Signature의 Static Sampler로 넣을 수 있다. Material마다 Filter와 Address Mode가 달라야 하면 Sampler Heap을 사용한다.
Sampler는 Texture 데이터가 아니라 UV가 Texel 사이에 있을 때 어떻게 필터링하고, 범위 밖의 UV를 어떻게 처리하며, 어떤 Mip 범위를 허용할지 정하는 객체다.
검은 화면을 점검하는 순서
- Shader-visible Heap을
SetDescriptorHeaps로 바인딩했는지 본다. - PSO와 호환되는 Root Signature를 설정했는지 본다.
- Root Parameter 인덱스와 Parameter 종류가 맞는지 본다.
- HLSL의
b,t,s,uRegister와 Descriptor Range가 맞는지 본다. - GPU Handle이 CPU Handle과 같은 Slot을 가리키는지 본다.
- Resource State가 Shader 읽기에 맞는지 본다.
- GPU가 참조 중인 Descriptor를 CPU가 덮어쓰지 않았는지 본다.
Descriptor 버그는 가끔 다른 Texture가 나오는 형태로 보이기도 한다. Resource의 소유권만 볼 것이 아니라 Heap Slot의 소유권과 Fence 수명도 같이 추적해야 한다.
공식 자료
확인일: 2026-08-15