[Vulkan] Vulkan Descriptor set layout
다음 내용은 OpenGL에는 존재하지 않고 Vulkan에서도 다소 복잡한 개념이다.
목차
인프런 삼각형님의 '삼각형의 실전! Vulkan 중급' 강의를 참고하였습니다.
😎 [삼각형의 실전! Vulkan 중급] 강의 들으러 가기!
Vulkan Descriptor set layout
삼각형이 움직이기 위해서는 필요한 것
삼각형을 움직이기 위해서 유니폼이 필요하다.
Vulkan에서는 유니폼 블록을 사용해서 유니폼을 정의해야 한다. 아래의 코드를 보면 OpenGL의 유니폼과 생김새가 다른 것을 알 수 있다. set, binding 같은 새로운 키워드가 등장한다.
"layout(set = 0, binding = 0) uniform Uniform { \n" " float position[2]; \n" "}; \n" "void main() { \n" " gl_Position = vec4(inPosition, 1.0); \n" " gl_Position.x += position[0]; \n" " gl_Position.y += position[1]; \n" "} \n"
위의 예제 코드에서는 위치 정보를 저장하는데 데이터 타입으로 Vector2 대신 float 배열을 사용했다. (float position[2]). 이는 std140 메모리 레이아웃을 설명하기 위한 선택으로 실제 응용에서는 Vector2 사용이 더 효과적이다.
GPU는 여러 개의 하드웨어 모듈로 구성되어 있다. Graphics pipeline도 GPU를 구성하는 하나의 하드웨어 모듈인데 이 하드웨어가 유니폼 블록과 같은 리소스를 읽기 위해서는 인터페이스가 필요하다.
C++ 함수에서 파라미터를 사용하기 위해서는 어떤 파라미터를 사용할 수 있는지 알아야합니다.
int foo(int a, int b, int c) { return a + b + c; }
Graphics pipeline도 리소스에 접근하기 위해서 사용할 리소스가 무엇인지 알고 있어야 합니다
리소스 접근을 위한 인터페이스
Graphics pipeline이 리소스에 접근하기 위해 필요한 인터페이스
이 인터페이스는 Graphics pipeline에 어떤 리소스를 사용할 수 있는지를 정의한다.

예를 들어, 유니폼 블록에서 set과 Binding이 모두 0으로 설정되었다면, 이는 해당 유니폼 블록에 필요한 리소스가 Desciptor set layout 0 위치에 설정되어 있다는 것을 의미한다. 반면 set이 1이고 Binding이 0인 경우에는 Desciptor set layout 1 위치에 리소스가 설정되어 있어야 한다.
이러한 Descriptr set layout들은 함께 묶여야 Pipeline layout을 형성한다. 이 Pipeline layout이 Graphics pipeline이 리소스를 읽기 위한 인터페이스다.
VkDescriptorSetLayoutCreateInfo 구조체
typedef struct VkDescriptorSetLayoutCreateInfo { VkStructureType sType; const void* pNext; VkDescriptorSetLayoutCreateFlags flags; uint32_t bindingCount; const VkDescriptorSetLayoutBinding* pBindings; } VkDescriptorSetLayoutCreateInfo;
멤버 변수 | 설명 |
sType | 구조체의 타입 |
pNext | NULL 또는 확장 기능 구조체의 포인터 |
flags | 일단 0을 사용 |
bindingCount | VkDescriptorSetLayoutBinding의 개수 |
pBindings | VkDescriptorSetLayoutBinding 배열의 포인터 |
VkDescriptorSetLayoutBinding 구조체
typedef struct VkDescriptorSetLayoutBinding { uint32_t binding; VkDescriptorType descriptorType; uint32_t descriptorCount; VkShaderStageFlags stageFlags; const VkSampler* pImmutableSamplers; } VkDescriptorSetLayoutBinding;
멤버 변수 | 설명 |
binding | 바인딩 인덱스 |
descriptorType | VkDescriptorType |
descriptorCount | 일단 1을 사용 |
stageFlags | VkShaderStageFlagBits의 조합 |
pImmutableSamplers | 일단 NULL을 사용 |
VkDescriptorType 열거형
바인딩을 정의할 때도 바인딩의 타입을 명시해야 된다.
typedef enum VkDescriptorType { VK_DESCRIPTOR_TYPE_SAMPLER = 0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER = 1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE = 2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE = 3, VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER = 4, VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER = 5, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER = 6, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER = 7, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC = 8, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC = 9, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT = 10, VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT = 1000138000, VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_NV = 1000165000, VK_DESCRIPTOR_TYPE_MAX_ENUM = 0x7FFFFFFF } VkDescriptorType;

바인딩을 0으로 설정하고, Descriptor type을 유니폼 버퍼로 정의하고, 현재 이 유니폼 버퍼는 Vertex Shader에서만 접근하도록 설정되어 있다.
만약 이 유니폼 블록이 Fragment Shader에서도 접근이 필요하다면 VK _SHADER_STAGE_FRAGMENT_BIT을 추가해야 한다.
이렇게 어떤 Shader stage에서 리소스에접근할 수 있는지를 명확히 정의함으로써 Graphics pipeline의 성능을 최대로 이끌어낼 수 있다.
VkDescriptorSetLayoutCreateInfo 구조체

