CPU는 GPU에 데이터를 넘겨서
쉐이더 코드를 실행하게 한다.
이때 전달하는 데이터를 상수 버퍼에 담아서 전달하는데
그냥 그냥 그냥 대충 담아서 바인딩하면
GPU가 내가 의도한대로 챡챡 알아주지 않는다.

구조체는 정렬 규칙이라는 것이 존재하는데
CPU의 정렬 규칙과 GPU의 정렬 규칙이 다르다.
이 차이점을 알고 상수버퍼를 전달할 때
GPU가 데이터를 잘 읽을 수 있도록
규칙을 맞추어 주는 선행 작업이 필요하다!
구조체 정렬과 패딩 (Structure Alignment and Padding)
DirectX / HLSL에서 GPU로 데이터를 넘길때 상수 버퍼(constant buffer)를 활용하는데 이때 중요한 규칙중 하나가 구조체 정렬과 패딩(Structure Alignment and padding)이다. 이 규칙은 CPU/ GPU가 해당 자료형을 읽을 때 효율적으로 접근할 수 있도록 하기 위해 맞춰둔 메모리 정렬 보정이다. 정렬(Aligment)와 패딩(Padding)을 활용하여 구조체 안에 있는 멤버 중 가장 큰 자료형의 바이트수의 배수로 각 멤버들의 메모리 시작 위치를 할당한다.
cbuffer MyCB : register(b0)
{
float3 position; // 12바이트
float padding; // 자동으로 4바이트 패딩이 들어감
float4 color; // 16바이트
};
정렬(Aligment)
멤버 변수가 메모리에서 시작하는 위치를 지정하는 규칙으로 구조체 안에 가장 큰 멤버변수의 size를 기준으로 잡아 해당 메모리 size의 배수로 멤버 변수의 시작 주소를 지정하는 것이다. 예를 들어 가장 큰 자료형이 Int(4Byte)라면 구조체의 모든 멤버변수는 4의 배수의 메모리에서 주소가 시작된다. 만약 char 멤버가 있더라도 다음 멤버는 3Byte를 건너 뛴 메모리 주소에서 시작하게 된다. 이 공간을 낭비하면 안좋은게 아닌가 생각이 들 수 있는데 CPU, GPU 모두 한번에 특정 크기만큼 고정해서 읽어들이는게 빠르기 때문에 해당 규칙이 존재한다.
패딩(Padding)
정렬 규칙을 맞추기 위해 빈 공간을 끼워 넣는 것으로 안쓰는 메모리 공간이지만 오로지 규칙을 위해 끼워 넣는 것이다. 예를 들어 정렬 규칙이 16바이트 정렬 규칙일 때, float3 멤버변수가 12바이트 공간을 차지하지만 16바이트를 채우기 위해 패딩 4바이트를 자동으로 추가한다.
상수 버퍼 (Constant Buffer)
상수 버퍼(Constant Buffer)는 CPU에서 GPU로 자주 바뀌는 데이터를 효율적으로 전달하기 위한 메모리 영역이다. 즉, 쉐이더(Shader)가 계산할 때 필요한 값들을 담아두는 역할을 한다. VS(vertex shader)는 Clip공간의 좌표를 계산하기 위해 world, view, projection의 transform matrix가 필요로 한다. 이때 CPU에서 GPU로 행렬 데이터를 넘기기 위해 사용하는 것이 바로 상수 버퍼이다. 상수 버퍼를 생성하여 CPU에서 값을 매 프레임 업데이트 해주고, GPU에 바인딩해서 바인딩 된 데이터를 읽을 수 있도록 해주어야 한다.
이때 주의할 점은 HLSL 구조체의 정렬 규칙과 CPU 구조체의 정렬규칙이 다르다는 것이다. HLSL 구조체의 정렬 규칙은 16바이트 단위이다. 때문에 GPU에 넘길 상수 버퍼의 정렬규칙도 16바이트로 맞추어주지 않으면 GPU에서 데이터를 올바르게 읽을 수 없다. (하지만 무조건 16바이트 배수로 맞추면 안된다. 아래 챕터에서 설명!)
정렬 규칙 지정 방법
alignas 명령어를 통해 수동으로 정렬 규칙을 지정할 수 있다. 상단에 한번만 쓴다면 이전 슬롯에 공간이 충분할 때 슬롯을 공유하여 알아서 정렬된다. 하지만 멤버변수마다 alignas를 지정한다면 슬롯 공유 없이 모든 멤버변수가 alignas한 Byte의 배수의 메모리 위치에서 시작하게 된다.
#include <DirectXMath.h>
// 1. 전체 정렬 규칙 지정 16 byte
// 멤버변수들이 HLSL 슬롯 규칙에 맞춰 순서대로 배치되며 슬롯이 공유된다.
// GPU와 HLSL cbuffer가 그대로 매칭 가능하다!
__declspec(align(16))
struct TransformBuffer
{
DirectX::XMMATRIX world;
DirectX::XMMATRIX view;
DirectX::XMMATRIX projection;
};
struct alignas(16) TransformBuffer
{
DirectX::XMMATRIX world;
DirectX::XMMATRIX view;
DirectX::XMMATRIX projection;
};
// 2. 멤버별 정렬 규칙 지정 16 byte
// 모든 멤버변수가 무조건 16byte 슬롯에 배치되어 슬롯 공유가 불가능하다.
struct TransformBuffer
{
alignas(16) DirectX::XMMATRIX world;
alignas(16) DirectX::XMMATRIX view;
alignas(16) DirectX::XMMATRIX projection;
};
상수 버퍼 생성과 바인딩
// CPU -------------------------------------------------------------
// 1. 상수 버퍼 구조체 정의
struct alignas(16) TransformBuffer // 16바이트 정렬 지정
{
DirectX::XMMATRIX world;
DirectX::XMMATRIX view;
DirectX::XMMATRIX projection;
};
// 2. 상수 버퍼 생성
ID3D11Buffer* g_pConstantBuffer = nullptr;
void CreateConstantBuffer(ID3D11Device* device)
{
D3D11_BUFFER_DESC bd = {};
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof(TransformBuffer); // 16바이트 배수
bd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
bd.CPUAccessFlags = 0;
device->CreateBuffer(&bd, nullptr, &g_pConstantBuffer);
}
// 3. 업데이트 및 바인딩
context->UpdateSubresource(g_pConstantBuffer, 0, nullptr, &cbData, 0, 0);
context->VSSetConstantBuffers(0, 1, &g_pConstantBuffer);
// GPU -------------------------------------------------------------
// b0에 바인딩될 상수 버퍼
cbuffer TransformBuffer : register(b0)
{
float4x4 world;
float4x4 view;
float4x4 projection;
};
// vs input struct (vertexbuffer, input layout)
struct VSInput
{
float3 position : POSITION;
};
// ps input struct
struct VSOutput
{
float4 position : SV_POSITION;
};
// vertex shader
VSOutput main(VSInput input)
{
VSOutput output;
// 상수버퍼에 바인딩된 데이터를 사용한다.
output.position = mul(projection, mul(view, mul(world, pos)));
return output;
}
HLSL 패킹과 CPU-GPU 구조체 정렬 규칙 맞추는 법
GPU 코드인 HLSL의 cbuffer는 16바이트 정렬 규칙이 기본이다. 하지만 모든 멤버 변수가 16바이트의 배수의 위치에서 시작하는건 아니다. HLSL는 가능하면 같은 슬롯 안에서 여러 멤버를 묶어서 효율적으로 사용하려고 한다. 즉, HLSL는 작은 데이터 타입끼리 16바이트 슬롯을 공유할 수 있다.
cbuffer ExampleCB : register(b0)
{
float3 a; // 12바이트
float b; // 4바이트 → float3와 합쳐서 한 16바이트 슬롯에 들어감
float2 c; // 8바이트
float2 d; // 8바이트 → 합쳐서 또 하나의 16바이트 슬롯에 들어감
};
이때 문제가 되는것이 바로 CPU쪽에서 16바이트로 정렬 규칙을 지정하는 것이다. CPU 상수 버퍼에서 멤버 변수마다 alignas(16)을 걸면 모든 멤버가 무조건 16바이트 배수의 위치에 놓여지기 때문에 HLSL에서 의도한 ‘슬롯공유’와 어긋나 데이터를 잘못 읽어올 수도 있다.
⭐ CPU와 GPU 상수버퍼 정렬 규칙 맞추는 법 (최종!)
CPU에서 상수버퍼를 선언할때 멤버마다 alignas를 거는게 아닌, 전체 구조체에 alignas(16)을 한번 걸고, 마지막 슬롯이 16Byte를 채우지 못했다면 남는 Byte만큼 패딩을 추가해주면 된다! (CPU에서 중간 패딩은 알아서 일어나지만, 마지막 패딩을 자동으로 생성되지 않기 때문)
// ================= CPU =================
struct alignas(16) ConstantBuffer
{
DirectX::XMFLOAT3 a; // 12B
float b; // 4B → GPU의 float b와 매칭, 슬롯0 완성
DirectX::XMFLOAT2 c; // 8B
DirectX::XMFLOAT2 d; // 8B → 슬롯1 완성
DirectX::XMFLOAT3 e; // 12B
float padding; // 4B → 마지막 슬롯 채우기
};
// ================= GPU =================
cbuffer ConstantBuffer : register(b0)
{
float3 a; // 12B
float b; // 4B → 슬롯0
float2 c; // 8B
float2 d; // 8B → 슬롯1
float3 e; // 12B → 슬롯2
// 마지막 4B는 GPU가 읽지만 CPU에서는 padding으로 채움
};
끝!
'Programming > 컴퓨터그래픽스 (DX 11)' 카테고리의 다른 글
| Texel, UV, Pixel (1) | 2025.09.17 |
|---|---|
| MipMapping과 LOD(Level of Detail) (0) | 2025.09.17 |
| [DirectX 11] DirectXMath vs SimpleMath 어떤 자료형을 사용해야할까? (0) | 2025.09.15 |
| [Lighting] 난반사(Diffuse Reflection)와 램버튼 법칙(Lambert's cosine law) (0) | 2025.09.15 |
| [DirectX 11] D3D11 렌더 타겟 뷰(RTV)와 SwapChain 개념 (0) | 2025.09.10 |