정점 버퍼에서 읽지 않고 셰이더가 삼각형을 만들 수 있을까요?
앞의 Raster 예제는 CPU가 만든 Vertex Buffer와 Index Buffer를 IA에 연결했습니다. Mesh Shader에서는 GPU의 작업 그룹이 정점과 삼각형 연결 정보를 직접 출력할 수 있습니다. 출력한 삼각형은 여전히 래스터라이저와 픽셀 셰이더로 이어집니다. 따라서 Mesh Shader는 래스터라이제이션을 없애는 기능이 아니라 기하를 준비하는 앞부분을 바꾸는 방식입니다.
이번 선택 강의에서는 정점 세 개를 출력하는 작은 예제로 이 차이를 배웁니다. 현재 YamYam의 Scene/Game 렌더링은 VS/PS 경로이며 이 글은 구현 완료 기록이 아닙니다. 실험 전에는 아래 OPTIONS7 검사로 MeshShaderTier 지원을 확인하고, DXC와 해당 인터페이스가 있는 SDK를 준비합니다.
예를 들어 큰 모델을 작은 삼각형 묶음인 meshlet으로 나누고, 보이는 묶음만 GPU에서 처리하는 구조를 만들 수 있습니다. 다만 데이터 분할·가시성 검사·작업 배분을 실제로 구현해야 이점을 얻습니다. 기존 Vertex Shader를 Mesh Shader라는 이름으로 바꾸는 것만으로 자동으로 빨라지지는 않습니다.
Resources
Mesh Shader 파이프라인에서도 역시 다양한 리소스들이 필요합니다. 구조화 버퍼(Structured Buffer), UAV, 상수 버퍼 등은 기존과 동일하게 활용됩니다.
그러나 Mesh Shader는 IA(Input Assembler) 단계가 비활성화되어 있어, 인덱스 버퍼 및 입력 레이아웃을 사용하지 않습니다. 따라서 모든 정점 및 프리미티브 데이터는 셰이더 내부에서 직접 생성되거나 구조화된 형태로 접근해야 합니다.
Amplification Shader / Mesh Shader Structure
Mesh Shader는 두 개의 셰이더 단계로 구성됩니다:
1. Amplification Shader (AS)
•
선택적(Optional) 단계입니다.
•
DispatchMesh()를 호출하여 여러 개의 Mesh Shader 그룹을 생성합니다.
•
AS는 가시성·LOD 판단과 Mesh Shader 작업 분배에 활용할 수 있습니다. Hull Shader의 테셀레이션 계약을 그대로 대체하는 동일한 단계는 아닙니다.
Amplification Shader는 다음 작업을 정할 수 있습니다
AS는 선택 단계입니다. 예를 들어 그룹 하나가 네 개의 meshlet을 처리할 MS 작업을 요청할 수 있습니다. 아래 코드는 가시성 검사까지 구현한 예제가 아니라, baseMeshlet 정보와 네 작업을 전달하는 별도의 작은 예제입니다.
// HLSL: AS를 사용할 때의 별도 개념 예시입니다.
// MS에서는 같은 MeshPayload 정의와 `in payload MeshPayload payload` 입력이 필요합니다.
struct MeshPayload { uint baseMeshlet; };
groupshared MeshPayload payload;
[numthreads(1, 1, 1)]
void ASMain(uint3 groupID : SV_GroupID)
{
payload.baseMeshlet = groupID.x * 4;
DispatchMesh(4, 1, 1, payload);
}
C++
복사
AS 그룹 번호가 2라면 baseMeshlet은 8이 되고 MS 그룹 네 개를 요청합니다. 실제 MS에서는 이 base 값과 자기 그룹 번호를 이용해 담당 meshlet을 고르게 설계할 수 있습니다. AS의 DispatchMesh는 HLSL에서 자식 작업과 payload를 전달하는 호출이며, CPU의 ID3D12GraphicsCommandList6::DispatchMesh와 실행 위치가 다릅니다.
아래 최소 MS는 AS를 생략하므로 이 payload를 받지 않습니다. 두 코드를 한 쌍으로 연결하려면 동일한 MeshPayload 정의와 MS의 in payload 매개변수를 추가해야 합니다.
2. Mesh Shader (MS)
•
Vertex Shader + Geometry Shader의 역할을 통합
•
각 스레드는 정점, 프리미티브를 자유롭게 출력
•
SetMeshOutputCounts(vertexCount, primitiveCount) 호출 필요
입력
•
SV_GroupID, SV_GroupThreadID 등 스레드 정보
•
(선택적) Amplification Shader로부터의 Payload 구조체
•
필요한 리소스들 (구조화 버퍼, 텍스처, 상수 버퍼 등)
동작 흐름
1.
SetMeshOutputCounts(vertexCount, primitiveCount) 호출
→ 출력할 정점/프리미티브 개수 지정
2.
출력 매개변수 배열에 정점과 도형 정보를 기록합니다. HLSL의 groupshared 저장소와 동일한 선언이 아닙니다.
•
vertices[]: 정점 속성 (SV_Position 등 포함 필수)
•
primitiveIndices[]: uint2/uint3로 프리미티브 구성
•
primitives[]: 프리미티브 속성 (컬링, 뷰포트 지정 등)
스레드 하나가 삼각형 하나를 만드는 최소 예제
처음에는 병렬성을 줄여 데이터 연결부터 봅니다. CPU에서 DispatchMesh(1,1,1)을 호출하고 numthreads(1,1,1)로 그룹 안에 스레드 하나를 둡니다. 한 스레드가 정점 세 개와 인덱스 하나를 모두 쓰므로 같은 출력에 여러 스레드가 덮어쓰는 문제가 없습니다.
// HLSL: AS를 생략한 최소 MS 예시입니다. CPU에서 DispatchMesh(1, 1, 1).
struct VertexAttributes { float4 position : SV_Position; };
[outputtopology("triangle")]
[numthreads(1, 1, 1)]
void MSMain(out vertices VertexAttributes verts[3], out indices uint3 tris[1])
{
SetMeshOutputCounts(3, 1);
verts[0].position = float4(-0.5, -0.5, 0, 1);
verts[1].position = float4(0.0, 0.5, 0, 1);
verts[2].position = float4(0.5, -0.5, 0, 1);
tris[0] = uint3(0, 1, 2);
}
// 하나의 스레드만 출력하므로 중복 쓰기가 없습니다.
// 실제 메시 처리에서는 여러 스레드가 서로 다른 요소를 담당하게 확장합니다.
C++
복사
SetMeshOutputCounts(3,1)은 정점 3개와 삼각형 1개를 출력하겠다는 선언입니다. verts 배열은 각 정점의 SV_Position을 담고, tris[0]의 uint3(0,1,2)는 그 정점들을 연결합니다. 전통적인 예제의 VB/IB 역할이 이 출력 배열로 옮겨온 것을 볼 수 있습니다.
numthreads를 32로만 바꾸면 32개 스레드가 같은 verts[0..2]와 tris[0]에 쓰게 됩니다. 병렬 예제로 확장하려면 각 스레드가 서로 다른 요소를 담당하도록 나눠야 합니다. SetMeshOutputCounts는 그룹 전체에서 일관된 제어 흐름으로 호출하고 선언한 배열 용량과 디바이스 지원 한도 안의 개수를 지정합니다. 출력 배열은 groupshared 임시 메모리 선언과도 다릅니다.
Pipeline State
Mesh Shader 파이프라인은 IA, VS, GS, HS, DS를 포함하지 않고, 대신 아래와 같이 구성됩니다:
•
Root Signature
•
Amplification Shader (선택)
•
Mesh Shader (필수)
•
Rasterization (하드웨어)
•
Pixel Shader (선택)
// d3dx12.h와 Mesh Shader를 지원하는 SDK가 필요합니다.
// 완전한 PSO 구성 시 출력 포맷·Rasterizer·Blend·Depth 상태도 맞춥니다.
struct PSO_STREAM
{
CD3DX12_PIPELINE_STATE_STREAM_ROOT_SIGNATURE rootSig;
CD3DX12_PIPELINE_STATE_STREAM_MS MS;
CD3DX12_PIPELINE_STATE_STREAM_PS PS;
CD3DX12_PIPELINE_STATE_STREAM_RENDER_TARGET_FORMATS formats;
CD3DX12_PIPELINE_STATE_STREAM_RASTERIZER raster;
CD3DX12_PIPELINE_STATE_STREAM_BLEND_DESC blend;
CD3DX12_PIPELINE_STATE_STREAM_DEPTH_STENCIL depth;
};
// stream의 각 필드는 컴파일한 바이트코드, root signature, RT 구성으로 채웁니다.
// 순서는 크기 → 포인터입니다. ID3D12Device2 인터페이스로 호출합니다.
D3D12_PIPELINE_STATE_STREAM_DESC desc{sizeof(stream), &stream};
ThrowIfFailed(device2->CreatePipelineState(&desc, IID_PPV_ARGS(&pipelineState)));
// AS 사용 시 AS subobject를 추가하며, 전통 VS/GS/HS/DS와 혼합하지 않습니다.
C++
복사
이 stream은 전통적인 Graphics PSO의 VS 자리에 MS를 넣어 보는 단순 치환이 아닙니다. MS를 사용하는 PSO에는 IA InputLayout과 VS/HS/DS/GS를 함께 구성하지 않습니다. 선택 AS, MS, PS와 출력·Rasterizer·Blend·Depth 상태를 subobject 형태로 모읍니다.
위 코드는 stream의 구조와 CreatePipelineState 호출 형식을 보여 주는 발췌입니다. 실제 stream 객체를 만들고 모든 필드를 채워야 합니다. 최소 예제라면 빈 Root Signature, 앞 MS의 바이트코드, 일정한 색을 반환하는 PS, 실제 RTV 포맷, SampleCount 1, 필요에 맞는 깊이·컬링 상태를 넣습니다. 정점 데이터가 셰이더에 상수로 들어 있으므로 이 최소 사례에는 VB·IB 바인딩이 필요 없습니다.
Mesh Shader Output
Mesh Shader는 다음과 같은 출력을 가집니다:
•
vertices[]: 정점 구조체 배열, 반드시 SV_Position 포함
•
primitiveIndices[]: uint3 배열, 삼각형 정점 인덱스
•
primitives[]: 프리미티브 속성 (선택 사항, SV_CullPrimitive, SV_RenderTargetIndex 등)
출력 속성 확장 예시 — 앞의 최소 VertexAttributes를 대체할 때 사용
struct VertexAttributes {
float4 position : SV_Position;
float3 normal : NORMAL;
};
struct PrimitiveAttributes {
uint RTIndex : SV_RenderTargetArrayIndex;
};
C++
복사
normal을 추가하면 각 정점에서 값을 써서 PS로 전달해야 의미가 있습니다. PrimitiveAttributes는 삼각형 단위 속성의 예입니다. 구조체 선언만 추가한다고 MS의 출력 매개변수나 렌더 타깃 배열 설정까지 연결되지는 않습니다. 첫 삼각형을 확인한 뒤 출력 항목을 하나씩 늘립니다.
Execution Flow
1.
DispatchMesh(x, y, z) 호출 → (AS 생략 시 직접 MS 실행)
2.
Amplification Shader가 Mesh Shader 그룹 런칭 (선택 사항)
3.
Mesh Shader 그룹 내 각 스레드가 정점/프리미티브 출력
4.
출력 도형이 클리핑·래스터라이제이션으로 이어지고 필요하면 Pixel Shader가 실행됩니다.
SetMeshOutputCounts는 그룹 전체에 일관된 제어 흐름으로 호출하고, 선언된 출력 배열 크기와 지원 한도 안의 개수를 지정합니다. 여러 스레드가 같은 출력 요소에 동시에 쓰지 않도록 작업을 나눕니다.
성능 추적 (통계)
// 선언만 해서는 통계가 수집되지 않습니다.
D3D12_QUERY_DATA_PIPELINE_STATISTICS1 stats = {};
// 지원 확인 → QueryHeap 생성 → BeginQuery/EndQuery → ResolveQueryData
// → Fence 완료 → readback 결과를 stats로 읽은 뒤 MSInvocations/MSPrimitives 사용.
// 이 절은 API 순서를 설명하며 실제 수집 구현은 포함하지 않습니다.
C++
복사
D3D12_FEATURE_DATA_D3D12_OPTIONS7 options = {};
const HRESULT hr = device->CheckFeatureSupport(
D3D12_FEATURE_D3D12_OPTIONS7, &options, sizeof(options));
const bool supported = SUCCEEDED(hr) &&
options.MeshShaderTier != D3D12_MESH_SHADER_TIER_NOT_SUPPORTED;
// 지원하지 않으면 기존 IA/VS 렌더링 경로를 사용합니다.
C++
복사
DX12 Device 생성 성공과 Mesh Shader 지원은 별개의 검사입니다. 지원하지 않는 장치에서는 기존 IA/VS 경로를 사용합니다. AS/MS는 DXC의 as_6_5 / ms_6_5 이상으로 컴파일하고 CPU DispatchMesh에는 ID3D12GraphicsCommandList6가 필요합니다. 지원 Tier 확인은 실제 PSO 생성·Dispatch보다 먼저 수행합니다.
참고자료
같은 삼각형을 다른 앞단으로 그려 봅니다
앞 Raster 강의의 VB/IB 삼각형과 이번 MS의 정점 좌표를 같게 놓고 비교하면, 화면의 도형은 같아도 기하를 공급하는 경로가 다름을 확인할 수 있습니다. MS의 꼭짓점 하나만 바꾸면 그 꼭짓점만 이동해야 합니다. CPU의 DispatchMesh 그룹 수를 늘리는 것만으로 서로 다른 위치의 삼각형이 나오지는 않습니다. 각 그룹이 위치나 meshlet 데이터를 다르게 선택해야 합니다.
기본 연결이 이해되면 AS를 붙여 작업 분배, 여러 스레드로 출력 요소 분담, meshlet별 가시성 검사 순으로 확장합니다. 통계 구조체를 선언하는 것만으로 측정이 생기지는 않으며 QueryHeap의 기록·Resolve·Fence 완료·readback까지 연결해야 실제 수치를 얻습니다. Microsoft Mesh Shader 명세에서 출력 규칙과 지원 조건을 확인할 수 있습니다.






