This PR adds basic support for texture update, read back and clearing
for Vulkan. In Vulkan we need to convert each data type ourselves as
vulkan buffers are untyped. Therefore this change mostly is about data
conversions.
Considerations:
- Use a compute shader to do the conversions:
- Leads to performance regression as compute pipeline can stall
graphics pipeline
- Lead to additional memory usage as two staging buffers are needed
one to hold the CPU data, and one to hold the converted data.
- Do inline conversion when sending the data to Vulkan using `eGPUDataFormat`
- Additional CPU cycles required and not easy to optimize as it the
implementation requires many branches.
- Do inline conversion when sending the data to Vulkan (optimized for CPU)
For this solution it was chosen to implement the 3rd option as it is fast
and doesn't require additional memory what the other options do.
**Use Imath/half.h**
This patch uses `Imath/half.h` (dependency of OpenEXR) similar to
alembic. But this makes vulkan dependent of the availability of
OpenEXR. For now this isn't checked, but when we are closer to
a working Vulkan backend we have to make a decision how to cope with
this dependency.
**Missing Features**
*Framebuffer textures*
This doesn't include all possible data transformations. Some of those
transformation can only be tested after the VKFramebuffer has been
implemented. Some texture types are only available when created for a
framebuffer. These include the depth and stencil variations.
*Component format*
Is more relevant when implementing VKVertexBuffer.
*SRGB textures*
SRGB encoded textures aren't natively supported on all platforms, in
all usages and might require workarounds. This should be done in a
separate PR in a later stage when we are required to use SRGB textures.
**Test cases**
The added test cases gives an overview of the missing bits and pieces of
the patch. When the implementation/direction is accepted more test cases
can be enabled/implemented.
Some of these test cases will skip depending on the actual support of
platform the tests are running on. For example OpenGL/NVidia will skip
the next test as it doesn't support the texture format on OpenGL, although
it does support it on Vulkan.
```
[ RUN ] GPUOpenGLTest.texture_roundtrip__GPU_DATA_2_10_10_10_REV__GPU_RGB10_A2UI
[ SKIPPED ] GPUOpenGLTest.texture_roundtrip__GPU_DATA_2_10_10_10_REV__GPU_RGB10_A2UI [ RUN ] GPUVulkanTest.texture_roundtrip__GPU_DATA_2_10_10_10_REV__GPU_RGB10_A2UI
[ OK ] GPUVulkanTest.texture_roundtrip__GPU_DATA_2_10_10_10_REV__GPU_RGB10_A2UI
```
Pull Request: blender/blender#105762
311 lines
9.9 KiB
C++
311 lines
9.9 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later
|
|
* Copyright 2022 Blender Foundation. All rights reserved. */
|
|
|
|
/** \file
|
|
* \ingroup gpu
|
|
*/
|
|
|
|
#include "vk_texture.hh"
|
|
|
|
#include "vk_buffer.hh"
|
|
#include "vk_context.hh"
|
|
#include "vk_data_conversion.hh"
|
|
#include "vk_memory.hh"
|
|
#include "vk_shader.hh"
|
|
#include "vk_shader_interface.hh"
|
|
|
|
#include "BLI_math_vector.hh"
|
|
|
|
#include "BKE_global.h"
|
|
|
|
namespace blender::gpu {
|
|
|
|
VKTexture::~VKTexture()
|
|
{
|
|
VK_ALLOCATION_CALLBACKS
|
|
|
|
VKContext &context = *VKContext::get();
|
|
vmaDestroyImage(context.mem_allocator_get(), vk_image_, allocation_);
|
|
vkDestroyImageView(context.device_get(), vk_image_view_, vk_allocation_callbacks);
|
|
}
|
|
|
|
void VKTexture::generate_mipmap()
|
|
{
|
|
}
|
|
|
|
void VKTexture::copy_to(Texture * /*tex*/)
|
|
{
|
|
}
|
|
|
|
template<typename T> void copy_color(T dst[4], const T *src)
|
|
{
|
|
dst[0] = src[0];
|
|
dst[1] = src[1];
|
|
dst[2] = src[2];
|
|
dst[3] = src[3];
|
|
}
|
|
|
|
static VkClearColorValue to_vk_clear_color_value(eGPUDataFormat format, const void *data)
|
|
{
|
|
VkClearColorValue result = {0.0f};
|
|
switch (format) {
|
|
case GPU_DATA_FLOAT: {
|
|
const float *float_data = static_cast<const float *>(data);
|
|
copy_color<float>(result.float32, float_data);
|
|
break;
|
|
}
|
|
|
|
case GPU_DATA_INT: {
|
|
const int32_t *int_data = static_cast<const int32_t *>(data);
|
|
copy_color<int32_t>(result.int32, int_data);
|
|
break;
|
|
}
|
|
|
|
case GPU_DATA_UINT: {
|
|
const uint32_t *uint_data = static_cast<const uint32_t *>(data);
|
|
copy_color<uint32_t>(result.uint32, uint_data);
|
|
break;
|
|
}
|
|
|
|
case GPU_DATA_HALF_FLOAT:
|
|
case GPU_DATA_UBYTE:
|
|
case GPU_DATA_UINT_24_8:
|
|
case GPU_DATA_10_11_11_REV:
|
|
case GPU_DATA_2_10_10_10_REV: {
|
|
BLI_assert_unreachable();
|
|
break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void VKTexture::clear(eGPUDataFormat format, const void *data)
|
|
{
|
|
if (!is_allocated()) {
|
|
allocate();
|
|
}
|
|
|
|
VKContext &context = *VKContext::get();
|
|
VKCommandBuffer &command_buffer = context.command_buffer_get();
|
|
VkClearColorValue clear_color = to_vk_clear_color_value(format, data);
|
|
VkImageSubresourceRange range = {0};
|
|
range.aspectMask = to_vk_image_aspect_flag_bits(format_);
|
|
range.levelCount = VK_REMAINING_MIP_LEVELS;
|
|
range.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
|
|
|
command_buffer.clear(
|
|
vk_image_, VK_IMAGE_LAYOUT_GENERAL, clear_color, Span<VkImageSubresourceRange>(&range, 1));
|
|
}
|
|
|
|
void VKTexture::swizzle_set(const char /*swizzle_mask*/[4])
|
|
{
|
|
}
|
|
|
|
void VKTexture::stencil_texture_mode_set(bool /*use_stencil*/)
|
|
{
|
|
}
|
|
|
|
void VKTexture::mip_range_set(int /*min*/, int /*max*/)
|
|
{
|
|
}
|
|
|
|
void *VKTexture::read(int mip, eGPUDataFormat format)
|
|
{
|
|
/* Vulkan images cannot be directly mapped to host memory and requires a staging buffer. */
|
|
VKContext &context = *VKContext::get();
|
|
VKBuffer staging_buffer;
|
|
|
|
/* NOTE: mip_size_get() won't override any dimension that is equal to 0. */
|
|
int extent[3] = {1, 1, 1};
|
|
mip_size_get(mip, extent);
|
|
size_t sample_len = extent[0] * extent[1] * extent[2];
|
|
size_t device_memory_size = sample_len * to_bytesize(format_);
|
|
size_t host_memory_size = sample_len * to_bytesize(format_, format);
|
|
|
|
staging_buffer.create(
|
|
context, device_memory_size, GPU_USAGE_DEVICE_ONLY, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
|
|
|
VkBufferImageCopy region = {};
|
|
region.imageExtent.width = extent[0];
|
|
region.imageExtent.height = extent[1];
|
|
region.imageExtent.depth = extent[2];
|
|
region.imageSubresource.aspectMask = to_vk_image_aspect_flag_bits(format_);
|
|
region.imageSubresource.mipLevel = mip;
|
|
region.imageSubresource.layerCount = 1;
|
|
|
|
VKCommandBuffer &command_buffer = context.command_buffer_get();
|
|
command_buffer.copy(staging_buffer, *this, Span<VkBufferImageCopy>(®ion, 1));
|
|
command_buffer.submit();
|
|
|
|
void *data = MEM_mallocN(host_memory_size, __func__);
|
|
convert_device_to_host(data, staging_buffer.mapped_memory_get(), sample_len, format, format_);
|
|
return data;
|
|
}
|
|
|
|
void VKTexture::update_sub(
|
|
int mip, int offset[3], int extent[3], eGPUDataFormat format, const void *data)
|
|
{
|
|
if (!is_allocated()) {
|
|
allocate();
|
|
}
|
|
|
|
/* Vulkan images cannot be directly mapped to host memory and requires a staging buffer. */
|
|
VKContext &context = *VKContext::get();
|
|
VKBuffer staging_buffer;
|
|
size_t sample_len = extent[0] * extent[1] * extent[2];
|
|
size_t device_memory_size = sample_len * to_bytesize(format_);
|
|
|
|
staging_buffer.create(
|
|
context, device_memory_size, GPU_USAGE_DEVICE_ONLY, VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
|
|
convert_host_to_device(staging_buffer.mapped_memory_get(), data, sample_len, format, format_);
|
|
|
|
VkBufferImageCopy region = {};
|
|
region.imageExtent.width = extent[0];
|
|
region.imageExtent.height = extent[1];
|
|
region.imageExtent.depth = extent[2];
|
|
region.imageOffset.x = offset[0];
|
|
region.imageOffset.y = offset[1];
|
|
region.imageOffset.z = offset[2];
|
|
region.imageSubresource.aspectMask = to_vk_image_aspect_flag_bits(format_);
|
|
region.imageSubresource.mipLevel = mip;
|
|
region.imageSubresource.layerCount = 1;
|
|
|
|
VKCommandBuffer &command_buffer = context.command_buffer_get();
|
|
command_buffer.copy(*this, staging_buffer, Span<VkBufferImageCopy>(®ion, 1));
|
|
command_buffer.submit();
|
|
}
|
|
|
|
void VKTexture::update_sub(int /*offset*/[3],
|
|
int /*extent*/[3],
|
|
eGPUDataFormat /*format*/,
|
|
GPUPixelBuffer * /*pixbuf*/)
|
|
{
|
|
}
|
|
|
|
/* TODO(fclem): Legacy. Should be removed at some point. */
|
|
uint VKTexture::gl_bindcode_get() const
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
bool VKTexture::init_internal()
|
|
{
|
|
/* Initialization can only happen after the usage is known. By the current API this isn't set
|
|
* at this moment, so we cannot initialize here. The initialization is postponed until the
|
|
* allocation of the texture on the device. */
|
|
|
|
/* TODO: return false when texture format isn't supported. */
|
|
return true;
|
|
}
|
|
|
|
bool VKTexture::init_internal(GPUVertBuf * /*vbo*/)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool VKTexture::init_internal(const GPUTexture * /*src*/, int /*mip_offset*/, int /*layer_offset*/)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool VKTexture::is_allocated()
|
|
{
|
|
return vk_image_ != VK_NULL_HANDLE && allocation_ != VK_NULL_HANDLE;
|
|
}
|
|
|
|
bool VKTexture::allocate()
|
|
{
|
|
BLI_assert(!is_allocated());
|
|
|
|
int extent[3] = {1, 1, 1};
|
|
mip_size_get(0, extent);
|
|
|
|
VKContext &context = *VKContext::get();
|
|
VkImageCreateInfo image_info = {};
|
|
image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
image_info.imageType = to_vk_image_type(type_);
|
|
image_info.extent.width = extent[0];
|
|
image_info.extent.height = extent[1];
|
|
image_info.extent.depth = extent[2];
|
|
image_info.mipLevels = 1;
|
|
image_info.arrayLayers = 1;
|
|
image_info.format = to_vk_format(format_);
|
|
image_info.tiling = VK_IMAGE_TILING_LINEAR;
|
|
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
image_info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
|
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT;
|
|
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
|
|
VkResult result;
|
|
if (G.debug & G_DEBUG_GPU) {
|
|
VkImageFormatProperties image_format = {};
|
|
result = vkGetPhysicalDeviceImageFormatProperties(context.physical_device_get(),
|
|
image_info.format,
|
|
image_info.imageType,
|
|
image_info.tiling,
|
|
image_info.usage,
|
|
image_info.flags,
|
|
&image_format);
|
|
if (result != VK_SUCCESS) {
|
|
printf("Image type not supported on device.\n");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
VmaAllocationCreateInfo allocCreateInfo = {};
|
|
allocCreateInfo.usage = VMA_MEMORY_USAGE_AUTO;
|
|
allocCreateInfo.flags = static_cast<VmaAllocationCreateFlagBits>(
|
|
VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT);
|
|
allocCreateInfo.priority = 1.0f;
|
|
result = vmaCreateImage(context.mem_allocator_get(),
|
|
&image_info,
|
|
&allocCreateInfo,
|
|
&vk_image_,
|
|
&allocation_,
|
|
nullptr);
|
|
if (result != VK_SUCCESS) {
|
|
return false;
|
|
}
|
|
|
|
/* Promote image to the correct layout. */
|
|
VkImageMemoryBarrier barrier{};
|
|
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
barrier.image = vk_image_;
|
|
barrier.subresourceRange.aspectMask = to_vk_image_aspect_flag_bits(format_);
|
|
barrier.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS;
|
|
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
|
context.command_buffer_get().pipeline_barrier(Span<VkImageMemoryBarrier>(&barrier, 1));
|
|
|
|
VK_ALLOCATION_CALLBACKS
|
|
VkImageViewCreateInfo image_view_info = {};
|
|
image_view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
|
|
image_view_info.image = vk_image_;
|
|
image_view_info.viewType = to_vk_image_view_type(type_);
|
|
image_view_info.format = to_vk_format(format_);
|
|
image_view_info.components = to_vk_component_mapping(format_);
|
|
image_view_info.subresourceRange.aspectMask = to_vk_image_aspect_flag_bits(format_);
|
|
image_view_info.subresourceRange.levelCount = VK_REMAINING_MIP_LEVELS;
|
|
image_view_info.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
|
|
|
result = vkCreateImageView(
|
|
context.device_get(), &image_view_info, vk_allocation_callbacks, &vk_image_view_);
|
|
return result == VK_SUCCESS;
|
|
}
|
|
|
|
void VKTexture::image_bind(int binding)
|
|
{
|
|
if (!is_allocated()) {
|
|
allocate();
|
|
}
|
|
VKContext &context = *VKContext::get();
|
|
VKShader *shader = static_cast<VKShader *>(context.shader);
|
|
const VKShaderInterface &shader_interface = shader->interface_get();
|
|
const VKDescriptorSet::Location location = shader_interface.descriptor_set_location(
|
|
shader::ShaderCreateInfo::Resource::BindType::IMAGE, binding);
|
|
shader->pipeline_get().descriptor_set_get().image_bind(*this, location);
|
|
}
|
|
|
|
} // namespace blender::gpu
|