Improve Vulkan Blit: caching, interpolation, swizzle, enable flags

- Cache a VkFramebuffer per texture to avoid creating/destroying one for
  every Blit() call.
- Create and cache both linear and nearest samplers; honor
  Texture::Interpolation in descriptor writes.
- Add single-channel image-view component swizzle (R -> RGB, A = 1) to
  match OpenGL grayscale behavior.
- Set texture-enable uniforms (NAME_enabled) when shaders declare them.
- Guard Blit() with null destination and emit a clear warning instead of
  recording an invalid render pass.

ctest passes 4/4 in both dynamic-backend ON and OFF builds.
This commit is contained in:
2026-07-13 10:19:30 +08:00
parent f4906862a1
commit e908786d43
2 changed files with 100 additions and 18 deletions
+97 -18
View File
@@ -20,6 +20,7 @@ struct VulkanRenderer::VulkanTexture {
VkImage image = VK_NULL_HANDLE; VkImage image = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
int width = 0; int width = 0;
int height = 0; int height = 0;
int depth = 0; int depth = 0;
@@ -84,6 +85,7 @@ void VulkanRenderer::PostInit()
} }
CreateVertexBuffer(); CreateVertexBuffer();
CreateLinearSampler(); CreateLinearSampler();
CreateNearestSampler();
} }
void VulkanRenderer::PostDestroy() void VulkanRenderer::PostDestroy()
@@ -142,6 +144,10 @@ void VulkanRenderer::DestroyInternal()
vkDestroySampler(device_, linear_sampler_, nullptr); vkDestroySampler(device_, linear_sampler_, nullptr);
linear_sampler_ = VK_NULL_HANDLE; linear_sampler_ = VK_NULL_HANDLE;
} }
if (nearest_sampler_ != VK_NULL_HANDLE) {
vkDestroySampler(device_, nearest_sampler_, nullptr);
nearest_sampler_ = VK_NULL_HANDLE;
}
if (vertex_buffer_ != VK_NULL_HANDLE) { if (vertex_buffer_ != VK_NULL_HANDLE) {
vkDestroyBuffer(device_, vertex_buffer_, nullptr); vkDestroyBuffer(device_, vertex_buffer_, nullptr);
@@ -505,6 +511,44 @@ bool VulkanRenderer::CreateLinearSampler()
return true; return true;
} }
bool VulkanRenderer::CreateNearestSampler()
{
VkSamplerCreateInfo sampler_info = {};
sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampler_info.magFilter = VK_FILTER_NEAREST;
sampler_info.minFilter = VK_FILTER_NEAREST;
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.anisotropyEnable = VK_FALSE;
sampler_info.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
sampler_info.unnormalizedCoordinates = VK_FALSE;
sampler_info.compareEnable = VK_FALSE;
sampler_info.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
sampler_info.mipLodBias = 0.0f;
sampler_info.minLod = 0.0f;
sampler_info.maxLod = 0.0f;
VkResult result = vkCreateSampler(device_, &sampler_info, nullptr, &nearest_sampler_);
if (result != VK_SUCCESS) {
qWarning() << "Failed to create Vulkan nearest sampler:" << result;
return false;
}
return true;
}
VkSampler VulkanRenderer::GetSampler(Texture::Interpolation interpolation) const
{
switch (interpolation) {
case Texture::kNearest:
return nearest_sampler_ != VK_NULL_HANDLE ? nearest_sampler_ : linear_sampler_;
case Texture::kLinear:
case Texture::kMipmappedLinear:
default:
return linear_sampler_;
}
}
bool VulkanRenderer::CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer, bool VulkanRenderer::CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer,
VkDeviceMemory *out_memory) VkDeviceMemory *out_memory)
{ {
@@ -919,6 +963,15 @@ QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth,
view_info.subresourceRange.baseArrayLayer = 0; view_info.subresourceRange.baseArrayLayer = 0;
view_info.subresourceRange.layerCount = 1; view_info.subresourceRange.layerCount = 1;
// Single-channel textures are typically intended as grayscale. Replicate the
// red channel to RGB and force alpha to 1, matching OpenGL's swizzle behavior.
if (channel_count == 1) {
view_info.components.r = VK_COMPONENT_SWIZZLE_R;
view_info.components.g = VK_COMPONENT_SWIZZLE_R;
view_info.components.b = VK_COMPONENT_SWIZZLE_R;
view_info.components.a = VK_COMPONENT_SWIZZLE_ONE;
}
result = vkCreateImageView(device_, &view_info, nullptr, &tex->view); result = vkCreateImageView(device_, &view_info, nullptr, &tex->view);
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
vkFreeMemory(device_, tex->memory, nullptr); vkFreeMemory(device_, tex->memory, nullptr);
@@ -989,6 +1042,9 @@ void VulkanRenderer::DestroyNativeTexture(QVariant texture)
if (!tex) { if (!tex) {
return; return;
} }
if (tex->framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, tex->framebuffer, nullptr);
}
if (tex->view != VK_NULL_HANDLE) { if (tex->view != VK_NULL_HANDLE) {
vkDestroyImageView(device_, tex->view, nullptr); vkDestroyImageView(device_, tex->view, nullptr);
} }
@@ -1815,9 +1871,14 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
if (!dest_tex) { if (!dest_tex) {
return; return;
} }
} else {
// TODO: support rendering to a temporary offscreen texture when the
// caller requests the default output (used by OpenGL direct-to-widget).
qWarning() << "VulkanRenderer::Blit with null destination is not implemented";
return;
} }
VkFormat render_pass_format = dest_tex ? dest_tex->vk_format : VK_FORMAT_R32G32B32A32_SFLOAT; VkFormat render_pass_format = dest_tex->vk_format;
VkRenderPass render_pass = GetOrCreateRenderPass(render_pass_format, clear_destination); VkRenderPass render_pass = GetOrCreateRenderPass(render_pass_format, clear_destination);
if (render_pass == VK_NULL_HANDLE) { if (render_pass == VK_NULL_HANDLE) {
return; return;
@@ -1931,18 +1992,42 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
} }
} }
// Create framebuffer and render pass for this blit // Set texture-enable flags for shaders that declare uniform bool NAME_enabled.
if (shader->ubo_size > 0) {
for (const TextureBinding &tb : bindings) {
QString enabled_name = tb.name + QStringLiteral("_enabled");
for (const UniformInfo &u : shader->uniforms) {
if (u.name == enabled_name && u.size == sizeof(int)) {
char *dst = ubo_data.data() + static_cast<int>(u.offset);
*reinterpret_cast<int *>(dst) = tb.tex ? 1 : 0;
break;
}
}
}
}
// Lazily create a per-texture framebuffer. The framebuffer is compatible
// with any render pass that uses the same format and sample count, so we
// build it once with the non-clear variant and reuse it.
VkFramebuffer framebuffer = VK_NULL_HANDLE; VkFramebuffer framebuffer = VK_NULL_HANDLE;
if (dest_tex) { if (dest_tex) {
VkFramebufferCreateInfo fb_info = {}; if (dest_tex->framebuffer == VK_NULL_HANDLE) {
fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; VkFramebufferCreateInfo fb_info = {};
fb_info.renderPass = render_pass; fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
fb_info.attachmentCount = 1; fb_info.renderPass = GetOrCreateRenderPass(render_pass_format, false);
fb_info.pAttachments = &dest_tex->view; fb_info.attachmentCount = 1;
fb_info.width = static_cast<uint32_t>(destination_params.effective_width()); fb_info.pAttachments = &dest_tex->view;
fb_info.height = static_cast<uint32_t>(destination_params.effective_height()); fb_info.width = static_cast<uint32_t>(dest_tex->width);
fb_info.layers = 1; fb_info.height = static_cast<uint32_t>(dest_tex->height);
vkCreateFramebuffer(device_, &fb_info, nullptr, &framebuffer); fb_info.layers = 1;
VkResult fb_result = vkCreateFramebuffer(device_, &fb_info, nullptr,
&dest_tex->framebuffer);
if (fb_result != VK_SUCCESS) {
qWarning() << "Failed to create Vulkan framebuffer:" << fb_result;
return;
}
}
framebuffer = dest_tex->framebuffer;
} }
// Create UBO buffer if needed // Create UBO buffer if needed
@@ -1973,9 +2058,6 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
VkResult result = vkAllocateDescriptorSets(device_, &ds_alloc, &descriptor_set); VkResult result = vkAllocateDescriptorSets(device_, &ds_alloc, &descriptor_set);
if (result != VK_SUCCESS) { if (result != VK_SUCCESS) {
qWarning() << "Failed to allocate Vulkan descriptor set:" << result; qWarning() << "Failed to allocate Vulkan descriptor set:" << result;
if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, framebuffer, nullptr);
}
if (ubo_buffer != VK_NULL_HANDLE) { if (ubo_buffer != VK_NULL_HANDLE) {
DestroyStagingBuffer(ubo_buffer, ubo_memory); DestroyStagingBuffer(ubo_buffer, ubo_memory);
} }
@@ -2007,7 +2089,7 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
for (int i = 0; i < bindings.size() && i < 16; i++) { for (int i = 0; i < bindings.size() && i < 16; i++) {
const TextureBinding &tb = bindings.at(i); const TextureBinding &tb = bindings.at(i);
VkDescriptorImageInfo img_info = {}; VkDescriptorImageInfo img_info = {};
img_info.sampler = linear_sampler_; img_info.sampler = GetSampler(tb.interp);
img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE; img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE;
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
image_infos.append(img_info); image_infos.append(img_info);
@@ -2113,9 +2195,6 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
if (descriptor_set != VK_NULL_HANDLE) { if (descriptor_set != VK_NULL_HANDLE) {
vkFreeDescriptorSets(device_, descriptor_pool_, 1, &descriptor_set); vkFreeDescriptorSets(device_, descriptor_pool_, 1, &descriptor_set);
} }
if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, framebuffer, nullptr);
}
if (ubo_buffer != VK_NULL_HANDLE) { if (ubo_buffer != VK_NULL_HANDLE) {
DestroyStagingBuffer(ubo_buffer, ubo_memory); DestroyStagingBuffer(ubo_buffer, ubo_memory);
} }
+3
View File
@@ -88,6 +88,8 @@ private:
bool CreateDescriptorPool(); bool CreateDescriptorPool();
bool CreateVertexBuffer(); bool CreateVertexBuffer();
bool CreateLinearSampler(); bool CreateLinearSampler();
bool CreateNearestSampler();
VkSampler GetSampler(Texture::Interpolation interpolation) const;
bool CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer, bool CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer,
VkDeviceMemory *out_memory); VkDeviceMemory *out_memory);
void DestroyStagingBuffer(VkBuffer buffer, VkDeviceMemory memory); void DestroyStagingBuffer(VkBuffer buffer, VkDeviceMemory memory);
@@ -141,6 +143,7 @@ private:
VkCommandPool command_pool_ = VK_NULL_HANDLE; VkCommandPool command_pool_ = VK_NULL_HANDLE;
VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE; VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE;
VkSampler linear_sampler_ = VK_NULL_HANDLE; VkSampler linear_sampler_ = VK_NULL_HANDLE;
VkSampler nearest_sampler_ = VK_NULL_HANDLE;
QHash<quint64, VkRenderPass> render_pass_cache_; QHash<quint64, VkRenderPass> render_pass_cache_;