앞서 정의한 바인딩에 대한 정보(= &descriptorSetLayoutBinding)만 정의해주면 된다.
Vulkan Descriptor set layout 생성
VkResult vkCreateDescriptorSetLayout( VkDevice device, const VkDescriptorSetLayoutCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDescriptorSetLayout* pSetLayout);
파라미터 | 설명 |
device | VkDevice |
pCreateInfo | VkDescriptorSetLayoutCreateInfo 변수의 포인터 |
pAllocator | 일단 NULL을 사용 |
pSetLayout | VkDescriptorSetLayout 변수의 포인터 |

mDescriptorSetLayout 생성
Vulkan Descriptor set layout 파괴
void vkDestroyDescriptorSetLayout( VkDevice device, VkDescriptorSetLayout descriptorSetLayout, const VkAllocationCallbacks* pAllocator);
파라미터 | 설명 |
device | VkDevice |
descriptorSetLayout | VkDescriptorSetLayout |
pAllocator | 일단 NULL을 사용 |

코드
#include ... using namespace std; VkRenderer::VkRenderer(ANativeWindow *window) { // 1. VkInstance 생성 // 2. VkPhysicalDevice 선택 // 3. VkPhysicalDeviceMemoryProperties 얻기 // 4. VkDevice 생성 // 5. VkSurface 생성 // 6. VkSwapchain 생성 mSwapchainImageViews.resize(swapchainImageCount); // ImageView를 Swapchain의 개수만큼 생성 for (auto i = 0; i != swapchainImageCount; ++i) { // 7. VkImageView 생성 } // 8. VkCommandPool 생성 // 9. VkCommandBuffer 할당 // 10. VkFence 생성 // 11. VkSemaphore 생성 // 12. VkRenderPass 생성 mFramebuffers.resize(swapchainImageCount); for (auto i = 0; i != swapchainImageCount; ++i) { // 13. VkFramebuffer 생성 } // 14. Vertex VkShaderModule 생성 // 15. Fragment VkShaderModule 생성 // ================================================================================ // 16. VkDescriptorSetLayout 생성 // ================================================================================ VkDescriptorSetLayoutBinding descriptorSetLayoutBinding{ .binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_VERTEX_BIT }; VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo{ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .bindingCount = 1, .pBindings = &descriptorSetLayoutBinding }; VK_CHECK_ERROR(vkCreateDescriptorSetLayout(mDevice, &descriptorSetLayoutCreateInfo, nullptr, &mDescriptorSetLayout)); // 17. VkPipelineLayout 생성 // 18. Graphics VkPipeline 생성 // 19. Vertex VkBuffer 생성 // 20. Vertex VkBuffer의 VkMemoryRequirements 얻기 // 21. Vertex VkDeviceMemory를 할당 할 수 있는 메모리 타입 인덱스 얻기 // 22. Vertex VkDeviceMemory 할당 // 23. Vertex VkBuffer와 Vertex VkDeviceMemory 바인드 // 24. Vertex 데이터 복사 } VkRenderer::~VkRenderer() { ... vkDestroyDescriptorSetLayout(mDevice, mDescriptorSetLayout, nullptr); ... } void VkRenderer::render() { // 1. 화면에 출력할 수 있는 VkImage 얻기 // 2. VkFence 기다린 후 초기화 // 3. VkCommandBuffer 초기화 // 4. VkCommandBuffer 기록 시작 // 5. VkRenderPass 시작 // 6. Graphics VkPipeline 바인드 // ================================================================================ // 7. Vertex VkBuffer 바인드 // ================================================================================ VkDeviceSize vertexBufferOffset{0}; vkCmdBindVertexBuffers(mCommandBuffer, 0, 1, &mVertexBuffer, &vertexBufferOffset); // 8. 삼각형 그리기 // 9. VkRenderPass 종료 // -------------9. Clear 색상 갱신--------------삭제됨. // 10. VkCommandBuffer 기록 종료 // 11. VkCommandBuffer 제출 // 12. VkImage 화면에 출력 }
전체코드
// MIT License // // Copyright (c) 2024 Daemyung Jang // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #include <cassert> #include <cstddef> #include <array> #include <vector> #include <iomanip> #include "VkRenderer.h" #include "VkUtil.h" #include "AndroidOut.h" using namespace std; struct Vector3 { union { float x; float r; }; union { float y; float g; }; union { float z; float b; }; }; struct Vertex { Vector3 position; Vector3 color; }; VkRenderer::VkRenderer(ANativeWindow *window) { // ================================================================================ // 1. VkInstance 생성 // ================================================================================ // VkApplicationInfo 구조체 정의 VkApplicationInfo applicationInfo{ .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "Practice Vulkan", .applicationVersion = VK_MAKE_API_VERSION(0, 0, 1, 0), .apiVersion = VK_MAKE_API_VERSION(0, 1, 3, 0) }; // 사용할 수 있는 레이어를 얻어온다. uint32_t instanceLayerCount; VK_CHECK_ERROR(vkEnumerateInstanceLayerProperties(&instanceLayerCount, nullptr)); vector<VkLayerProperties> instanceLayerProperties(instanceLayerCount); VK_CHECK_ERROR(vkEnumerateInstanceLayerProperties(&instanceLayerCount, instanceLayerProperties.data())); // 활성화할 레이어의 이름을 배열로 만든다. vector<const char*> instanceLayerNames; for (const auto &layerProperty : instanceLayerProperties) { instanceLayerNames.push_back(layerProperty.layerName); } uint32_t instanceExtensionCount; // 사용 가능한 InstanceExtension 개수 VK_CHECK_ERROR(vkEnumerateInstanceExtensionProperties(nullptr, &instanceExtensionCount, nullptr)); vector<VkExtensionProperties> instanceExtensionProperties(instanceExtensionCount); VK_CHECK_ERROR(vkEnumerateInstanceExtensionProperties(nullptr, &instanceExtensionCount, instanceExtensionProperties.data())); vector<const char *> instanceExtensionNames; // instanceExtensionName을 담는 배열 for (const auto &properties: instanceExtensionProperties) { if (properties.extensionName == string("VK_KHR_surface") || properties.extensionName == string("VK_KHR_android_surface")) { instanceExtensionNames.push_back(properties.extensionName); } } assert(instanceExtensionNames.size() == 2); // 반드시 2개의 이름이 필요하기 때문에 확인 // sType: 구조체의 타입, pApplicationInfo: 어플리케이션의 이름 // enabledLayerCount, ppEnableLayerNames: 사용할 레이어의 정보를 정의 VkInstanceCreateInfo instanceCreateInfo{ .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &applicationInfo, .enabledLayerCount = static_cast<uint32_t>(instanceLayerNames.size()), .ppEnabledLayerNames = instanceLayerNames.data(), .enabledExtensionCount = static_cast<uint32_t>(instanceExtensionNames.size()), .ppEnabledExtensionNames = instanceExtensionNames.data() }; // vkCreateInstance로 인스턴스 생성. 생성된 인스턴스가 mInstance에 쓰여진다. VK_CHECK_ERROR(vkCreateInstance(&instanceCreateInfo, nullptr, &mInstance)); // ================================================================================ // 2. VkPhysicalDevice 선택 // ================================================================================ uint32_t physicalDeviceCount; VK_CHECK_ERROR(vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, nullptr)); vector<VkPhysicalDevice> physicalDevices(physicalDeviceCount); VK_CHECK_ERROR(vkEnumeratePhysicalDevices(mInstance, &physicalDeviceCount, physicalDevices.data())); // 간단한 예제를 위해 첫 번째 VkPhysicalDevice를 사용 mPhysicalDevice = physicalDevices[0]; VkPhysicalDeviceProperties physicalDeviceProperties; // 이 구조체 안에 GPU에 필요한 모든 정보가 있다. vkGetPhysicalDeviceProperties(mPhysicalDevice, &physicalDeviceProperties); aout << "Selected Physical Device Information ↓" << endl; aout << setw(16) << left << " - Device Name: " << string_view(physicalDeviceProperties.deviceName) << endl; aout << setw(16) << left << " - Device Type: " << vkToString(physicalDeviceProperties.deviceType) << endl; aout << std::hex; aout << setw(16) << left << " - Device ID: " << physicalDeviceProperties.deviceID << endl; aout << setw(16) << left << " - Vendor ID: " << physicalDeviceProperties.vendorID << endl; aout << std::dec; aout << setw(16) << left << " - API Version: " << VK_API_VERSION_MAJOR(physicalDeviceProperties.apiVersion) << "." << VK_API_VERSION_MINOR(physicalDeviceProperties.apiVersion); aout << setw(16) << left << " - Driver Version: " << VK_API_VERSION_MAJOR(physicalDeviceProperties.driverVersion) << "." << VK_API_VERSION_MINOR(physicalDeviceProperties.driverVersion); // ================================================================================ // 3. VkPhysicalDeviceMemoryProperties 얻기 // ================================================================================ vkGetPhysicalDeviceMemoryProperties(mPhysicalDevice, &mPhysicalDeviceMemoryProperties); // ================================================================================ // 4. VkDevice 생성 // ================================================================================ uint32_t queueFamilyPropertiesCount; //--------------------------------------------------------------------------------- //** queueFamily 속성을 조회 // 사용 가능한 queueFamily의 수(=queueFamilyPropertiesCount)를 얻어온다. vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueFamilyPropertiesCount, nullptr); vector<VkQueueFamilyProperties> queueFamilyProperties(queueFamilyPropertiesCount); // 해당 queueFamily들의 속성을 배열에 얻어온다. vkGetPhysicalDeviceQueueFamilyProperties(mPhysicalDevice, &queueFamilyPropertiesCount, queueFamilyProperties.data()); //--------------------------------------------------------------------------------- // 특정 queueFamilyProperties가 VK_QUEUE_GRAPHICS_BIT를 지원하는지 확인. // 지원하는 queueFamilyProperties를 찾으면 break. queueFamily에 대한 정보는 mQueueFamilyIndex에 저장. for (mQueueFamilyIndex = 0; mQueueFamilyIndex != queueFamilyPropertiesCount; ++mQueueFamilyIndex) { if (queueFamilyProperties[mQueueFamilyIndex].queueFlags & VK_QUEUE_GRAPHICS_BIT) { break; } } // 생성할 큐를 정의 const vector<float> queuePriorities{1.0}; VkDeviceQueueCreateInfo deviceQueueCreateInfo{ .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .queueFamilyIndex = mQueueFamilyIndex, // queueFamilyIndex .queueCount = 1, // 생성할 큐의 개수 .pQueuePriorities = queuePriorities.data() // 큐의 우선순위 }; uint32_t deviceExtensionCount; // 사용 가능한 deviceExtension 개수 VK_CHECK_ERROR(vkEnumerateDeviceExtensionProperties(mPhysicalDevice, nullptr, &deviceExtensionCount, nullptr)); vector<VkExtensionProperties> deviceExtensionProperties(deviceExtensionCount); VK_CHECK_ERROR(vkEnumerateDeviceExtensionProperties(mPhysicalDevice, nullptr, &deviceExtensionCount, deviceExtensionProperties.data())); vector<const char *> deviceExtensionNames; for (const auto &properties: deviceExtensionProperties) { if (properties.extensionName == string("VK_KHR_swapchain")) { deviceExtensionNames.push_back(properties.extensionName); } } assert(deviceExtensionNames.size() == 1); // VK_KHR_swapchain이 반드시 필요하기 때문에 확인 // 생성할 Device 정의 VkDeviceCreateInfo deviceCreateInfo{ .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .queueCreateInfoCount = 1, // 큐의 개수 .pQueueCreateInfos = &deviceQueueCreateInfo, // 생성할 큐의 정보 .enabledExtensionCount = static_cast<uint32_t>(deviceExtensionNames.size()), .ppEnabledExtensionNames = deviceExtensionNames.data() // 활성화하려는 deviceExtension들을 넘겨줌 }; // vkCreateDevice를 호출하여 Device 생성(= mDevice 생성) VK_CHECK_ERROR(vkCreateDevice(mPhysicalDevice, &deviceCreateInfo, nullptr, &mDevice)); // 생성된 Device(= mDevice)로부터 큐를 vkGetDeviceQueue를 호출하여 얻어온다. vkGetDeviceQueue(mDevice, mQueueFamilyIndex, 0, &mQueue); // ================================================================================ // 5. VkSurface 생성 // ================================================================================ VkAndroidSurfaceCreateInfoKHR surfaceCreateInfo{ .sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, .window = window }; // surface 생성. VK_CHECK_ERROR(vkCreateAndroidSurfaceKHR(mInstance, &surfaceCreateInfo, nullptr, &mSurface)); VkBool32 supported; // surface 지원 여부 VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceSupportKHR(mPhysicalDevice, mQueueFamilyIndex, mSurface, &supported)); // 지원 여부를 받아옴. assert(supported); // ================================================================================ // 6. VkSwapchain 생성 // ================================================================================ VkSurfaceCapabilitiesKHR surfaceCapabilities; VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(mPhysicalDevice, mSurface, &surfaceCapabilities)); VkCompositeAlphaFlagBitsKHR compositeAlpha = VK_COMPOSITE_ALPHA_FLAG_BITS_MAX_ENUM_KHR; for (auto i = 0; i <= 4; ++i) { if (auto flag = 0x1u << i; surfaceCapabilities.supportedCompositeAlpha & flag) { compositeAlpha = static_cast<VkCompositeAlphaFlagBitsKHR>(flag); break; } } assert(compositeAlpha != VK_COMPOSITE_ALPHA_FLAG_BITS_MAX_ENUM_KHR); VkImageUsageFlags swapchainImageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; assert(surfaceCapabilities.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); uint32_t surfaceFormatCount = 0; VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceFormatsKHR(mPhysicalDevice, mSurface, &surfaceFormatCount, nullptr)); vector<VkSurfaceFormatKHR> surfaceFormats(surfaceFormatCount); VK_CHECK_ERROR(vkGetPhysicalDeviceSurfaceFormatsKHR(mPhysicalDevice, mSurface, &surfaceFormatCount, surfaceFormats.data())); uint32_t surfaceFormatIndex = VK_FORMAT_MAX_ENUM; for (auto i = 0; i != surfaceFormatCount; ++i) { if (surfaceFormats[i].format == VK_FORMAT_R8G8B8A8_UNORM) { surfaceFormatIndex = i; break; } } assert(surfaceFormatIndex != VK_FORMAT_MAX_ENUM); uint32_t presentModeCount; VK_CHECK_ERROR(vkGetPhysicalDeviceSurfacePresentModesKHR(mPhysicalDevice, mSurface, &presentModeCount, nullptr)); vector<VkPresentModeKHR> presentModes(presentModeCount); VK_CHECK_ERROR(vkGetPhysicalDeviceSurfacePresentModesKHR(mPhysicalDevice, mSurface, &presentModeCount, presentModes.data())); uint32_t presentModeIndex = VK_PRESENT_MODE_MAX_ENUM_KHR; for (auto i = 0; i != presentModeCount; ++i) { if (presentModes[i] == VK_PRESENT_MODE_FIFO_KHR) { presentModeIndex = i; break; } } assert(presentModeIndex != VK_PRESENT_MODE_MAX_ENUM_KHR); VkSwapchainCreateInfoKHR swapchainCreateInfo{ .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .surface = mSurface, .minImageCount = surfaceCapabilities.minImageCount, .imageFormat = surfaceFormats[surfaceFormatIndex].format, .imageColorSpace = surfaceFormats[surfaceFormatIndex].colorSpace, .imageExtent = mSwapchainImageExtent, .imageArrayLayers = 1, .imageUsage = swapchainImageUsage, .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, .preTransform = surfaceCapabilities.currentTransform, .compositeAlpha = compositeAlpha, .presentMode = presentModes[presentModeIndex] }; VK_CHECK_ERROR(vkCreateSwapchainKHR(mDevice, &swapchainCreateInfo, nullptr, &mSwapchain)); uint32_t swapchainImageCount; VK_CHECK_ERROR(vkGetSwapchainImagesKHR(mDevice, mSwapchain, &swapchainImageCount, nullptr)); mSwapchainImages.resize(swapchainImageCount); VK_CHECK_ERROR(vkGetSwapchainImagesKHR(mDevice, mSwapchain, &swapchainImageCount, mSwapchainImages.data())); mSwapchainImageViews.resize(swapchainImageCount); // ImageView를 Swapchain의 개수만큼 생성 for (auto i = 0; i != swapchainImageCount; ++i) { // ================================================================================ // 7. VkImageView 생성 // ================================================================================ VkImageViewCreateInfo imageViewCreateInfo{ // 생성할 ImageView를 정의 .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = mSwapchainImages[i], .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = surfaceFormats[surfaceFormatIndex].format, // Swapchain 이미지 포맷과 동일한 포맷으로 설정 .components = { .r = VK_COMPONENT_SWIZZLE_R, .g = VK_COMPONENT_SWIZZLE_G, .b = VK_COMPONENT_SWIZZLE_B, .a = VK_COMPONENT_SWIZZLE_A, }, .subresourceRange = { // 모든 이미지에 대해서 이 이미지 뷰가 접근할 수 있도록 설정 .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1 } }; VK_CHECK_ERROR(vkCreateImageView(mDevice, &imageViewCreateInfo, nullptr, &mSwapchainImageViews[i])); // mSwapchainImageViews[i] 생성 } // ================================================================================ // 8. VkCommandPool 생성 // ================================================================================ VkCommandPoolCreateInfo commandPoolCreateInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | // command buffer가 자주 변경될 것임을 알려줌 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, // command buffer를 개별적으로 초기화 가능하게 설정 .queueFamilyIndex = mQueueFamilyIndex }; VK_CHECK_ERROR(vkCreateCommandPool(mDevice, &commandPoolCreateInfo, nullptr, &mCommandPool)); // mCommandPool 생성 // ================================================================================ // 9. VkCommandBuffer 할당 // ================================================================================ VkCommandBufferAllocateInfo commandBufferAllocateInfo{ // 할당하려는 command buffer 정의 .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = mCommandPool, .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 1 }; VK_CHECK_ERROR(vkAllocateCommandBuffers(mDevice, &commandBufferAllocateInfo, &mCommandBuffer)); // ================================================================================ // 10. VkFence 생성 // ================================================================================ VkFenceCreateInfo fenceCreateInfo{ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; // 생성할 Fence의 정보를 해당 구조체에서 정의 VK_CHECK_ERROR(vkCreateFence(mDevice, &fenceCreateInfo, nullptr, &mFence)); // mFence 생성. flag에 아무것도 넣어주지 않았기 때문에 생성된 Fence의 초기 상태는 Unsignal 상태다. // ================================================================================ // 11. VkSemaphore 생성 // ================================================================================ VkSemaphoreCreateInfo semaphoreCreateInfo{ .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, }; VK_CHECK_ERROR(vkCreateSemaphore(mDevice, &semaphoreCreateInfo, nullptr, &mSemaphore)); // ================================================================================ // 12. VkRenderPass 생성 // ================================================================================ VkAttachmentDescription attachmentDescription{ .format = surfaceFormats[surfaceFormatIndex].format, .samples = VK_SAMPLE_COUNT_1_BIT, .loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR, .storeOp = VK_ATTACHMENT_STORE_OP_STORE, .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, .finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR }; VkAttachmentReference attachmentReference{ .attachment = 0, .layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL }; VkSubpassDescription subpassDescription{ .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, .colorAttachmentCount = 1, .pColorAttachments = &attachmentReference }; VkRenderPassCreateInfo renderPassCreateInfo{ .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, .attachmentCount = 1, .pAttachments = &attachmentDescription, .subpassCount = 1, .pSubpasses = &subpassDescription }; VK_CHECK_ERROR(vkCreateRenderPass(mDevice, &renderPassCreateInfo, nullptr, &mRenderPass)); // mRenderPass 생성. mFramebuffers.resize(swapchainImageCount); for (auto i = 0; i != swapchainImageCount; ++i) { // ================================================================================ // 13. VkFramebuffer 생성 // ================================================================================ VkFramebufferCreateInfo framebufferCreateInfo{ .sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, .renderPass = mRenderPass, .attachmentCount = 1, .pAttachments = &mSwapchainImageViews[i], // ImageView .width = mSwapchainImageExtent.width, .height = mSwapchainImageExtent.height, .layers = 1 }; VK_CHECK_ERROR(vkCreateFramebuffer(mDevice, &framebufferCreateInfo, nullptr, &mFramebuffers[i]));// mFramebuffers[i] 생성 } // ================================================================================ // 14. Vertex VkShaderModule 생성 // ================================================================================ string_view vertexShaderCode = { "#version 310 es \n" " \n" "layout(location = 0) in vec3 inPosition; \n" "layout(location = 1) in vec3 inColor; \n" " \n" "layout(location = 0) out vec3 outColor; \n" " \n" "void main() { \n" " gl_Position = vec4(inPosition, 1.0); \n" " outColor = inColor; \n" "} \n" }; std::vector<uint32_t> vertexShaderBinary; // VKSL을 SPIR-V로 변환. VK_CHECK_ERROR(vkCompileShader(vertexShaderCode, VK_SHADER_TYPE_VERTEX, &vertexShaderBinary)); VkShaderModuleCreateInfo vertexShaderModuleCreateInfo{ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = vertexShaderBinary.size() * sizeof(uint32_t), // 바이트 단위. .pCode = vertexShaderBinary.data() }; VK_CHECK_ERROR(vkCreateShaderModule(mDevice, &vertexShaderModuleCreateInfo, nullptr, &mVertexShaderModule)); // mVertexShaderModule 생성. // ================================================================================ // 15. Fragment VkShaderModule 생성 // ================================================================================ string_view fragmentShaderCode = { "#version 310 es \n" "precision mediump float; \n" " \n" "layout(location = 0) in vec3 inColor; \n" " \n" "layout(location = 0) out vec4 outColor; \n" " \n" "void main() { \n" " outColor = vec4(inColor, 1.0); \n" "} \n" }; std::vector<uint32_t> fragmentShaderBinary; VK_CHECK_ERROR(vkCompileShader(fragmentShaderCode, VK_SHADER_TYPE_FRAGMENT, &fragmentShaderBinary)); VkShaderModuleCreateInfo fragmentShaderModuleCreateInfo{ .sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = fragmentShaderBinary.size() * sizeof(uint32_t), .pCode = fragmentShaderBinary.data() }; VK_CHECK_ERROR(vkCreateShaderModule(mDevice, &fragmentShaderModuleCreateInfo, nullptr, &mFragmentShaderModule)); // ================================================================================ // 16. VkDescriptorSetLayout 생성 // ================================================================================ VkDescriptorSetLayoutBinding descriptorSetLayoutBinding{ .binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .descriptorCount = 1, .stageFlags = VK_SHADER_STAGE_VERTEX_BIT }; VkDescriptorSetLayoutCreateInfo descriptorSetLayoutCreateInfo{ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .bindingCount = 1, .pBindings = &descriptorSetLayoutBinding }; VK_CHECK_ERROR(vkCreateDescriptorSetLayout(mDevice, &descriptorSetLayoutCreateInfo, nullptr, &mDescriptorSetLayout)); // mDescriptorSetLayout 생성 // ================================================================================ // 17. VkPipelineLayout 생성 // ================================================================================ VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO }; VK_CHECK_ERROR(vkCreatePipelineLayout(mDevice, &pipelineLayoutCreateInfo, nullptr, &mPipelineLayout)); // ================================================================================ // 18. Graphics VkPipeline 생성 // ================================================================================ array<VkPipelineShaderStageCreateInfo, 2> pipelineShaderStageCreateInfos{ VkPipelineShaderStageCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_VERTEX_BIT, .module = mVertexShaderModule, .pName = "main" }, VkPipelineShaderStageCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = VK_SHADER_STAGE_FRAGMENT_BIT, .module = mFragmentShaderModule, .pName = "main" } }; VkVertexInputBindingDescription vertexInputBindingDescription{ .binding = 0, .stride = sizeof(Vertex), .inputRate = VK_VERTEX_INPUT_RATE_VERTEX }; array<VkVertexInputAttributeDescription, 2> vertexInputAttributeDescriptions{ VkVertexInputAttributeDescription{ .location = 0, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = offsetof(Vertex, position) }, VkVertexInputAttributeDescription{ .location = 1, .binding = 0, .format = VK_FORMAT_R32G32B32_SFLOAT, .offset = offsetof(Vertex, color) } }; VkPipelineVertexInputStateCreateInfo pipelineVertexInputStateCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, .vertexBindingDescriptionCount = 1, .pVertexBindingDescriptions = &vertexInputBindingDescription, .vertexAttributeDescriptionCount = static_cast<uint32_t>(vertexInputAttributeDescriptions.size()), .pVertexAttributeDescriptions = vertexInputAttributeDescriptions.data() }; VkPipelineInputAssemblyStateCreateInfo pipelineInputAssemblyStateCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, .topology =VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST }; VkViewport viewport{ .width = static_cast<float>(mSwapchainImageExtent.width), .height = static_cast<float>(mSwapchainImageExtent.height), .maxDepth = 1.0f }; VkRect2D scissor{ .extent = mSwapchainImageExtent }; VkPipelineViewportStateCreateInfo pipelineViewportStateCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, .viewportCount = 1, .pViewports = &viewport, .scissorCount = 1, .pScissors = &scissor }; VkPipelineRasterizationStateCreateInfo pipelineRasterizationStateCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, .polygonMode = VK_POLYGON_MODE_FILL, .cullMode = VK_CULL_MODE_NONE, .lineWidth = 1.0f }; VkPipelineMultisampleStateCreateInfo pipelineMultisampleStateCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT }; VkPipelineDepthStencilStateCreateInfo pipelineDepthStencilStateCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO }; VkPipelineColorBlendAttachmentState pipelineColorBlendAttachmentState{ .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT }; VkPipelineColorBlendStateCreateInfo pipelineColorBlendStateCreateInfo{ .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, .attachmentCount = 1, .pAttachments = &pipelineColorBlendAttachmentState }; VkGraphicsPipelineCreateInfo graphicsPipelineCreateInfo{ .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .stageCount = pipelineShaderStageCreateInfos.size(), .pStages = pipelineShaderStageCreateInfos.data(), .pVertexInputState = &pipelineVertexInputStateCreateInfo, .pInputAssemblyState = &pipelineInputAssemblyStateCreateInfo, .pViewportState = &pipelineViewportStateCreateInfo, .pRasterizationState = &pipelineRasterizationStateCreateInfo, .pMultisampleState = &pipelineMultisampleStateCreateInfo, .pDepthStencilState = &pipelineDepthStencilStateCreateInfo, .pColorBlendState = &pipelineColorBlendStateCreateInfo, .layout = mPipelineLayout, .renderPass = mRenderPass }; VK_CHECK_ERROR(vkCreateGraphicsPipelines(mDevice, VK_NULL_HANDLE, 1, &graphicsPipelineCreateInfo, nullptr, &mPipeline)); // ================================================================================ // 19. Vertex VkBuffer 생성 // ================================================================================ constexpr array<Vertex, 3> vertices{ Vertex{ .position{0.0, -0.5, 0.0}, .color{1.0, 0.0, 0.0} }, Vertex{ .position{0.5, 0.5, 0.0}, .color{0.0, 1.0, 0.0} }, Vertex{ .position{-0.5, 0.5, 0.0}, .color{0.0, 0.0, 1.0} }, }; constexpr VkDeviceSize vertexDataSize{vertices.size() * sizeof(Vertex)}; VkBufferCreateInfo vertexBufferCreateInfo{ .sType =VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = vertexDataSize, .usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT }; VK_CHECK_ERROR(vkCreateBuffer(mDevice, &vertexBufferCreateInfo, nullptr, &mVertexBuffer)); // ================================================================================ // 20. Vertex VkBuffer의 VkMemoryRequirements 얻기 // ================================================================================ VkMemoryRequirements vertexMemoryRequirements; vkGetBufferMemoryRequirements(mDevice, mVertexBuffer, &vertexMemoryRequirements); // ================================================================================ // 21. Vertex VkDeviceMemory를 할당 할 수 있는 메모리 타입 인덱스 얻기 // ================================================================================ uint32_t vertexMemoryTypeIndex; VK_CHECK_ERROR(vkGetMemoryTypeIndex(mPhysicalDeviceMemoryProperties, vertexMemoryRequirements, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &vertexMemoryTypeIndex)); // ================================================================================ // 22. Vertex VkDeviceMemory 할당 // ================================================================================ VkMemoryAllocateInfo vertexMemoryAllocateInfo{ .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = vertexMemoryRequirements.size, .memoryTypeIndex = vertexMemoryTypeIndex }; VK_CHECK_ERROR(vkAllocateMemory(mDevice, &vertexMemoryAllocateInfo, nullptr, &mVertexMemory)); // ================================================================================ // 23. Vertex VkBuffer와 Vertex VkDeviceMemory 바인드 // ================================================================================ VK_CHECK_ERROR(vkBindBufferMemory(mDevice, mVertexBuffer, mVertexMemory, 0)); // ================================================================================ // 24. Vertex 데이터 복사 // ================================================================================ void* vertexData; VK_CHECK_ERROR(vkMapMemory(mDevice, mVertexMemory, 0, vertexDataSize, 0, &vertexData)); memcpy(vertexData, vertices.data(), vertexDataSize); vkUnmapMemory(mDevice, mVertexMemory); } VkRenderer::~VkRenderer() { vkFreeMemory(mDevice, mVertexMemory, nullptr); vkDestroyBuffer(mDevice, mVertexBuffer, nullptr); vkDestroyDescriptorSetLayout(mDevice, mDescriptorSetLayout, nullptr); vkDestroyPipelineLayout(mDevice, mPipelineLayout, nullptr); vkDestroyPipeline(mDevice, mPipeline, nullptr); vkDestroyShaderModule(mDevice, mVertexShaderModule, nullptr); vkDestroyShaderModule(mDevice, mFragmentShaderModule, nullptr); for (auto framebuffer : mFramebuffers) { vkDestroyFramebuffer(mDevice, framebuffer, nullptr); } mFramebuffers.clear(); vkDestroyRenderPass(mDevice, mRenderPass, nullptr); for (auto imageView : mSwapchainImageViews) { vkDestroyImageView(mDevice, imageView, nullptr); } mSwapchainImageViews.clear(); vkDestroySemaphore(mDevice, mSemaphore, nullptr); vkDestroyFence(mDevice, mFence, nullptr); vkFreeCommandBuffers(mDevice, mCommandPool, 1, &mCommandBuffer); vkDestroyCommandPool(mDevice, mCommandPool, nullptr); vkDestroySwapchainKHR(mDevice, mSwapchain, nullptr); vkDestroySurfaceKHR(mInstance, mSurface, nullptr); vkDestroyDevice(mDevice, nullptr); // Device 파괴. queue의 경우 Device를 생성하면서 생겼기 때문에 따로 파괴하는 API가 존재하지 않는다. vkDestroyInstance(mInstance, nullptr); } void VkRenderer::render() { // ================================================================================ // 1. 화면에 출력할 수 있는 VkImage 얻기 // ================================================================================ uint32_t swapchainImageIndex; VK_CHECK_ERROR(vkAcquireNextImageKHR(mDevice, mSwapchain, UINT64_MAX, VK_NULL_HANDLE, mFence, // Fence 설정 &swapchainImageIndex)); // 사용 가능한 이미지 변수에 담기 //auto swapchainImage = mSwapchainImages[swapchainImageIndex]; // swapchainImage에 더 이상 직접 접근하지 않으므로 이제 사용X auto framebuffer = mFramebuffers[swapchainImageIndex]; // ================================================================================ // 2. VkFence 기다린 후 초기화 // ================================================================================ // mFence가 Signal 될 때까지 기다린다. VK_CHECK_ERROR(vkWaitForFences(mDevice, 1, &mFence, VK_TRUE, UINT64_MAX)); // mFence가 Siganl이 되면 vkResetFences를 호출해서 Fence의 상태를 다시 초기화한다. // 초기화하는 이유: vkAcquireNextImageKHR을 호출할 때 이 Fence의 상태는 항상 Unsignal 상태여야 하기 때문이다. VK_CHECK_ERROR(vkResetFences(mDevice, 1, &mFence)); // ================================================================================ // 3. VkCommandBuffer 초기화 // ================================================================================ vkResetCommandBuffer(mCommandBuffer, 0); // ================================================================================ // 4. VkCommandBuffer 기록 시작 // ================================================================================ VkCommandBufferBeginInfo commandBufferBeginInfo{ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT // 한 번만 기록되고 다시 리셋 될 것이라는 의미 }; // mCommandBuffer를 기록중인 상태로 변경. VK_CHECK_ERROR(vkBeginCommandBuffer(mCommandBuffer, &commandBufferBeginInfo)); // ================================================================================ // 5. VkRenderPass 시작 // ================================================================================ VkRenderPassBeginInfo renderPassBeginInfo{ .sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, .renderPass = mRenderPass, .framebuffer = framebuffer, .renderArea{ .extent = mSwapchainImageExtent }, .clearValueCount = 1, .pClearValues = &mClearValue }; vkCmdBeginRenderPass(mCommandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE); // ================================================================================ // 6. Graphics VkPipeline 바인드 // ================================================================================ vkCmdBindPipeline(mCommandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, mPipeline); // ================================================================================ // 7. Vertex VkBuffer 바인드 // ================================================================================ VkDeviceSize vertexBufferOffset{0}; vkCmdBindVertexBuffers(mCommandBuffer, 0, 1, &mVertexBuffer, &vertexBufferOffset); // ================================================================================ // 8. 삼각형 그리기 // ================================================================================ vkCmdDraw(mCommandBuffer, 3, 1, 0, 0); // ================================================================================ // 9. VkRenderPass 종료 // ================================================================================ vkCmdEndRenderPass(mCommandBuffer); // ================================================================================ // 10. VkCommandBuffer 기록 종료 // ================================================================================ VK_CHECK_ERROR(vkEndCommandBuffer(mCommandBuffer)); // mCommandBuffer는 Executable 상태가 된다. // ================================================================================ // 11. VkCommandBuffer 제출 // ================================================================================ VkSubmitInfo submitInfo{ .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 1, .pCommandBuffers = &mCommandBuffer, .signalSemaphoreCount = 1, .pSignalSemaphores = &mSemaphore }; // submitInfo 구조체를 넘김으로써 commandBuffer 정보를 queue에 제출 VK_CHECK_ERROR(vkQueueSubmit(mQueue, 1, &submitInfo, VK_NULL_HANDLE)); // ================================================================================ // 12. VkImage 화면에 출력 // ================================================================================ VkPresentInfoKHR presentInfo{ .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .waitSemaphoreCount = 1, .pWaitSemaphores = &mSemaphore, .swapchainCount = 1, .pSwapchains = &mSwapchain, .pImageIndices = &swapchainImageIndex }; VK_CHECK_ERROR(vkQueuePresentKHR(mQueue, &presentInfo)); // 화면에 출력. VK_CHECK_ERROR(vkQueueWaitIdle(mQueue)); }
'⭐ Vulkan & CMake > Vulkan' 카테고리의 다른 글
[Vulkan] Vulkan Descriptor pool (0) | 2024.09.16 |
---|---|
[Vulkan] Hello Triangle (0) | 2024.09.14 |
[Vulkan] Vulkan Memory (0) | 2024.09.12 |
[Vulkan] Vulkan Buffer (0) | 2024.09.12 |
[Vulkan] Vulkan Graphics pipeline 벌컨 그래픽스 파이프라인 (0) | 2024.09.11 |
댓글
이 글 공유하기
다른 글
-
[Vulkan] Vulkan Descriptor pool
[Vulkan] Vulkan Descriptor pool
2024.09.16 -
[Vulkan] Hello Triangle
[Vulkan] Hello Triangle
2024.09.14 -
[Vulkan] Vulkan Memory
[Vulkan] Vulkan Memory
2024.09.12 -
[Vulkan] Vulkan Buffer
[Vulkan] Vulkan Buffer
2024.09.12
댓글을 사용할 수 없습니다.