Skip to content

Latest commit

 

History

History
599 lines (448 loc) · 22.1 KB

mapping_data_to_shaders.adoc

File metadata and controls

599 lines (448 loc) · 22.1 KB

쉐이더에 대한 데이터 매핑

Note

모든 SPIR-V 어셈블리는 glslangValidator로 생성되었습니다.

이 장에서는 데이터를 매핑하기 위한 Vulkan과 SPIR-V의 인터페이스 방법에 대해 설명합니다. vkAllocateMemory 에서 할당된 VkDeviceMemory 객체를 사용하여 Vulkan으로부터의 데이터를 SPIR-V 쉐이더가 올바르게 이용할 수 있도록 적절히 매핑하는 것은 애플리케이션의 책임입니다.

Vulkan 코어에서는 Vulkan 애플리케이션의 데이터를 매핑하여 SPIR-V와 인터페이스하는 5가지 기본 방법이 있습니다:

입력 속성

Vulkan 코어에서 Vulkan이 제어하는 입력 속성을 가지는 쉐이더 스테이지는 정점 쉐이더 스테이지(VK_SHADER_STAGE_VERTEX_BIT)뿐입니다. 여기에는 VkPipeline 을 생성할 때 인터페이스 슬롯을 선언하고 드로우콜 전에 VkBuffer 에 매핑할 데이터를 바인딩하는 작업이 포함됩니다. 프래그먼트 쉐이더 스테이지와 같은 다른 쉐이더 스테이지에는 입력 속성을 가지고 있지만, 그 값은 그 전에 실행된 스테이지에서 출력됩니다.

vkCreateGraphicsPipelines 를 호출하기 전에 쉐이더에 대한 VkVertexInputAttributeDescription 매핑 목록으로 VkPipelineVertexInputStateCreateInfo 구조체를 채워야 합니다.

GLSL 정점 쉐이더 예제 (온라인 체험):

#version 450
layout(location = 0) in vec3 inPosition;

void main() {
    gl_Position = vec4(inPosition, 1.0);
}

location 0에는 입력 속성이 하나만 있습니다. 이는 생성된 SPIR-V 어셈블리에서도 확인할 수 있습니다:

              OpDecorate %inPosition Location 0

       %ptr = OpTypePointer Input %v3float
%inPosition = OpVariable %ptr Input
        %20 = OpLoad %v3float %inPosition

이 예제에서는 VkVertexInputAttributeDescription 에 다음과 같은 것을 사용할 수 있습니다:

VkVertexInputAttributeDescription input = {};
input.location = 0;
input.binding  = 0;
input.format   = VK_FORMAT_R32G32B32_SFLOAT; // maps to vec3
input.offset   = 0;

남은 작업은 드로우콜 전에 정점 버퍼와 선택적 인덱스 버퍼를 바인딩하는 것뿐입니다.

Note

VkBuffer 를 생성할 때 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT 을 사용하면 “vertex buffer” 가 됩니다.

vkBeginCommandBuffer();
// ...
vkCmdBindVertexBuffer();
vkCmdDraw();
// ...
vkCmdBindVertexBuffer();
vkCmdBindIndexBuffer();
vkCmdDrawIndexed();
// ...
vkEndCommandBuffer();
Note

자세한 내용은 정점 입력 데이터 처리 챕터에서 확인할 수 있습니다.

디스크립터(Descriptors)

리소스 디스크립터는 유니폼 버퍼, 스토리지 버퍼, 샘플러 등의 데이터를 Vulkan의 임의의 쉐이더 스테이지에 매핑하는 핵심 방법입니다. 개념적으로 디스크립터는 쉐이더가 사용할 수 있는 메모리에 대한 포인터로 생각하면 됩니다.

Vulkan에는 다양한 디스크립터 유형이 있으며, 각 유형이 무엇을 허용하고 있는지 상세하게 설명되어 있습니다.

디스크립터는 쉐이더에 바인딩되는 디스크립터 세트로 함께 그룹화됩니다. 디스크립터 세트 안에 디스크립터가 하나만 있더라도 쉐이더에 바인딩할 때는 VkDescriptorSet 전체가 사용됩니다.

예제

이 예제에서는 다음과 같은 3개의 디스크립터 세트가 있습니다:

mapping_data_to_shaders_descriptor_1.png

