Make Vulkan shader/UBO/layout path real enough for end-to-end blits

- Add shared UBO layout across vertex + fragment stages so vertex
  uniforms like ove_mvpmat compile and bind correctly.
- Rewrite uniform extraction/injection to avoid corrupting the UBO
  block and to keep explicit sampler bindings stable.
- Make the UBO and sampler descriptor bindings visible to both vertex
  and fragment stages.
- Add layout locations for vertex varyings (ove_texcoord) and fragment
  inputs so SPIR-V generation succeeds.
- Extend TransitionImageLayout() to cover all layout pairs used by
  upload/download/clear/blit.
- Add subpass dependencies to the cached render pass and leave
  destinations in SHADER_READ_ONLY_OPTIMAL after Blit().
- Move descriptor allocation before command recording and abort cleanly
  if allocation fails; switch Blit() to the persistent linear sampler.
- Add a gtest that creates a Vulkan backend, uploads a red U8 RGBA
  texture, blits through the default pass-through shader, and verifies
  the downloaded pixel is red. This test passes on the current system.

ctest passes 4/4; DynamicRenderBackend.* passes 3/1 (Vulkan fallback
skipped because Vulkan is available).
This commit is contained in:
2026-07-13 10:19:30 +08:00
parent f1d19a03f8
commit f4906862a1
3 changed files with 468 additions and 205 deletions
+376 -202
View File
@@ -3,6 +3,7 @@
#include <QDebug>
#include <QFile>
#include <QRegularExpression>
#include <cstdio>
#include "node/value.h"
#include "render/job/shaderjob.h"
@@ -327,7 +328,7 @@ VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format, bool clear)
color_attachment.format = format;
color_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
color_attachment.loadOp = clear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
VK_ATTACHMENT_LOAD_OP_LOAD;
VK_ATTACHMENT_LOAD_OP_LOAD;
color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
@@ -343,12 +344,28 @@ VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format, bool clear)
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_ref;
// Explicit dependencies so layout transitions and read-after-write are
// correctly synchronized. The destination image is brought in by the
// pipeline barrier before the render pass; here we synchronize the render
// pass output with whatever stage reads it next.
VkSubpassDependency dependency = {};
dependency.srcSubpass = 0;
dependency.dstSubpass = VK_SUBPASS_EXTERNAL;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_TRANSFER_BIT;
dependency.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
dependency.dstAccessMask = VK_ACCESS_SHADER_READ_BIT |
VK_ACCESS_TRANSFER_READ_BIT;
VkRenderPassCreateInfo render_pass_info = {};
render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
render_pass_info.attachmentCount = 1;
render_pass_info.pAttachments = &color_attachment;
render_pass_info.subpassCount = 1;
render_pass_info.pSubpasses = &subpass;
render_pass_info.dependencyCount = 1;
render_pass_info.pDependencies = &dependency;
VkRenderPass render_pass = VK_NULL_HANDLE;
VkResult result = vkCreateRenderPass(device_, &render_pass_info, nullptr,
@@ -362,6 +379,7 @@ VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format, bool clear)
return render_pass;
}
bool VulkanRenderer::CreateVertexBuffer()
{
VkDeviceSize buffer_size = sizeof(kBlitVertices);
@@ -578,8 +596,8 @@ void VulkanRenderer::EndOneTimeCommands(VkCommandBuffer cmd)
}
void VulkanRenderer::TransitionImageLayout(VkCommandBuffer cmd, VkImage image,
VkImageLayout old_layout,
VkImageLayout new_layout)
VkImageLayout old_layout,
VkImageLayout new_layout)
{
VkImageMemoryBarrier barrier = {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
@@ -594,56 +612,97 @@ void VulkanRenderer::TransitionImageLayout(VkCommandBuffer cmd, VkImage image,
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;
VkPipelineStageFlags source_stage;
VkPipelineStageFlags destination_stage;
VkPipelineStageFlags source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkPipelineStageFlags destination_stage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED &&
new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
auto set_transfer = [&]() {
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL &&
new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
};
auto set_shader_read = [&]() {
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED &&
new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
} else if (old_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL &&
new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
source_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL &&
new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
};
auto set_color_attachment = [&]() {
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
} else if (old_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL &&
new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
source_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
} else {
};
if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED) {
barrier.srcAccessMask = 0;
barrier.dstAccessMask = 0;
source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
destination_stage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
}
} else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
set_shader_read();
} else if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
set_color_attachment();
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
set_transfer();
}
} else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
set_transfer();
}
} else if (old_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
source_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
}
} else if (old_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
source_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
if (new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT;
}
}
vkCmdPipelineBarrier(cmd, source_stage, destination_stage, 0, 0, nullptr, 0,
nullptr, 1, &barrier);
}
void VulkanRenderer::CopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer,
VkImage image, uint32_t width,
uint32_t height, uint32_t depth)
@@ -1150,29 +1209,49 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture,
// Shader compilation (GLSL -> SPIR-V via shaderc)
// ------------------------------------------------------------------
static bool IsSamplerType(const QString &type)
{
static const QRegularExpression sampler_re(
QStringLiteral(R"(sampler\d*D|samplerCube|sampler2DArray|sampler3D)"));
return sampler_re.match(type).hasMatch();
}
QString VulkanRenderer::EnsureGlslVersion450(const QString &glsl) const
{
QString result = glsl.trimmed();
if (result.startsWith(QStringLiteral("#version"))) {
int end = result.indexOf(QChar('\n'));
QString rest = (end >= 0) ? result.mid(end + 1) : QString();
return QStringLiteral("#version 450 core\n") + rest;
}
return QStringLiteral("#version 450 core\n") + result;
}
QString VulkanRenderer::ConvertGlslToVulkan(const QString &glsl,
VkShaderStageFlagBits stage)
VkShaderStageFlagBits stage)
{
QString result = glsl;
// Replace version 150 with 450 core
if (result.contains(QStringLiteral("#version 150"))) {
result.replace(QStringLiteral("#version 150"),
QStringLiteral("#version 450 core"));
}
// Olive uses the legacy texture2D/texture3D names in some shaders.
result.replace(QStringLiteral("texture2D("), QStringLiteral("texture("));
result.replace(QStringLiteral("texture3D("), QStringLiteral("texture("));
// Ensure fragment shader output has layout
if (stage == VK_SHADER_STAGE_FRAGMENT_BIT) {
result.replace(QStringLiteral("out vec4 frag_color;"),
QStringLiteral("layout(location = 0) out vec4 frag_color;"));
result.replace(QStringLiteral("in vec2 ove_texcoord;"),
QStringLiteral("layout(location = 0) in vec2 ove_texcoord;"));
}
// Add layout to vertex attributes
// Add layout to vertex attributes and varyings
if (stage == VK_SHADER_STAGE_VERTEX_BIT) {
result.replace(QStringLiteral("in vec4 a_position;"),
QStringLiteral("layout(location = 0) in vec4 a_position;"));
result.replace(QStringLiteral("in vec2 a_texcoord;"),
QStringLiteral("layout(location = 1) in vec2 a_texcoord;"));
result.replace(QStringLiteral("out vec2 ove_texcoord;"),
QStringLiteral("layout(location = 0) out vec2 ove_texcoord;"));
}
return result;
@@ -1200,94 +1279,129 @@ VkDeviceSize VulkanRenderer::GetStd140Alignment(const QString &type) const
return 4;
}
QString VulkanRenderer::ConvertGlslUniformsToUbo(const QString &glsl,
QVector<UniformInfo> *out_uniforms,
int *out_sampler_count)
void VulkanRenderer::ExtractUniforms(const QString &glsl,
QVector<UniformInfo> *out_uniforms,
QVector<QString> *out_samplers) const
{
static const QRegularExpression re(
QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"),
QRegularExpression::MultilineOption);
QRegularExpressionMatchIterator it = re.globalMatch(glsl);
while (it.hasNext()) {
QRegularExpressionMatch m = it.next();
QString type = m.captured(1);
QString name = m.captured(2);
if (IsSamplerType(type)) {
if (out_samplers && !out_samplers->contains(name)) {
out_samplers->append(name);
}
} else if (out_uniforms) {
bool exists = false;
for (const UniformInfo &u : *out_uniforms) {
if (u.name == name) {
exists = true;
break;
}
}
if (!exists) {
UniformInfo info;
info.name = name;
info.type = type;
info.offset = 0;
info.size = 0;
out_uniforms->append(info);
}
}
}
}
void VulkanRenderer::ComputeUniformLayout(QVector<UniformInfo> *uniforms) const
{
VkDeviceSize offset = 0;
for (UniformInfo &info : *uniforms) {
VkDeviceSize align = GetStd140Alignment(info.type);
offset = AlignSize(offset, align);
info.offset = offset;
info.size = GetStd140Size(info.type);
offset += info.size;
}
}
QString VulkanRenderer::BuildUboBlock(const QVector<UniformInfo> &uniforms) const
{
if (uniforms.isEmpty()) {
return QString();
}
QString ubo = QStringLiteral("\nlayout(set = 0, binding = 0) uniform UniformBuffer {\n");
for (const UniformInfo &info : uniforms) {
ubo += QStringLiteral(" %1 %2;\n").arg(info.type, info.name);
}
ubo += QStringLiteral("} ubo;\n\n");
return ubo;
}
QString VulkanRenderer::RewriteShaderWithUbo(
const QString &glsl,
const QVector<UniformInfo> &all_uniforms,
const QHash<QString, int> &sampler_bindings) const
{
QString result = glsl;
QVector<UniformInfo> uniforms;
// Regex to match uniform declarations like: uniform vec4 color;
QRegularExpression re(QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"),
QRegularExpression::MultilineOption);
QRegularExpression sampler_re(QStringLiteral(R"(sampler\d*D|samplerCube|sampler2DArray)"));
static const QRegularExpression re(
QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"),
QRegularExpression::MultilineOption);
int offset = 0;
int sampler_count = 0;
// First pass: replace sampler declarations with explicit bindings and remove
// plain uniform declarations. Process in reverse so indices stay valid.
QRegularExpressionMatchIterator it = re.globalMatch(result);
QVector<QRegularExpressionMatch> matches;
while (it.hasNext()) {
matches.append(it.next());
}
// Process in reverse order so removal doesn't shift indices
for (int i = matches.size() - 1; i >= 0; --i) {
const QRegularExpressionMatch &m = matches[i];
QString type = m.captured(1);
QString name = m.captured(2);
if (sampler_re.match(type).hasMatch()) {
// Assign explicit binding per sampler. Binding 0 is reserved for the
// UBO, so samplers start at binding 1.
const int binding = 1 + sampler_count;
QString new_decl = QStringLiteral(
"layout(set = 0, binding = %1) uniform %2 %3;")
.arg(binding).arg(type, name);
result.replace(m.capturedStart(), m.capturedLength(), new_decl);
sampler_count++;
continue;
if (IsSamplerType(type)) {
int binding = sampler_bindings.value(name, -1);
if (binding >= 0) {
QString new_decl = QStringLiteral(
"layout(set = 0, binding = %1) uniform %2 %3;")
.arg(binding).arg(type, name);
result.replace(m.capturedStart(), m.capturedLength(), new_decl);
}
} else {
result.remove(m.capturedStart(), m.capturedLength());
}
UniformInfo info;
info.name = name;
info.type = type;
VkDeviceSize align = GetStd140Alignment(type);
offset = static_cast<int>(AlignSize(offset, align));
info.offset = offset;
info.size = GetStd140Size(type);
offset += static_cast<int>(info.size);
uniforms.prepend(info);
// Remove the original declaration
result.remove(m.capturedStart(), m.capturedLength());
}
if (!uniforms.isEmpty()) {
// Build UBO block
QString ubo = QStringLiteral("\nlayout(set = 0, binding = 0) uniform UniformBuffer {\n");
for (const UniformInfo &info : uniforms) {
ubo += QStringLiteral(" %1 %2;\n").arg(info.type, info.name);
}
ubo += QStringLiteral("} ubo;\n\n");
// Second pass: rewrite bare uniform names to ubo.name. The UBO block has not
// been inserted yet, so we cannot accidentally rewrite names inside it.
for (const UniformInfo &info : all_uniforms) {
QString old_name = QStringLiteral("\\b%1\\b").arg(info.name);
QString new_name = QStringLiteral("ubo.%1").arg(info.name);
result.replace(QRegularExpression(old_name), new_name);
}
// Insert UBO after #version line or at the beginning
int version_end = result.indexOf(QStringLiteral("\n"));
// Third pass: insert the shared UBO block after the #version line.
if (!all_uniforms.isEmpty()) {
QString ubo = BuildUboBlock(all_uniforms);
int version_end = result.indexOf(QChar('\n'));
if (version_end >= 0 && result.startsWith(QStringLiteral("#version"))) {
result.insert(version_end + 1, ubo);
} else {
result.prepend(ubo);
}
// Replace bare uniform names with ubo.name in the rest of the shader
// We do this carefully to avoid replacing the names inside the UBO block itself
for (const UniformInfo &info : uniforms) {
QString old_name = QStringLiteral("\\b%1\\b").arg(info.name);
QString new_name = QStringLiteral("ubo.%1").arg(info.name);
result.replace(QRegularExpression(old_name), new_name);
}
// Fix double ubo.ubo. that may have been created
result.replace(QStringLiteral("ubo.ubo."), QStringLiteral("ubo."));
}
if (out_uniforms) {
*out_uniforms = uniforms;
}
if (out_sampler_count) {
*out_sampler_count = sampler_count;
}
return result;
}
bool VulkanRenderer::CompileGlslToSpv(const QString &glsl,
VkShaderStageFlagBits stage,
QByteArray *out_spv)
@@ -1300,7 +1414,8 @@ bool VulkanRenderer::CompileGlslToSpv(const QString &glsl,
}
shaderc_compile_options_t options = shaderc_compile_options_initialize();
shaderc_compile_options_set_auto_bind_uniforms(options, true);
// Bindings are set explicitly by RewriteShaderWithUbo; do not let shaderc
// reassign them.
shaderc_compile_options_set_optimization_level(
options, shaderc_optimization_level_performance);
@@ -1329,8 +1444,8 @@ bool VulkanRenderer::CompileGlslToSpv(const QString &glsl,
if (shaderc_result_get_compilation_status(compile_result) !=
shaderc_compilation_status_success) {
qWarning() << "shaderc compilation failed:"
<< shaderc_result_get_error_message(compile_result);
fprintf(stderr, "shaderc compilation failed: %s\n",
shaderc_result_get_error_message(compile_result));
shaderc_result_release(compile_result);
shaderc_compiler_release(compiler);
return false;
@@ -1373,25 +1488,69 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code)
QStringLiteral(":/shaders/default.frag"));
}
// Convert fragment shader uniforms to UBO before compiling
QVector<UniformInfo> frag_uniforms;
int sampler_count = 0;
QString converted_frag = ConvertGlslUniformsToUbo(frag_code, &frag_uniforms,
&sampler_count);
// Make sure both stages declare a Vulkan-compatible version.
vert_code = EnsureGlslVersion450(vert_code);
frag_code = EnsureGlslVersion450(frag_code);
if (!CompileGlslToSpv(vert_code, VK_SHADER_STAGE_VERTEX_BIT, &vert_spv)) {
// Extract uniforms and samplers from both stages. We build a single shared
// UBO layout and a single sampler binding table so both vertex and fragment
// shaders see the same descriptor set.
QVector<UniformInfo> vert_uniforms;
QVector<UniformInfo> frag_uniforms;
QVector<QString> vert_samplers;
QVector<QString> frag_samplers;
ExtractUniforms(vert_code, &vert_uniforms, &vert_samplers);
ExtractUniforms(frag_code, &frag_uniforms, &frag_samplers);
QVector<UniformInfo> all_uniforms = vert_uniforms;
for (const UniformInfo &fu : frag_uniforms) {
bool exists = false;
for (const UniformInfo &u : all_uniforms) {
if (u.name == fu.name) {
exists = true;
break;
}
}
if (!exists) {
all_uniforms.append(fu);
}
}
ComputeUniformLayout(&all_uniforms);
QVector<QString> all_samplers = vert_samplers;
for (const QString &name : frag_samplers) {
if (!all_samplers.contains(name)) {
all_samplers.append(name);
}
}
QHash<QString, int> sampler_bindings;
for (int i = 0; i < all_samplers.size(); ++i) {
sampler_bindings[all_samplers[i]] = 1 + i;
}
QString converted_vert = RewriteShaderWithUbo(vert_code, all_uniforms, sampler_bindings);
QString converted_frag = RewriteShaderWithUbo(frag_code, all_uniforms, sampler_bindings);
converted_vert = ConvertGlslToVulkan(converted_vert, VK_SHADER_STAGE_VERTEX_BIT);
converted_frag = ConvertGlslToVulkan(converted_frag, VK_SHADER_STAGE_FRAGMENT_BIT);
if (!CompileGlslToSpv(converted_vert, VK_SHADER_STAGE_VERTEX_BIT, &vert_spv)) {
fprintf(stderr, "Failed to compile Vulkan vertex shader:\n%s\n",
converted_vert.toUtf8().constData());
return QVariant();
}
if (!CompileGlslToSpv(converted_frag, VK_SHADER_STAGE_FRAGMENT_BIT, &frag_spv)) {
fprintf(stderr, "Failed to compile Vulkan fragment shader:\n%s\n",
converted_frag.toUtf8().constData());
return QVariant();
}
VulkanShader *sh = new VulkanShader();
sh->id = next_shader_id_++;
sh->uniforms = frag_uniforms;
sh->sampler_count = sampler_count;
sh->uniforms = all_uniforms;
sh->sampler_count = all_samplers.size();
sh->ubo_size = 0;
for (const UniformInfo &u : frag_uniforms) {
for (const UniformInfo &u : all_uniforms) {
sh->ubo_size = qMax(sh->ubo_size, u.offset + u.size);
}
@@ -1401,7 +1560,7 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code)
vert_info.pCode = reinterpret_cast<const uint32_t *>(vert_spv.constData());
VkResult result = vkCreateShaderModule(device_, &vert_info, nullptr,
&sh->vert_module);
&sh->vert_module);
if (result != VK_SUCCESS) {
delete sh;
return QVariant();
@@ -1419,24 +1578,28 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code)
return QVariant();
}
// Create descriptor set layout: binding 0 = UBO, binding 1..N = samplers
// Create descriptor set layout: binding 0 = shared UBO, binding 1..N = samplers
QVector<VkDescriptorSetLayoutBinding> bindings;
bindings.reserve(1 + sampler_count);
bindings.reserve(1 + all_samplers.size());
VkDescriptorSetLayoutBinding ubo_binding = {};
ubo_binding.binding = 0;
ubo_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
ubo_binding.descriptorCount = 1;
ubo_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
bindings.append(ubo_binding);
if (!all_uniforms.isEmpty()) {
VkDescriptorSetLayoutBinding ubo_binding = {};
ubo_binding.binding = 0;
ubo_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
ubo_binding.descriptorCount = 1;
ubo_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT |
VK_SHADER_STAGE_FRAGMENT_BIT;
bindings.append(ubo_binding);
}
for (int i = 0; i < sampler_count; ++i) {
for (int i = 0; i < all_samplers.size(); ++i) {
VkDescriptorSetLayoutBinding sampler_binding = {};
sampler_binding.binding = 1 + i;
sampler_binding.descriptorType =
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
sampler_binding.descriptorCount = 1;
sampler_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
sampler_binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT |
VK_SHADER_STAGE_FRAGMENT_BIT;
bindings.append(sampler_binding);
}
@@ -1473,6 +1636,7 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code)
return QVariant::fromValue(sh->id);
}
void VulkanRenderer::DestroyNativeShader(QVariant shader)
{
QMutexLocker lock(&mutex_);
@@ -1793,6 +1957,79 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
}
}
// Allocate and update descriptors before recording commands so we can bail
// out cleanly if descriptor allocation fails.
VkDescriptorSet descriptor_set = VK_NULL_HANDLE;
VkDescriptorBufferInfo buffer_info = {};
QVector<VkDescriptorImageInfo> image_infos;
bool descriptors_needed = (shader->ubo_size > 0 || !bindings.isEmpty());
if (descriptors_needed) {
VkDescriptorSetAllocateInfo ds_alloc = {};
ds_alloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
ds_alloc.descriptorPool = descriptor_pool_;
ds_alloc.descriptorSetCount = 1;
ds_alloc.pSetLayouts = &shader->descriptor_layout;
VkResult result = vkAllocateDescriptorSets(device_, &ds_alloc, &descriptor_set);
if (result != VK_SUCCESS) {
qWarning() << "Failed to allocate Vulkan descriptor set:" << result;
if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, framebuffer, nullptr);
}
if (ubo_buffer != VK_NULL_HANDLE) {
DestroyStagingBuffer(ubo_buffer, ubo_memory);
}
return;
}
QVector<VkWriteDescriptorSet> writes;
// UBO binding
if (ubo_buffer != VK_NULL_HANDLE) {
buffer_info.buffer = ubo_buffer;
buffer_info.offset = 0;
buffer_info.range = shader->ubo_size;
VkWriteDescriptorSet write = {};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptor_set;
write.dstBinding = 0;
write.dstArrayElement = 0;
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
write.descriptorCount = 1;
write.pBufferInfo = &buffer_info;
writes.append(write);
}
// Texture samplers binding
if (!bindings.isEmpty()) {
image_infos.reserve(qMin(bindings.size(), 16));
for (int i = 0; i < bindings.size() && i < 16; i++) {
const TextureBinding &tb = bindings.at(i);
VkDescriptorImageInfo img_info = {};
img_info.sampler = linear_sampler_;
img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE;
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
image_infos.append(img_info);
VkWriteDescriptorSet write = {};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptor_set;
write.dstBinding = 1 + i;
write.dstArrayElement = 0;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.descriptorCount = 1;
write.pImageInfo = &image_infos.last();
writes.append(write);
}
}
if (!writes.isEmpty()) {
vkUpdateDescriptorSets(device_, writes.size(), writes.constData(), 0,
nullptr);
}
}
VkCommandBuffer cmd = BeginOneTimeCommands();
if (dest_tex && dest_tex->current_layout != VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) {
@@ -1849,93 +2086,30 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
VkDeviceSize offsets[] = { 0 };
vkCmdBindVertexBuffers(cmd, 0, 1, &vertex_buffer_, offsets);
// Create and update descriptor set
VkDescriptorSet descriptor_set = VK_NULL_HANDLE;
VkSampler sampler = VK_NULL_HANDLE;
VkDescriptorBufferInfo buffer_info = {};
QVector<VkDescriptorImageInfo> image_infos;
if (shader->ubo_size > 0 || !bindings.isEmpty()) {
VkDescriptorSetAllocateInfo ds_alloc = {};
ds_alloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
ds_alloc.descriptorPool = descriptor_pool_;
ds_alloc.descriptorSetCount = 1;
ds_alloc.pSetLayouts = &shader->descriptor_layout;
VkResult result = vkAllocateDescriptorSets(device_, &ds_alloc, &descriptor_set);
if (result == VK_SUCCESS) {
QVector<VkWriteDescriptorSet> writes;
// UBO binding
if (ubo_buffer != VK_NULL_HANDLE) {
buffer_info.buffer = ubo_buffer;
buffer_info.offset = 0;
buffer_info.range = shader->ubo_size;
VkWriteDescriptorSet write = {};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptor_set;
write.dstBinding = 0;
write.dstArrayElement = 0;
write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
write.descriptorCount = 1;
write.pBufferInfo = &buffer_info;
writes.append(write);
}
// Texture samplers binding
if (!bindings.isEmpty()) {
VkSamplerCreateInfo sampler_info = {};
sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
sampler_info.magFilter = VK_FILTER_LINEAR;
sampler_info.minFilter = VK_FILTER_LINEAR;
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;
vkCreateSampler(device_, &sampler_info, nullptr, &sampler);
image_infos.reserve(qMin(bindings.size(), 16));
for (int i = 0; i < bindings.size() && i < 16; i++) {
const TextureBinding &tb = bindings.at(i);
VkDescriptorImageInfo img_info = {};
img_info.sampler = sampler;
img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE;
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
image_infos.append(img_info);
VkWriteDescriptorSet write = {};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptor_set;
write.dstBinding = 1 + i;
write.dstArrayElement = 0;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.descriptorCount = 1;
write.pImageInfo = &image_infos.last();
writes.append(write);
}
}
if (!writes.isEmpty()) {
vkUpdateDescriptorSets(device_, writes.size(), writes.constData(), 0,
nullptr);
}
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
// Bind descriptor set
if (descriptor_set != VK_NULL_HANDLE) {
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
shader->pipeline_layout, 0, 1, &descriptor_set, 0,
nullptr);
}
}
// Draw
vkCmdDraw(cmd, 6, 1, 0, 0);
vkCmdEndRenderPass(cmd);
// Leave the destination in a shader-readable state so it can be sampled or
// downloaded without an extra layout transition on the caller side.
if (dest_tex) {
TransitionImageLayout(cmd, dest_tex->image,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
dest_tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
EndOneTimeCommands(cmd);
if (sampler != VK_NULL_HANDLE) {
vkDestroySampler(device_, sampler, nullptr);
}
if (descriptor_set != VK_NULL_HANDLE) {
vkFreeDescriptorSets(device_, descriptor_pool_, 1, &descriptor_set);
}
+8 -3
View File
@@ -115,9 +115,14 @@ private:
bool CompileGlslToSpv(const QString &glsl, VkShaderStageFlagBits stage,
QByteArray *out_spv);
QString ConvertGlslToVulkan(const QString &glsl, VkShaderStageFlagBits stage);
QString ConvertGlslUniformsToUbo(const QString &glsl,
QVector<UniformInfo> *out_uniforms,
int *out_sampler_count = nullptr);
QString EnsureGlslVersion450(const QString &glsl) const;
void ExtractUniforms(const QString &glsl, QVector<UniformInfo> *out_uniforms,
QVector<QString> *out_samplers) const;
void ComputeUniformLayout(QVector<UniformInfo> *uniforms) const;
QString BuildUboBlock(const QVector<UniformInfo> &uniforms) const;
QString RewriteShaderWithUbo(const QString &glsl,
const QVector<UniformInfo> &all_uniforms,
const QHash<QString, int> &sampler_bindings) const;
VkDeviceSize GetStd140Size(const QString &type) const;
VkDeviceSize GetStd140Alignment(const QString &type) const;
@@ -1,6 +1,14 @@
#include <gtest/gtest.h>
#include <QByteArray>
#include <QMatrix4x4>
#include "node/value.h"
#include "render/backend/dynamicrenderer.h"
#include "render/job/shaderjob.h"
#include "render/shadercode.h"
#include "render/texture.h"
#include "render/videoparams.h"
TEST(DynamicRenderBackend, LoadsExperimentalOpenGLBackend)
{
@@ -62,3 +70,79 @@ TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable)
EXPECT_STREQ(info.name, "opengl");
#endif
}
TEST(DynamicRenderBackend, VulkanUploadBlitDownload)
{
#ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
GTEST_SKIP() << "Dynamic render backend is not enabled in this build";
#else
olive::DynamicRenderer renderer(QStringLiteral("vulkan"));
ASSERT_TRUE(renderer.Load());
OakRenderBackendInfo info = {};
ASSERT_TRUE(renderer.GetBackendInfo(&info));
if (info.kind != OAK_RENDER_BACKEND_VULKAN) {
GTEST_SKIP() << "Vulkan backend is not available on this system";
}
ASSERT_TRUE(renderer.Init());
renderer.PostInit();
const int kSize = 64;
olive::VideoParams params(kSize, kSize, olive::PixelFormat::U8,
olive::VideoParams::kRGBAChannelCount);
olive::TexturePtr src = renderer.CreateTexture(params);
ASSERT_NE(src, nullptr);
ASSERT_FALSE(src->IsDummy());
QByteArray src_data(kSize * kSize * 4, 0);
for (int i = 0; i < kSize * kSize; ++i) {
src_data[i * 4 + 0] = static_cast<char>(255); // R
src_data[i * 4 + 1] = static_cast<char>(0); // G
src_data[i * 4 + 2] = static_cast<char>(0); // B
src_data[i * 4 + 3] = static_cast<char>(255); // A
}
src->Upload(src_data.data(), kSize * 4);
olive::TexturePtr dst = renderer.CreateTexture(params);
ASSERT_NE(dst, nullptr);
ASSERT_FALSE(dst->IsDummy());
const QString vert = QStringLiteral(
"uniform mat4 ove_mvpmat;\n"
"in vec4 a_position;\n"
"in vec2 a_texcoord;\n"
"out vec2 ove_texcoord;\n"
"void main() {\n"
" gl_Position = ove_mvpmat * a_position;\n"
" ove_texcoord = a_texcoord;\n"
"}\n");
const QString frag = QStringLiteral(
"uniform sampler2D ove_maintex;\n"
"in vec2 ove_texcoord;\n"
"out vec4 frag_color;\n"
"void main() {\n"
" frag_color = texture(ove_maintex, ove_texcoord);\n"
"}\n");
QVariant shader = renderer.CreateNativeShader(olive::ShaderCode(frag, vert));
ASSERT_FALSE(shader.isNull());
olive::ShaderJob job;
job.Insert(QStringLiteral("ove_maintex"),
olive::NodeValue(olive::NodeValue::kTexture,
QVariant::fromValue(src)));
job.Insert(QStringLiteral("ove_mvpmat"),
olive::NodeValue(olive::NodeValue::kMatrix, QMatrix4x4()));
renderer.BlitToTexture(shader, job, dst.get(), true);
QByteArray dst_data(kSize * kSize * 4, 0);
dst->Download(dst_data.data(), kSize * 4);
// The default pass-through shader should reproduce the red source pixel.
EXPECT_EQ(static_cast<uint8_t>(dst_data[0]), 255u);
EXPECT_EQ(static_cast<uint8_t>(dst_data[1]), 0u);
EXPECT_EQ(static_cast<uint8_t>(dst_data[2]), 0u);
EXPECT_EQ(static_cast<uint8_t>(dst_data[3]), 255u);
#endif
}