DX12 HLSL과 Pipeline State Object
정점 형식, Shader와 고정 Pipeline State를 하나의 계약으로 묶기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
DirectX 12의 Pipeline State Object는 Shader 바이트코드와 정점 형식, Rasterizer, Blend, Depth/Stencil, Render Target Format을 하나의 계약으로 묶는다. Draw 직전에 각 상태를 임의로 바꾸는 구조가 아니라, 미리 검증된 조합을 선택하는 구조다.
가장 작은 Shader 계약
struct VSInput
{
float3 Position : POSITION;
float4 Color : COLOR;
};
struct VSOutput
{
float4 Position : SV_Position;
float4 Color : COLOR;
};
VSOutput VSMain(VSInput input)
{
VSOutput output;
output.Position = float4(input.Position, 1.0f);
output.Color = input.Color;
return output;
}
float4 PSMain(VSOutput input) : SV_Target0
{
return input.Color;
}
Vertex Shader의 출력 SV_Position은 Clip Space 위치다. Rasterizer는 이 값으로 Perspective Divide, Viewport 변환, Triangle 생성을 진행한다. Pixel Shader의 SV_Target0는 첫 번째 Render Target에 쓸 출력이다.
POSITION, COLOR는 C++ Input Layout과 맞추는 Semantic이고, SV_ 접두어는 Pipeline이 특별한 의미로 읽는 System-value Semantic이다.
C++ Vertex Layout과 HLSL 입력을 일치시킨다
struct Vertex
{
DirectX::XMFLOAT3 position;
DirectX::XMFLOAT4 color;
};
static_assert(offsetof(Vertex, position) == 0);
static_assert(offsetof(Vertex, color) == 12);
static_assert(sizeof(Vertex) == 28);
const D3D12_INPUT_ELEMENT_DESC inputElements[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT,
0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
Semantic 이름과 Index, Format, Input Slot, Byte Offset이 모두 맞아야 한다. sizeof(Vertex)만 맞는다고 입력이 올바른 것은 아니다. Compiler Padding을 추측하지 말고 offsetof과 정적 검증으로 계약을 고정한다.
DXC로 오류를 손실 없이 보고한다
렌더러는 Shader 파일, Entry Point, Target Profile, Define, Include 경로를 빌드 입력으로 다뤄야 한다.
triangle.hlsl
VSMain → vs_6_0
PSMain → ps_6_0
Debug 빌드에서는 디버그 정보와 소스 연결을 보존하고, Release 빌드에서는 제품 정책에 맞는 최적화를 한다. Compile 실패 시 오류 Blob을 버리지 말고 파일, 줄, Entry, Target과 함께 출력한다.
Runtime Compile은 반복 개발에 편리하지만 최종 배포 경로에서는 누락된 Permutation과 첫 실행 Hitch를 만들 수 있다. Shipping Shader는 Offline Compile과 Cache Version 정책을 따로 둔다.
PSO가 묶는 상태
D3D12_GRAPHICS_PIPELINE_STATE_DESC pso{};
pso.pRootSignature = rootSignature.Get();
pso.VS = { vertexShader->GetBufferPointer(), vertexShader->GetBufferSize() };
pso.PS = { pixelShader->GetBufferPointer(), pixelShader->GetBufferSize() };
pso.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
pso.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
pso.DepthStencilState.DepthEnable = FALSE;
pso.DepthStencilState.StencilEnable = FALSE;
pso.SampleMask = UINT_MAX;
pso.InputLayout = { inputElements, _countof(inputElements) };
pso.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
pso.NumRenderTargets = 1;
pso.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
pso.SampleDesc = { 1, 0 };
ThrowIfFailed(device->CreateGraphicsPipelineState(
&pso, IID_PPV_ARGS(&pipelineState)));
PSO에는 다음 계약이 함께 들어간다.
- 모든 Graphics Shader 바이트코드
- Input Layout과 Primitive Topology Type
- Rasterizer, Blend, Depth/Stencil State
- Render Target 개수와 Format, Depth Format
- MSAA Sample 설정
- Root Signature
Render Target의 실제 Format이 PSO의 RTVFormats[0]과 다르면 Draw 계약이 틀린다. MSAA Count, DSV Format, Root Signature 호환성도 같은 식으로 맞춰야 한다.
PSO 밖에 남는 상태
모든 상태가 PSO에 들어가는 것은 아니다.
commandList->SetPipelineState(pipelineState.Get());
commandList->SetGraphicsRootSignature(rootSignature.Get());
commandList->RSSetViewports(1, &viewport);
commandList->RSSetScissorRects(1, &scissorRect);
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr);
Viewport, Scissor Rect, 정확한 Primitive Topology, Vertex/Index Buffer, Render Target Binding, Descriptor Heap과 Root Argument는 Command List에 따로 기록한다.
PrimitiveTopologyType = TRIANGLE은 PSO의 큰 분류고, TRIANGLELIST는 실제 정점 연결 규칙이다. 이 둘을 같은 값으로 생각하면 Line List나 Triangle Strip을 추가할 때 헷갈린다.
상태 조합을 키로 관리한다
PipelineKey
shaderProgram
vertexLayout
renderTargetFormats
depthFormat
blendMode
rasterMode
sampleCount
PSO를 Draw마다 생성하면 안 된다. 예상 조합은 로딩 단계에서 만들고, Runtime에서는 안정적인 Key로 Cache에서 찾는다. 키에 포함되어야 할 상태를 빠면 서로 다른 조합이 같은 PSO를 공유하는 버그가 생긴다.
Permutation이 shader × material × pass × sample count × feature flag로 무제한 늘어나지 않게 모든 옵션이 정말 Compile-time 분기여야 하는지 검토한다.
검은 화면을 조사하는 순서
- Shader Compile 오류와 PSO 생성
HRESULT를 확인한다. - Clear Color가 보이는지 확인해 Swap Chain과 RTV 문제를 분리한다.
- PSO의 RTV/DSV Format과 실제 View Format을 비교한다.
- Input Layout의 Semantic, Format, Offset과 Vertex Stride를 비교한다.
- Root Signature과 HLSL Register 계약을 비교한다.
- Viewport, Scissor, Winding, Cull Mode를 확인한다.
- Pixel Shader를 상수 색으로 바꾸어 Interpolation과 Resource Binding을 분리한다.
Triangle이 보인다는 결과만으로는 PSO 계약을 증명할 수 없다. Input Layout의 항목 하나를 의도적으로 틀리고, Cull Mode를 바꾸고, Render Target Format 불일치를 재현해 각 실패가 어떤 증상을 만드는지 기록해야 한다.
공식 자료
- Direct3D 12의 Graphics Pipeline State 관리
- Direct3D 12 Pipeline과 Shader
- Command List와 Bundle 기록
- Root Signature 사용
확인일: 2026-08-15