GLSL 쉐이더 (온라인 체험):

// Note - 이 쉐이더에서는 세트 0과 2만 사용됩니다

layout(set = 0, binding = 0) uniform sampler2D myTextureSampler;

layout(set = 0, binding = 2) uniform uniformBuffer0 {
    float someData;
} ubo_0;

layout(set = 0, binding = 3) uniform uniformBuffer1 {
    float moreData;
} ubo_1;

layout(set = 2, binding = 0) buffer storageBuffer {
    float myResults;
} ssbo;

대응되는 SPIR-V 어셈블리:

OpDecorate %myTextureSampler DescriptorSet 0
OpDecorate %myTextureSampler Binding 0

OpMemberDecorate %uniformBuffer0 0 Offset 0
OpDecorate %uniformBuffer0 Block
OpDecorate %ubo_0 DescriptorSet 0
OpDecorate %ubo_0 Binding 2

OpMemberDecorate %uniformBuffer1 0 Offset 0
OpDecorate %uniformBuffer1 Block
OpDecorate %ubo_1 DescriptorSet 0
OpDecorate %ubo_1 Binding 3

OpMemberDecorate %storageBuffer 0 Offset 0
OpDecorate %storageBuffer BufferBlock
OpDecorate %ssbo DescriptorSet 2
OpDecorate %ssbo Binding 0

디스크립터 바인딩은 커맨드 버퍼를 기록하는 동안 수행됩니다. 디스크립터는 드로우/디스패치 콜 할 때 바인딩되어야 합니다. 다음은 이를 더 잘 표현하기 위한 의사 코드입니다:

vkBeginCommandBuffer();
// ...
vkCmdBindPipeline(); // 쉐이더 바인드

// 두 세트를 바인딩하는 한 가지 가능한 방법
vkCmdBindDescriptorSets(firstSet = 0, pDescriptorSets = &descriptor_set_c);
vkCmdBindDescriptorSets(firstSet = 2, pDescriptorSets = &descriptor_set_b);

vkCmdDraw(); // or dispatch
// ...
vkEndCommandBuffer();

결과는 다음과 같습니다

mapping_data_to_shaders_descriptor_2.png

디스크립터 유형

Vulkan 사양서에는 쉐이더 리소스와 스토리지 클래스 대응표가 있으며 각 디스크립터 유형이 SPIR-V에서 어떻게 매핑되어야 하는지 설명되어있습니다.

다음은 각 디스크립터 유형에 대한 GLSL 및 SPIR-V 매핑의 예시입니다.

GLSL의 경우 자세한 내용은 GLSL 사양 - 12.2.4. Vulkan 전용: 샘플러, 이미지, 텍스쳐 및 버퍼에서 확인할 수 있습니다.

스토리지 이미지(Storage Image)

VK_DESCRIPTOR_TYPE_STORAGE_IMAGE

// VK_FORMAT_R32_UINT
layout(set = 0, binding = 0, r32ui) uniform uimage2D storageImage;

// GLSL에서 읽고 쓰기 사용법 예제
const uvec4 texel = imageLoad(storageImage, ivec2(0, 0));
imageStore(storageImage, ivec2(1, 1), texel);
OpDecorate %storageImage DescriptorSet 0
OpDecorate %storageImage Binding 0

%r32ui        = OpTypeImage %uint 2D 0 0 0 2 R32ui
%ptr          = OpTypePointer UniformConstant %r32ui
%storageImage = OpVariable %ptr UniformConstant

샘플러와 샘플링된 이미지

VK_DESCRIPTOR_TYPE_SAMPLER and VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE

layout(set = 0, binding = 0) uniform sampler samplerDescriptor;
layout(set = 0, binding = 1) uniform texture2D sampledImage;

// GLSL에서 texture()를 이용한 사용법 예제
vec4 data = texture(sampler2D(sampledImage,  samplerDescriptor), vec2(0.0, 0.0));
OpDecorate %sampledImage DescriptorSet 0
OpDecorate %sampledImage Binding 1
OpDecorate %samplerDescriptor DescriptorSet 0
OpDecorate %samplerDescriptor Binding 0

%image        = OpTypeImage %float 2D 0 0 0 1 Unknown
%imagePtr     = OpTypePointer UniformConstant %image
%sampledImage = OpVariable %imagePtr UniformConstant

