Vulkan大闷锅之Buffers

你可曾熟悉这两句代码:

glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT);

OpenGL中,我们只要告诉全局上下文,启用深度缓冲(Depth Buffer)。然后,就没有然后了。

当然,以Vulkan的设计哲学,想用Depth Buffer?自己配置。

创建Depth Buffer

我们之前使用了扩展函数vkCreateSwapchainKHR,它会自动创建Swapchain的每一张Image。这使人产生不需要手动创建Image的错觉:

VkImageCreateInfo image_info = {};
const VkFormat depth_format = VK_FORMAT_D16_UNORM;
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(gpus[0], depth_format, &props);
if (props.linearTilingFeatures &
    VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)
{
  image_info.tiling = VK_IMAGE_TILING_LINEAR;
}
else if (props.optimalTilingFeatures &
          VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT)
{
  image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
}
else
{
  std::cerr << "VK_FORMAT_D16_UFORM Unsupported." << std::endl;
  exit(-1);
}

上述代码顺便校验GPU支持的平铺格式。这里将Depth Buffer相关数据打包方便观察:

//depth defined here
struct
{
  VkImage image;
  VkDeviceMemory mem;
  VkImageView view;
} depth;

image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
image_info.pNext = nullptr;
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.format = depth_format;
image_info.extent.width = WIDTH;
image_info.extent.height = HEIGHT;
image_info.extent.depth = 1;
image_info.mipLevels = 1;
image_info.arrayLayers = 1;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
image_info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
image_info.queueFamilyIndexCount = 0;
image_info.pQueueFamilyIndices = nullptr;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_info.flags = 0;

res = vkCreateImage(device, &image_info, nullptr, &depth.image);
assert(res == VK_SUCCESS);

此处尚欠一块显存,Vulkan提供了函数获取申请内存的信息:

VkMemoryRequirements mem_reqs;
vkGetImageMemoryRequirements(device, depth.image, &mem_reqs);

可以顺便打印一下:

std::cout << "alloc size: " << mem_reqs.size << std::endl;

我的程序输出723,200,单位是Byte。我尝试修改长宽为1920×1080,重新打印的结果是4,861,440。比想像中小很多?

