← SEARCHABLE ARCHIVE
DirectX 12 · 2026-08-15

DX12 텍스처 업로드와 샘플링

Row Pitch, SRV, Sampler와 sRGB를 하나의 흐름으로 연결하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

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 수명을 각각 고장내 증상을 기록해 둔다.

공식 자료

확인일: 2026-08-15