%sampler           = OpTypeSampler
%samplerPtr        = OpTypePointer UniformConstant %sampler
%samplerDescriptor = OpVariable %samplerPtr UniformConstant

%imageLoad       = OpLoad %image %sampledImage
%samplerLoad     = OpLoad %sampler %samplerDescriptor

%sampleImageType = OpTypeSampledImage %image
%1               = OpSampledImage %sampleImageType %imageLoad %samplerLoad

%textureSampled = OpImageSampleExplicitLod %v4float %1 %coordinate Lod %float_0

결합 이미지 샘플러

VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER

Note

구현에 따라서 결합된 디스크립터 내의 디스크립터 세트에 함께 저장된 샘플러와 샘플링된 이미지를 조합하여 이미지로부터 샘플링하는 것이 더 효율적일 수 있습니다.

layout(set = 0, binding = 0) uniform sampler2D combinedImageSampler;

// GLSL에서 texture() 이용한 사용법 예제
vec4 data = texture(combinedImageSampler, vec2(0.0, 0.0));
OpDecorate %combinedImageSampler DescriptorSet 0
OpDecorate %combinedImageSampler Binding 0

%imageType            = OpTypeImage %float 2D 0 0 0 1 Unknown
%sampleImageType      = OpTypeSampledImage imageType
%ptr                  = OpTypePointer UniformConstant %sampleImageType
%combinedImageSampler = OpVariable %ptr UniformConstant

%load           = OpLoad %sampleImageType %combinedImageSampler
%textureSampled = OpImageSampleExplicitLod %v4float %load %coordinate Lod %float_0

유니폼 버퍼(Uniform Buffer)

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER

Note

유니폼 버퍼는 바인드 시간에 동적 오프셋을 가질 수도 있습니다(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)

layout(set = 0, binding = 0) uniform uniformBuffer {
    float a;
    int b;
} ubo;

// example of reading from UBO in GLSL
int x = ubo.b + 1;
vec3 y = vec3(ubo.a);
OpMemberDecorate %uniformBuffer 0 Offset 0
OpMemberDecorate %uniformBuffer 1 Offset 4
OpDecorate %uniformBuffer Block
OpDecorate %ubo DescriptorSet 0
OpDecorate %ubo Binding 0

%uniformBuffer = OpTypeStruct %float %int
%ptr           = OpTypePointer Uniform %uniformBuffer
%ubo           = OpVariable %ptr Uniform

스토리지 버퍼(Storage Buffer)

VK_DESCRIPTOR_TYPE_STORAGE_BUFFER

Note

스토리지 버퍼는 바인드 시간에 동적 오프셋을 가질 수도 있습니다(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC)

layout(set = 0, binding = 0) buffer storageBuffer {
    float a;
    int b;
} ssbo;

// example of reading and writing SSBO in GLSL
ssbo.a = ssbo.a + 1.0;
ssbo.b = ssbo.b + 1;
Note
중요

BufferBlockUniformVK_KHR_storage_buffer_storage_class 이전부터 존재합니다.

OpMemberDecorate %storageBuffer 0 Offset 0
OpMemberDecorate %storageBuffer 1 Offset 4
OpDecorate %storageBuffer Block
OpDecorate %ssbo DescriptorSet 0
OpDecorate %ssbo Binding 0

%storageBuffer = OpTypeStruct %float %int
%ptr           = OpTypePointer StorageBuffer %storageBuffer
%ssbo          = OpVariable %ptr StorageBuffer

유니폼 텍셀 버퍼(Uniform Texel Buffer)

VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER

layout(set = 0, binding = 0) uniform textureBuffer uniformTexelBuffer;

// GLSL에서 텍셀 버퍼 읽기 예제
vec4 data = texelFetch(uniformTexelBuffer, 0);
OpDecorate %uniformTexelBuffer DescriptorSet 0
OpDecorate %uniformTexelBuffer Binding 0

%texelBuffer        = OpTypeImage %float Buffer 0 0 0 1 Unknown
%ptr                = OpTypePointer UniformConstant %texelBuffer
%uniformTexelBuffer = OpVariable %ptr UniformConstant

스토리지 텍셀 버퍼(Storage Texel Buffer)

VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER

// VK_FORMAT_R8G8B8A8_UINT
layout(set = 0, binding = 0, rgba8ui) uniform uimageBuffer storageTexelBuffer;

// GLSL에서 텍셀 버퍼 읽고 쓰기 예제
int offset = int(gl_GlobalInvocationID.x);
vec4 data = imageLoad(storageTexelBuffer, offset);
imageStore(storageTexelBuffer, offset, uvec4(0));
OpDecorate %storageTexelBuffer DescriptorSet 0
OpDecorate %storageTexelBuffer Binding 0

%rgba8ui            = OpTypeImage %uint Buffer 0 0 0 2 Rgba8ui
%ptr                = OpTypePointer UniformConstant %rgba8ui
%storageTexelBuffer = OpVariable %ptr UniformConstant

입력 첨부(Input Attachment)

VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT

layout (input_attachment_index = 0, set = 0, binding = 0) uniform subpassInput inputAttachment;

// GLSL에서 첨부 데이터 불러오기 예제
vec4 data = subpassLoad(inputAttachment);
OpDecorate %inputAttachment DescriptorSet 0
OpDecorate %inputAttachment Binding 0
OpDecorate %inputAttachment InputAttachmentIndex 0

%subpass         = OpTypeImage %float SubpassData 0 0 0 2 Unknown
%ptr             = OpTypePointer UniformConstant %subpass
%inputAttachment = OpVariable %ptr UniformConstant

푸시 상수(Push Constants)

푸시 상수는 쉐이더에서 액세스할 수 있는 작은 값 모임입니다. 푸시 상수를 사용하면 애플리케이션에서 버퍼를 생성하거나 업데이트할 때마다 디스크립터 세트를 수정 및 바인딩하지 않고도 쉐이더에 사용되는 값을 설정할 수 있습니다.

이것들은 소량(몇 워드)의 빈번하게 갱신되는 데이터를 커맨드 버퍼의 기록별로 업데이트하는 것에 적합하도록 설계되었습니다.

자세한 내용은 푸시 상수 챕터에서 확인할 수 있습니다.

특수화 상수(Specialization Constants)

특수화 상수VkPipeline 생성 시 SPIR-V의 상수 값을 지정할 수 있는 메커니즘입니다. 이는 고수준 쉐이딩 언어(GLSL, HLSL 등)에서 전처리기 매크로 사용을 대체할 수 있는 강력한 기능입니다.

예제

애플리케이션이 각각 다른 색상 값을 가진 VkPipeline 을 생성하려는 경우, 순진한(naive) 접근 방식은 두 개의 쉐이더를 사용하는 것입니다:

// shader_a.frag
#version 450
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(0.0);
}
// shader_b.frag
#version 450
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(1.0);
}

특수화 상수를 사용하면 쉐이더를 컴파일하기 위해 vkCreateGraphicsPipelines 를 호출할 때 대신 결정할 수 있습니다. 즉, 쉐이더가 하나만 있으면 됩니다.

#version 450
layout (constant_id = 0) const float myColor = 1.0;
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(myColor);
}

SPIR-V 어셈블리 결과:

           OpDecorate %outColor Location 0
           OpDecorate %myColor SpecId 0

// 0x3f800000 as decimal which is 1.0 for a 32 bit float
%myColor = OpSpecConstant %float 1065353216

특수화 상수를 사용하면 쉐이더 내부에서는 여전히 값이 상수이지만, 예를 들어 다른 VkPipeline 이 동일한 쉐이더를 사용하지만 myColor 값을 0.5f 로 설정하려는 경우, 런타임에 이를 설정할 수 있습니다.

struct myData {
    float myColor = 1.0f;
} myData;

VkSpecializationMapEntry mapEntry = {};
mapEntry.constantID = 0; // GLSL에서는 constant_id, SPIR-V에서는 SpecId와 일치
mapEntry.offset     = 0;
mapEntry.size       = sizeof(float);

VkSpecializationInfo specializationInfo = {};
specializationInfo.mapEntryCount = 1;
specializationInfo.pMapEntries   = &mapEntry;
specializationInfo.dataSize      = sizeof(myData);
specializationInfo.pData         = &myData;

VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.pStages[fragIndex].pSpecializationInfo = &specializationInfo;