VkMemoryAllocateInfo mem_alloc = {};
mem_alloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
mem_alloc.pNext = nullptr;
mem_alloc.allocationSize = mem_reqs.size;
VkPhysicalDeviceMemoryProperties memory_properties;
vkGetPhysicalDeviceMemoryProperties(gpus[0], &memory_properties);
bool pass = false;
for (uint32_t i = 0; i < memory_properties.memoryTypeCount; i++) 
{ 
  if ((mem_reqs.memoryTypeBits & 1) == 1) 
  { 
    // Type is available, does it match user properties? 
    if ((memory_properties.memoryTypes[i].propertyFlags & 
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == 
         VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) 
    { 
      //assign here 
      mem_alloc.memoryTypeIndex = i; 
      pass = true; 
    } 
   } 
  mem_reqs.memoryTypeBits >>= 1;
}
assert(pass);

VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT表示该内存位于GPU。为了方便演示,这里没有封装函数。申请内存,然后绑定:

res = vkAllocateMemory(device, &mem_alloc, nullptr, &depth.mem);
assert(res == VK_SUCCESS);

res = vkBindImageMemory(device, depth.image, depth.mem, 0);
assert(res == VK_SUCCESS);

就像上一章,我们同样为Depth Buffer创建Image View:

VkImageViewCreateInfo view_info = {};
view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
view_info.pNext = nullptr;
view_info.image = VK_NULL_HANDLE;
view_info.format = depth_format;
view_info.components.r = VK_COMPONENT_SWIZZLE_R;
view_info.components.g = VK_COMPONENT_SWIZZLE_G;
view_info.components.b = VK_COMPONENT_SWIZZLE_B;
view_info.components.a = VK_COMPONENT_SWIZZLE_A;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
view_info.subresourceRange.baseMipLevel = 0;
view_info.subresourceRange.levelCount = 1;
view_info.subresourceRange.baseArrayLayer = 0;
view_info.subresourceRange.layerCount = 1;
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.flags = 0;
view_info.image = depth.image;
res = vkCreateImageView(device, &view_info, nullptr, &depth.view);
assert(res == VK_SUCCESS);

这里和之前的主要区别就是format。按照这个思路,接下来创建Uniform Buffer。

创建Uniform Buffer

OpenGL中出现UBO已经到了后期版本。Vulkan中貌似没有单独的Uniform变量,那就只能手动配置UBO:

VkBufferCreateInfo buf_info = {};
buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
buf_info.pNext = nullptr;
buf_info.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT;
// glm::mat4 used here
buf_info.size = sizeof(glm::mat4);
buf_info.queueFamilyIndexCount = 0;
buf_info.pQueueFamilyIndices = nullptr;
buf_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
buf_info.flags = 0;
struct
{
  VkBuffer buf;
  VkDeviceMemory mem;
  VkDescriptorBufferInfo buffer_info;
} uniform_data;
res = vkCreateBuffer(device, &buf_info, nullptr, &uniform_data.buf);
assert(res == VK_SUCCESS);

和Depth Buffer的创建信息非常类似,主要区别是usage。这里为了方便,直接引入glm库。接下来就是申请内存:

mem_reqs = {};
vkGetBufferMemoryRequirements(device, uniform_data.buf, &mem_reqs);
VkMemoryAllocateInfo alloc_info = {};
alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
alloc_info.pNext = nullptr;
alloc_info.memoryTypeIndex = 0;
alloc_info.allocationSize = mem_reqs.size;
pass = false;
for (uint32_t i = 0; i < memory_properties.memoryTypeCount; i++) 
{ 
  if ((mem_reqs.memoryTypeBits & 1) == 1) 
  { 
    if ((memory_properties.memoryTypes[i].propertyFlags & 
        (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | 
         VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) == 
        (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | 
         VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) 
    { 
      alloc_info.memoryTypeIndex = i; 
      pass = true; 
    } 
  } 
  mem_reqs.memoryTypeBits >>= 1;
}
assert(pass);
res = vkAllocateMemory(device, &alloc_info, nullptr, &uniform_data.mem);
assert(res == VK_SUCCESS);

VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT表示该块(GPU)内存被映射到了主(HOST)内存上,CPU可以对其访问。
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT表示该块内存被CPU更改时可以直接反映到GPU上,其实就是内存屏障。目的就是防止数据更改后,GPU还在读缓存;实际上Vulkan提供两个函数来刷新GPU缓存 vkFlushMappedMemoryRangesvkInvalidateMappedMemoryRanges,这样做有些麻烦就是了。
结下来将这块显存影射给一个指针,然后拷贝未来需要的MVP矩阵给它:

uint8_t *pData;
res = vkMapMemory(device, uniform_data.mem, 0, mem_reqs.size, 0, (void **)&pData);
assert(res == VK_SUCCESS);
glm::mat4 Projection = glm::perspective(glm::radians(45.0f), HEIGHT/WIDTH, 0.1f, 100.0f);
glm::mat4 View = glm::lookAt(
    glm::vec3(-5, 3, -10), // Camera is at (-5,3,-10), in World Space
    glm::vec3(0, 0, 0),    // and looks at the origin
    glm::vec3(0, -1, 0)    // Head is up (set to 0,-1,0 to look upside-down)
);
glm::mat4 Model = glm::mat4(1.0f);
// Vulkan clip space has inverted Y and half Z.
glm::mat4 Clip = glm::mat4(1.0f, 0.0f, 0.0f, 0.0f,
                            0.0f, -1.0f, 0.0f, 0.0f,
                            0.0f, 0.0f, 0.5f, 0.0f,
                            0.0f, 0.0f, 0.5f, 1.0f);
glm::mat4 MVP = Clip * Projection * View * Model;
memcpy(pData, &MVP, sizeof(MVP));

值得注意的是,这块内存同时对CPU和GPU可见,这类资源比较有限。所以在拷贝数据之后立刻取消影射:

vkUnmapMemory(device, uniform_data.mem);

最后记得绑定Buffer和内存:

res = vkBindBufferMemory(device, uniform_data.buf, uniform_data.mem, 0);
assert(res == VK_SUCCESS);

uniform_data.buffer_info.buffer = uniform_data.buf;
uniform_data.buffer_info.offset = 0;
uniform_data.buffer_info.range = sizeof(MVP);

这里顺便为后续的流程准备好Buffer信息。

到此,Vulkan开始像一个图形接口。其余内容,请关注后续博文。

Leave a Reply

Your email address will not be published. Required fields are marked *