DX12 텍스처 업로드와 샘플링
Row Pitch, SRV, Sampler와 sRGB를 하나의 흐름으로 연결하기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
Texture Upload은 Pixel 배열을 memcpy하는 작업으로 끝나지 않는다. CPU 이미지의 행 크기와 GPU Copy의 Row Pitch가 다를 수 있고, Mip마다 Subresource가 나뉘며, SRV Format과 Sampler가 Shader의 읽기 결과를 결정한다.
Buffer와 Texture의 Layout은 다르다
Buffer는 앱이 이해하는 연속 Byte 배열이다. Texture는 GPU Sampling과 Cache에 유리하도록 Adapter가 선택한 불투명 Layout을 쓸 수 있다. 따라서 CPU 포인터로 Default Heap Texture의 Pixel 주소를 계산해 직접 쓰는 구조가 아니다.
CPU image rows
→ row-pitched Upload Buffer
→ CopyTextureRegion
→ GPU Texture subresource
→ COPY_DEST → PIXEL_SHADER_RESOURCE
Texture Resource를 Copy 대상으로 만든다
auto textureDesc = CD3DX12_RESOURCE_DESC::Tex2D(
DXGI_FORMAT_R8G8B8A8_UNORM,
width,
height,
1,
mipCount);
auto defaultProperties =
CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT);
ThrowIfFailed(device->CreateCommittedResource(
&defaultProperties,
D3D12_HEAP_FLAG_NONE,
&textureDesc,
D3D12_RESOURCE_STATE_COPY_DEST,
nullptr,
IID_PPV_ARGS(&texture)));
Resource Format은 메모리의 Channel 구조를 정한다. Albedo처럼 색상 데이터는 후에 SRV를 _SRGB로 보아 Sampling 시 Linear Light로 Decode할 수 있다. Normal, Roughness, Metallic, Mask 같은 데이터 Texture를 sRGB로 Decode하면 값의 의미가 깨진다.
GetCopyableFootprints로 배치를 질의한다
D3D12_PLACED_SUBRESOURCE_FOOTPRINT layout{};
UINT rowCount = 0;
UINT64 rowSize = 0;
UINT64 uploadBytes = 0;
device->GetCopyableFootprints(
&textureDesc,
0,
1,
0,
&layout,
&rowCount,
&rowSize,
&uploadBytes);
rowSize는 실제 Pixel Byte가 든 한 행의 크기고, layout.Footprint.RowPitch는 GPU Copy 정렬을 반영한 행 간격이다. 예를 들어 RGBA8 32 Pixel의 실제 행은 128 Byte지만 Row Pitch는 256 Byte가 될 수 있다.
Texture Upload의 일반 규칙은 다음과 같다.
- 각 Subresource의 Row Pitch는 256 Byte 배수에 맞춰야 한다.
- Upload Buffer 내 Subresource 시작 Offset은 일반적으로 512 Byte 배수에 맞춘다.
- 정확한 조건은 직접 계산하기보다
GetCopyableFootprints로 받는다.
D3D12_FEATURE_DATA_D3D12_OPTIONS13(Agility SDK 1.606 / Windows 11 22H2, 2022년)의 UnrestrictedBufferTextureCopyPitchSupported가 일부 제약을 완화할 수 있지만, 지원을 가정하지 말고 Feature Query와 Fallback을 두자.
행마다 Padding을 넘어 복사한다
std::byte* mapped = nullptr;
D3D12_RANGE noRead{ 0, 0 };
ThrowIfFailed(uploadBuffer->Map(
0, &noRead, reinterpret_cast<void**>(&mapped)));
for (UINT row = 0; row < rowCount; ++row)
{
std::memcpy(
mapped + layout.Offset +
std::size_t(row) * layout.Footprint.RowPitch,
sourcePixels + std::size_t(row) * sourceRowPitch,
static_cast<std::size_t>(rowSize));
}
uploadBuffer->Unmap(0, nullptr);
sourceRowPitch와 GPU RowPitch를 같다고 가정하고 전체 크기를 한 번에 memcpy하면 행 끝의 Padding을 무시하게 된다. 가로줄이 기울거나 색이 뒤섞히는 표적인 원인이다.
DDS, PNG 디코더가 Top-down인지 Bottom-up인지, Channel 순서가 RGBA인지 BGRA인지도 자원 계약의 일부다.
CopyTextureRegion과 State Transition
D3D12_TEXTURE_COPY_LOCATION source{};
source.pResource = uploadBuffer.Get();
source.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
source.PlacedFootprint = layout;
D3D12_TEXTURE_COPY_LOCATION destination{};
destination.pResource = texture.Get();
destination.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
destination.SubresourceIndex = 0;
commandList->CopyTextureRegion(
&destination, 0, 0, 0, &source, nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
texture.Get(),
D3D12_RESOURCE_STATE_COPY_DEST,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
commandList->ResourceBarrier(1, &barrier);
Vertex Upload와 마찬가지로 Upload Buffer는 Copy가 끝나는 Fence까지 살아 있어야 한다. Texture를 Compute와 Pixel Shader 모두에서 읽는다면 필요한 Read State 조합을 선택하고 State Tracker가 추적한다.
SRV와 Sampler를 분리한다
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 = mipCount;
device->CreateShaderResourceView(texture.Get(), &srv, srvCpuHandle);
Texture2D Albedo : register(t0);
SamplerState LinearWrap : register(s0);
float4 PSMain(VSOutput input) : SV_Target0
{
return Albedo.Sample(LinearWrap, input.UV);
}
SRV는 어느 Texture의 어느 Mip과 Format을 읽는지 정한다. Sampler는 Point/Linear/Anisotropic Filter, Wrap/Clamp/Border Address Mode, LOD 범위를 정한다. Texture가 같아도 Sampler가 다르면 결과가 달라진다.
Mip과 LOD는 선명도 옵션만이 아니다
Mip Chain이 없는 고해상도 Texture를 먼 거리에서 샘플링하면 많은 Texel이 Pixel 하나에 압축되면서 번쩍임과 Moiré Pattern이 나타난다. Mip은 필터링 품질을 높이고 Cache/Bandwidth 비용을 줄인다.
LOD는 Pixel Shader 인접 Invocation의 UV Derivative를 이용해 선택될 수 있다. UV가 끊기거나 비선형 분기 안에서 Derivative를 사용할 때는 결과에 주의한다. Texture Asset Pipeline은 각 Mip을 생성하고 올바른 Color Space에서 필터링해야 한다.
증상별 점검
| 증상 | 점검 |
|---|---|
| 가로줄이 기울어진다 | Source Row Pitch와 GPU Row Pitch |
| 빨간색과 파란색이 바뀌었다 | RGBA/BGRA Format |
| Texture가 너무 어둡거나 밝다 | sRGB Decode 중복/누락 |
| 먼 거리에서 번쩍인다 | Mip Chain, Sampler Filter, UV |
| 다른 Texture가 가끔 나온다 | Descriptor Slot 수명 |
| 첫 로드 후 깨진다 | Upload Resource 퇴역 Fence |
Texture는 Resource, Copy Layout, View, Sampler, Color Space가 합쳐져야 의미가 완성된다. 화면에 그림이 나왔다고 끝내지 말고 Row Pitch, Mip, sRGB, Descriptor 수명을 각각 고장내 증상을 기록해 둔다.