// myColor를 1.0으로 설정하여 첫 번째 파이프라인 생성
vkCreateGraphicsPipelines(&pipelineInfo);

// 동일한 쉐이더를 사용하지만 다른 값을 설정하는 두 번째 파이프라인 생성
myData.myColor = 0.5f;
vkCreateGraphicsPipelines(&pipelineInfo);

역어셈블한 두 번째 VkPipeline 쉐이더에서는 myColor 의 새로운 상수 값이 0.5f 를 가집니다.

3 가지 유형의 특수화 상수 사용법

특수화 상수의 일반적인 사용 사례는 크게 3가지로 분류할 수 있습니다.

  • 토글링 기능

    • Vulkan 내에서 지원하는 기능은 실행 시까지 알 수 없습니다. 이 특수화 상수의 사용법은 두 개의 쉐이더를 별도로 작성하는 것을 방지하가 위한 것으로 대신 런타임에 상수 값을 결정합니다.

  • 백엔드 최적화 개선

    • 여기서 말하는 “백엔드” 란 SPIR-V의 결과를 기기에서 실행할 수 있도록 일부 ISA로 낮추는 컴파일러의 동작을 의미합니다.

    • 상수 값을 사용하면 상수 접기, 죽은 코드 제거 등과 같은 일련의 최적화를 수행할 수 있습니다.

  • 타입 및 메모리 크기에 미치는 영향

    • 특수화 상수에서 사용되는 배열이나 변수형의 길이를 설정할 수 있습니다.

    • 여기서 중요한 것은 이러한 타입과 크기에 따라 컴파일러가 레지스터를 할당해야 한다는 것입니다. 즉 할당된 레지스터에 큰 차이가 있으면 파이프라인 캐시가 실패할 가능성이 높아집니다.

물리적 스토리지 버퍼(Physical Storage Buffer)

Vulkan 1.2에서 승격된 VK_KHR_buffer_device_address 확장을 통해 “쉐이더 내에 포인터” 를 가진 기능이 추가되었습니다. 애플리케이션은 SPIR-V의 PhysicalStorageBuffer 스토리지 클래스를 사용하여 vkGetBufferDeviceAddress 를 호출하면 VkDeviceAddress 를 메모리로 반환할 수 있습니다.

이것은 데이터를 쉐이더에 매핑하는 방법이긴 하지만, 쉐이더와 인터페이스 되는 것은 아닙니다. 예를 들어, 애플리케이션이 유니폼 버퍼에서 이를 사용하고 싶다면 VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BITVK_BUFFER_USAGE_UNIFORM_BUFFER_BIT 를 모두 포함하는 VkBuffer 를 생성해야 합니다. 이 예제에서 Vulkan은 디스크립터를 사용하여 쉐이더와 인터페이스하지만, 그 이후에는 물리적 스토리지 버퍼를 사용하여 값을 업데이트할 수 있습니다.

제한 사항

Vulkan에는 한 번에 바인딩할 수 있는 데이터의 양에 제한이 있다는 점을 알아두는 것이 중요합니다.

  • 입력 속성

    • maxVertexInputAttributes

    • maxVertexInputAttributeOffset

  • 디스크립터

    • maxBoundDescriptorSets

    • 스테이지별 제한

    • maxPerStageDescriptorSamplers

    • maxPerStageDescriptorUniformBuffers

    • maxPerStageDescriptorStorageBuffers

    • maxPerStageDescriptorSampledImages

    • maxPerStageDescriptorStorageImages

    • maxPerStageDescriptorInputAttachments

    • 유형별 제한

    • maxPerStageResources

    • maxDescriptorSetSamplers

    • maxDescriptorSetUniformBuffers

    • maxDescriptorSetUniformBuffersDynamic

    • maxDescriptorSetStorageBuffers

    • maxDescriptorSetStorageBuffersDynamic

    • maxDescriptorSetSampledImages

    • maxDescriptorSetStorageImages

    • maxDescriptorSetInputAttachments

    • VkPhysicalDeviceDescriptorIndexingProperties Descriptor Indexing를 사용하는 경우

    • VkPhysicalDeviceInlineUniformBlockPropertiesEXT Inline Uniform Block를 사용하는 경우

  • 푸시 상수

    • maxPushConstantsSize - 모든 장치에서 최소 128 바이트 보장