diff --git a/app/render/backend/dynamicrenderer.h b/app/render/backend/dynamicrenderer.h index 95b92bf2d..772c44f2a 100644 --- a/app/render/backend/dynamicrenderer.h +++ b/app/render/backend/dynamicrenderer.h @@ -17,6 +17,8 @@ public: explicit DynamicRenderer(const QString &backend, QObject *parent = nullptr); virtual ~DynamicRenderer() override; + using Renderer::Blit; + bool Load(); bool InitWithOpenGLContext(QOpenGLContext *context); bool GetBackendInfo(OakRenderBackendInfo *out_info) const; diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 2059e6c77..253920869 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -987,6 +987,11 @@ bool OpenGLRenderer::EnsureContextCurrent(const char *caller) return false; } + if (context_->thread() != QThread::currentThread()) { + qWarning() << caller << "called from the wrong thread for this OpenGL context"; + return false; + } + if (QOpenGLContext::currentContext() != context_) { if (context_->parent() == this && surface_.isValid()) { if (!context_->makeCurrent(&surface_)) { diff --git a/app/render/vulkan/vulkanrenderer.cpp b/app/render/vulkan/vulkanrenderer.cpp index 7a186f19c..9e7c6bcad 100644 --- a/app/render/vulkan/vulkanrenderer.cpp +++ b/app/render/vulkan/vulkanrenderer.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "node/value.h" @@ -857,6 +858,106 @@ VkFormat VulkanRenderer::PickRenderableFormat(PixelFormat format, return VK_FORMAT_UNDEFINED; } +int VulkanRenderer::GetVkFormatBytesPerPixel(VkFormat format) const +{ + switch (format) { + case VK_FORMAT_R8_UNORM: + case VK_FORMAT_R8_UINT: + case VK_FORMAT_R8_SINT: + return 1; + case VK_FORMAT_R8G8_UNORM: + return 2; + case VK_FORMAT_R8G8B8_UNORM: + return 3; + case VK_FORMAT_R8G8B8A8_UNORM: + return 4; + case VK_FORMAT_R16_UNORM: + case VK_FORMAT_R16_SFLOAT: + return 2; + case VK_FORMAT_R16G16_UNORM: + case VK_FORMAT_R16G16_SFLOAT: + return 4; + case VK_FORMAT_R16G16B16_UNORM: + case VK_FORMAT_R16G16B16_SFLOAT: + return 6; + case VK_FORMAT_R16G16B16A16_UNORM: + case VK_FORMAT_R16G16B16A16_SFLOAT: + return 8; + case VK_FORMAT_R32_SFLOAT: + return 4; + case VK_FORMAT_R32G32_SFLOAT: + return 8; + case VK_FORMAT_R32G32B32_SFLOAT: + return 12; + case VK_FORMAT_R32G32B32A32_SFLOAT: + return 16; + default: + // For packed or compressed formats, return 0 and let callers fall back + // to the requested channel count. + return 0; + } +} + +float VulkanRenderer::GetFormatMaxAlpha(PixelFormat format) const +{ + if (format == PixelFormat::U8) { + return 255.0f; + } else if (format == PixelFormat::U16) { + return 65535.0f; + } + return 1.0f; +} + +void VulkanRenderer::CopyPixelsWithChannelConversion(const void *src, void *dst, + int width, int height, int depth, + int src_channels, int dst_channels, + PixelFormat format) const +{ + int src_bpc = VideoParams::GetBytesPerChannel(format); + int dst_bpc = src_bpc; + float alpha = GetFormatMaxAlpha(format); + + int plane_pixels = width * height; + int total_pixels = plane_pixels * depth; + + const char *src_ptr = static_cast(src); + char *dst_ptr = static_cast(dst); + + for (int i = 0; i < total_pixels; ++i) { + for (int c = 0; c < dst_channels; ++c) { + if (c < src_channels) { + memcpy(dst_ptr + (i * dst_channels + c) * dst_bpc, + src_ptr + (i * src_channels + c) * src_bpc, + dst_bpc); + } else { + // Fill missing channels with 0 (color) or max alpha. + if (c == 3) { + if (format == PixelFormat::U8) { + *reinterpret_cast(dst_ptr + + (i * dst_channels + c) * dst_bpc) = + static_cast(alpha); + } else if (format == PixelFormat::U16) { + *reinterpret_cast(dst_ptr + + (i * dst_channels + c) * dst_bpc) = + static_cast(alpha); + } else if (format == PixelFormat::F16) { + // Half-float 1.0: 0x3C00 + *reinterpret_cast(dst_ptr + + (i * dst_channels + c) * dst_bpc) = + 0x3C00; + } else { + *reinterpret_cast(dst_ptr + + (i * dst_channels + c) * dst_bpc) = + alpha; + } + } else { + memset(dst_ptr + (i * dst_channels + c) * dst_bpc, 0, dst_bpc); + } + } + } + } +} + VkDeviceSize VulkanRenderer::AlignSize(VkDeviceSize size, VkDeviceSize alignment) const { @@ -982,11 +1083,15 @@ QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth, // Upload initial data if provided if (data) { - int bytes_per_pixel = VideoParams::GetBytesPerPixel(format, channel_count); + int cpu_bytes_per_pixel = VideoParams::GetBytesPerPixel(format, channel_count); + int gpu_bytes_per_pixel = GetVkFormatBytesPerPixel(vk_format); + if (gpu_bytes_per_pixel == 0) { + gpu_bytes_per_pixel = cpu_bytes_per_pixel; + } VkDeviceSize image_size = static_cast(width) * height * depth * - bytes_per_pixel; + gpu_bytes_per_pixel; if (linesize == 0) { - linesize = width * bytes_per_pixel; + linesize = width * cpu_bytes_per_pixel; } VkBuffer staging_buffer; @@ -994,16 +1099,41 @@ QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth, if (CreateStagingBuffer(image_size, &staging_buffer, &staging_memory)) { void *mapped; vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); - if (linesize == width * bytes_per_pixel) { - memcpy(mapped, data, static_cast(image_size)); - } else { - char *dst = static_cast(mapped); - const char *src = static_cast(data); - for (int row = 0; row < height * depth; row++) { - memcpy(dst + row * width * bytes_per_pixel, - src + row * linesize, - static_cast(width * bytes_per_pixel)); + if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) { + if (linesize == width * cpu_bytes_per_pixel) { + memcpy(mapped, data, static_cast(image_size)); + } else { + char *dst = static_cast(mapped); + const char *src = static_cast(data); + for (int row = 0; row < height * depth; row++) { + memcpy(dst + row * width * cpu_bytes_per_pixel, + src + row * linesize, + static_cast(width * cpu_bytes_per_pixel)); + } } + } else { + // The GPU format has a different channel count than the CPU data + // (e.g. 3-channel RGB fallback to 4-channel RGBA). Repack the data + // in the staging buffer so the copy uses the GPU texel layout. + QByteArray tmp(width * height * depth * cpu_bytes_per_pixel, + Qt::Uninitialized); + if (linesize == width * cpu_bytes_per_pixel) { + memcpy(tmp.data(), data, static_cast(tmp.size())); + } else { + char *dst = tmp.data(); + const char *src = static_cast(data); + for (int row = 0; row < height * depth; row++) { + memcpy(dst + row * width * cpu_bytes_per_pixel, + src + row * linesize, + static_cast(width * cpu_bytes_per_pixel)); + } + } + int gpu_channels = gpu_bytes_per_pixel / + VideoParams::GetBytesPerChannel(format); + CopyPixelsWithChannelConversion(tmp.constData(), mapped, + width, height, depth, + channel_count, gpu_channels, + format); } vkUnmapMemory(device_, staging_memory); @@ -1071,12 +1201,16 @@ void VulkanRenderer::UploadToTexture(const QVariant &handle, int width = params.effective_width(); int height = params.effective_height(); int depth = params.effective_depth(); - int bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), - params.channel_count()); + int cpu_bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), + params.channel_count()); + int gpu_bytes_per_pixel = GetVkFormatBytesPerPixel(tex->vk_format); + if (gpu_bytes_per_pixel == 0) { + gpu_bytes_per_pixel = cpu_bytes_per_pixel; + } VkDeviceSize image_size = - static_cast(width) * height * depth * bytes_per_pixel; + static_cast(width) * height * depth * gpu_bytes_per_pixel; if (linesize == 0) { - linesize = width * bytes_per_pixel; + linesize = width * cpu_bytes_per_pixel; } VkBuffer staging_buffer; @@ -1087,16 +1221,38 @@ void VulkanRenderer::UploadToTexture(const QVariant &handle, void *mapped; vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); - if (linesize == width * bytes_per_pixel) { - memcpy(mapped, data, static_cast(image_size)); - } else { - char *dst = static_cast(mapped); - const char *src = static_cast(data); - for (int row = 0; row < height * depth; row++) { - memcpy(dst + row * width * bytes_per_pixel, - src + row * linesize, - static_cast(width * bytes_per_pixel)); + if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) { + if (linesize == width * cpu_bytes_per_pixel) { + memcpy(mapped, data, static_cast(image_size)); + } else { + char *dst = static_cast(mapped); + const char *src = static_cast(data); + for (int row = 0; row < height * depth; row++) { + memcpy(dst + row * width * cpu_bytes_per_pixel, + src + row * linesize, + static_cast(width * cpu_bytes_per_pixel)); + } } + } else { + QByteArray tmp(width * height * depth * cpu_bytes_per_pixel, + Qt::Uninitialized); + if (linesize == width * cpu_bytes_per_pixel) { + memcpy(tmp.data(), data, static_cast(tmp.size())); + } else { + char *dst = tmp.data(); + const char *src = static_cast(data); + for (int row = 0; row < height * depth; row++) { + memcpy(dst + row * width * cpu_bytes_per_pixel, + src + row * linesize, + static_cast(width * cpu_bytes_per_pixel)); + } + } + int gpu_channels = gpu_bytes_per_pixel / + VideoParams::GetBytesPerChannel(params.format()); + CopyPixelsWithChannelConversion(tmp.constData(), mapped, + width, height, depth, + params.channel_count(), gpu_channels, + params.format()); } vkUnmapMemory(device_, staging_memory); @@ -1130,13 +1286,17 @@ void VulkanRenderer::DownloadFromTexture(const QVariant &handle, int width = params.effective_width(); int height = params.effective_height(); - int bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), - params.channel_count()); + int cpu_bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), + params.channel_count()); + int gpu_bytes_per_pixel = GetVkFormatBytesPerPixel(tex->vk_format); + if (gpu_bytes_per_pixel == 0) { + gpu_bytes_per_pixel = cpu_bytes_per_pixel; + } if (linesize == 0) { - linesize = width * bytes_per_pixel; + linesize = width * cpu_bytes_per_pixel; } VkDeviceSize image_size = - static_cast(width) * height * bytes_per_pixel; + static_cast(width) * height * gpu_bytes_per_pixel; VkBuffer staging_buffer; VkDeviceMemory staging_memory; @@ -1156,20 +1316,43 @@ void VulkanRenderer::DownloadFromTexture(const QVariant &handle, void *mapped; vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); - if (linesize == width * bytes_per_pixel) { - memcpy(data, mapped, static_cast(image_size)); + if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) { + if (linesize == width * cpu_bytes_per_pixel) { + memcpy(data, mapped, static_cast(image_size)); + } else { + char *dst = static_cast(data); + const char *src = static_cast(mapped); + for (int row = 0; row < height; row++) { + memcpy(dst + row * linesize, + src + row * width * cpu_bytes_per_pixel, + static_cast(width * cpu_bytes_per_pixel)); + } + } } else { - char *dst = static_cast(data); - const char *src = static_cast(mapped); - for (int row = 0; row < height; row++) { - memcpy(dst + row * linesize, - src + row * width * bytes_per_pixel, - static_cast(width * bytes_per_pixel)); + int gpu_channels = gpu_bytes_per_pixel / + VideoParams::GetBytesPerChannel(params.format()); + QByteArray tmp(width * height * gpu_bytes_per_pixel, Qt::Uninitialized); + memcpy(tmp.data(), mapped, static_cast(tmp.size())); + CopyPixelsWithChannelConversion(tmp.constData(), data, + width, height, 1, + gpu_channels, params.channel_count(), + params.format()); + if (linesize != width * cpu_bytes_per_pixel) { + // Repack from tight CPU layout to caller's stride in-place. + QByteArray tight(static_cast(data), + width * height * cpu_bytes_per_pixel); + char *dst = static_cast(data); + for (int row = 0; row < height; row++) { + memcpy(dst + row * linesize, + tight.constData() + row * width * cpu_bytes_per_pixel, + static_cast(width * cpu_bytes_per_pixel)); + } } } vkUnmapMemory(device_, staging_memory); DestroyStagingBuffer(staging_buffer, staging_memory); + tex->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; } void VulkanRenderer::Flush() @@ -1222,9 +1405,9 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture, if (!texture) { return Color(); } - int bytes_per_pixel = VideoParams::GetBytesPerPixel(texture->format(), - texture->channel_count()); - QByteArray data(bytes_per_pixel, Qt::Uninitialized); + int cpu_bytes_per_pixel = VideoParams::GetBytesPerPixel(texture->format(), + texture->channel_count()); + QByteArray data(cpu_bytes_per_pixel, Qt::Uninitialized); quint64 id = texture->id().value(); QMutexLocker lock(&mutex_); @@ -1236,9 +1419,14 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture, uint32_t px = static_cast(qBound(0.0, pt.x(), double(tex->width - 1))); uint32_t py = static_cast(qBound(0.0, pt.y(), double(tex->height - 1))); + int gpu_bytes_per_pixel = GetVkFormatBytesPerPixel(tex->vk_format); + if (gpu_bytes_per_pixel == 0) { + gpu_bytes_per_pixel = cpu_bytes_per_pixel; + } + VkBuffer staging_buffer; VkDeviceMemory staging_memory; - if (!CreateStagingBuffer(bytes_per_pixel, &staging_buffer, &staging_memory)) { + if (!CreateStagingBuffer(gpu_bytes_per_pixel, &staging_buffer, &staging_memory)) { return Color(); } @@ -1251,11 +1439,23 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture, EndOneTimeCommands(cmd); void *mapped; - vkMapMemory(device_, staging_memory, 0, bytes_per_pixel, 0, &mapped); - memcpy(data.data(), mapped, static_cast(bytes_per_pixel)); + vkMapMemory(device_, staging_memory, 0, gpu_bytes_per_pixel, 0, &mapped); + if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) { + memcpy(data.data(), mapped, static_cast(cpu_bytes_per_pixel)); + } else { + int gpu_channels = gpu_bytes_per_pixel / + VideoParams::GetBytesPerChannel(texture->format()); + QByteArray gpu_pixel(gpu_bytes_per_pixel, Qt::Uninitialized); + memcpy(gpu_pixel.data(), mapped, static_cast(gpu_bytes_per_pixel)); + CopyPixelsWithChannelConversion(gpu_pixel.constData(), data.data(), + 1, 1, 1, + gpu_channels, texture->channel_count(), + texture->format()); + } vkUnmapMemory(device_, staging_memory); DestroyStagingBuffer(staging_buffer, staging_memory); + tex->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; return Color::fromData(data.data(), texture->format(), texture->channel_count()); @@ -1845,36 +2045,15 @@ bool VulkanRenderer::CreatePipelineForShader(VulkanShader *shader, return true; } -void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, - olive::Texture *destination, - VideoParams destination_params, - bool clear_destination) +void VulkanRenderer::BlitPass(VulkanShader *shader, VulkanTexture *dest_tex, + const QVector &bindings, + const QByteArray &ubo_data, + const VideoParams &destination_params, + bool clear_destination, int iteration) { - QMutexLocker lock(&mutex_); + (void)iteration; - ShaderJob *s_job = dynamic_cast(&a_job); - if (!s_job) { - return; - } - ShaderJob job(*s_job); - - quint64 shader_id = shader_variant.value(); - VulkanShader *shader = shaders_.value(shader_id); - if (!shader) { - return; - } - - VulkanTexture *dest_tex = nullptr; - if (destination) { - quint64 dest_id = destination->id().value(); - dest_tex = textures_.value(dest_id); - if (!dest_tex) { - 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"; + if (!dest_tex) { return; } @@ -1890,145 +2069,26 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, VkPipeline pipeline = shader->pipeline_cache.value(render_pass_format); - // Collect textures to bind and build UBO data - struct TextureBinding { - QString name; - VulkanTexture *tex; - Texture::Interpolation interp; - }; - QVector bindings; - - QByteArray ubo_data; - if (shader->ubo_size > 0) { - ubo_data.resize(static_cast(shader->ubo_size)); - ubo_data.fill(0); - } - - for (auto it = job.GetValues().constBegin(); it != job.GetValues().constEnd(); - ++it) { - const NodeValue &value = it.value(); - if (value.type() == NodeValue::kTexture) { - TexturePtr texture = value.toTexture(); - VulkanTexture *vtex = nullptr; - if (texture) { - quint64 tid = texture->id().value(); - vtex = textures_.value(tid); - } - bindings.append({ it.key(), vtex, - job.GetInterpolation(it.key()) }); - } else if (!shader->uniforms.isEmpty() && shader->ubo_size > 0) { - // Find matching uniform - for (const UniformInfo &u : shader->uniforms) { - if (u.name != it.key()) - continue; - char *dst = ubo_data.data() + static_cast(u.offset); - switch (value.type()) { - case NodeValue::kFloat: - *reinterpret_cast(dst) = static_cast(value.toDouble()); - break; - case NodeValue::kInt: - *reinterpret_cast(dst) = static_cast(value.toInt()); - break; - case NodeValue::kBoolean: - *reinterpret_cast(dst) = value.toBool() ? 1 : 0; - break; - case NodeValue::kVec2: { - QVector2D v = value.toVec2(); - memcpy(dst, &v, sizeof(float) * 2); - break; - } - case NodeValue::kVec3: { - QVector3D v = value.toVec3(); - memcpy(dst, &v, sizeof(float) * 3); - break; - } - case NodeValue::kVec4: { - QVector4D v = value.toVec4(); - memcpy(dst, &v, sizeof(float) * 4); - break; - } - case NodeValue::kMatrix: { - QMatrix4x4 m = value.toMatrix(); - memcpy(dst, m.constData(), sizeof(float) * 16); - break; - } - case NodeValue::kColor: { - Color c = value.toColor(); - float col[4] = { static_cast(c.red()), - static_cast(c.green()), - static_cast(c.blue()), - static_cast(c.alpha()) }; - memcpy(dst, col, sizeof(float) * 4); - break; - } - case NodeValue::kCombo: - *reinterpret_cast(dst) = value.toInt(); - break; - default: - break; - } - break; - } - } - } - - // Handle special uniforms that may not be in job values - if (shader->ubo_size > 0) { - for (const UniformInfo &u : shader->uniforms) { - char *dst = ubo_data.data() + static_cast(u.offset); - if (u.name == QStringLiteral("ove_mvpmat")) { - QMatrix4x4 m = job.Get(QStringLiteral("ove_mvpmat")).toMatrix(); - memcpy(dst, m.constData(), sizeof(float) * 16); - } else if (u.name == QStringLiteral("ove_cropmatrix")) { - QMatrix4x4 m = job.Get(QStringLiteral("ove_cropmatrix")).toMatrix(); - memcpy(dst, m.constData(), sizeof(float) * 16); - } else if (u.name == QStringLiteral("ove_maintex_alpha")) { - *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_maintex_alpha")).toInt(); - } else if (u.name == QStringLiteral("ove_force_opaque")) { - *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_force_opaque")).toBool() ? 1 : 0; - } else if (u.name == QStringLiteral("ove_iteration")) { - *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_iteration")).toInt(); - } - } - } - - // 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(u.offset); - *reinterpret_cast(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; - if (dest_tex) { - if (dest_tex->framebuffer == VK_NULL_HANDLE) { - VkFramebufferCreateInfo fb_info = {}; - fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fb_info.renderPass = GetOrCreateRenderPass(render_pass_format, false); - fb_info.attachmentCount = 1; - fb_info.pAttachments = &dest_tex->view; - fb_info.width = static_cast(dest_tex->width); - fb_info.height = static_cast(dest_tex->height); - 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; - } + if (dest_tex->framebuffer == VK_NULL_HANDLE) { + VkFramebufferCreateInfo fb_info = {}; + fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fb_info.renderPass = GetOrCreateRenderPass(render_pass_format, false); + fb_info.attachmentCount = 1; + fb_info.pAttachments = &dest_tex->view; + fb_info.width = static_cast(dest_tex->width); + fb_info.height = static_cast(dest_tex->height); + 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; } + VkFramebuffer framebuffer = dest_tex->framebuffer; // Create UBO buffer if needed VkBuffer ubo_buffer = VK_NULL_HANDLE; @@ -2108,13 +2168,13 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, if (!writes.isEmpty()) { vkUpdateDescriptorSets(device_, writes.size(), writes.constData(), 0, - nullptr); + nullptr); } } VkCommandBuffer cmd = BeginOneTimeCommands(); - if (dest_tex && dest_tex->current_layout != VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { + if (dest_tex->current_layout != VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { TransitionImageLayout(cmd, dest_tex->image, dest_tex->current_layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); dest_tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; @@ -2175,7 +2235,6 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, nullptr); } - // Draw vkCmdDraw(cmd, 6, 1, 0, 0); @@ -2183,12 +2242,10 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, // 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; - } + 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); @@ -2200,4 +2257,225 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, } } +void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, + olive::Texture *destination, + VideoParams destination_params, + bool clear_destination) +{ + ShaderJob *s_job = dynamic_cast(&a_job); + if (!s_job) { + return; + } + ShaderJob job(*s_job); + + quint64 shader_id = shader_variant.value(); + + // Iterative shaders require ping-pong textures. Create them before locking + // the renderer mutex because CreateTexture also locks it. + int real_iteration_count = 1; + if (job.GetIterationCount() > 1 && !job.GetIterativeInput().isEmpty()) { + real_iteration_count = job.GetIterationCount(); + } + + struct PingPongTexture { + TexturePtr texture; + VulkanTexture *native = nullptr; + }; + + PingPongTexture output_tex, input_tex, final_tex; + if (real_iteration_count > 1) { + output_tex.texture = CreateTexture(destination_params); + if (real_iteration_count > 2) { + input_tex.texture = CreateTexture(destination_params); + } + } + + if (!destination) { + final_tex.texture = CreateTexture(destination_params); + } + + QMutexLocker lock(&mutex_); + + VulkanShader *shader = shaders_.value(shader_id); + if (!shader) { + return; + } + + VulkanTexture *dest_tex = nullptr; + if (destination) { + quint64 dest_id = destination->id().value(); + dest_tex = textures_.value(dest_id); + if (!dest_tex) { + return; + } + } else { + quint64 final_id = final_tex.texture->id().value(); + final_tex.native = textures_.value(final_id); + if (!final_tex.native) { + qWarning() << "VulkanRenderer::Blit failed to resolve temporary destination texture"; + return; + } + dest_tex = final_tex.native; + } + + if (output_tex.texture) { + quint64 id = output_tex.texture->id().value(); + output_tex.native = textures_.value(id); + } + if (input_tex.texture) { + quint64 id = input_tex.texture->id().value(); + input_tex.native = textures_.value(id); + } + + // Collect textures to bind and build base UBO data + QVector base_bindings; + QByteArray base_ubo_data; + if (shader->ubo_size > 0) { + base_ubo_data.resize(static_cast(shader->ubo_size)); + base_ubo_data.fill(0); + } + + for (auto it = job.GetValues().constBegin(); it != job.GetValues().constEnd(); + ++it) { + const NodeValue &value = it.value(); + if (value.type() == NodeValue::kTexture) { + TexturePtr texture = value.toTexture(); + VulkanTexture *vtex = nullptr; + if (texture) { + quint64 tid = texture->id().value(); + vtex = textures_.value(tid); + } + base_bindings.append({ it.key(), vtex, + job.GetInterpolation(it.key()) }); + } else if (!shader->uniforms.isEmpty() && shader->ubo_size > 0) { + // Find matching uniform + for (const UniformInfo &u : shader->uniforms) { + if (u.name != it.key()) + continue; + char *dst = base_ubo_data.data() + static_cast(u.offset); + switch (value.type()) { + case NodeValue::kFloat: + *reinterpret_cast(dst) = static_cast(value.toDouble()); + break; + case NodeValue::kInt: + *reinterpret_cast(dst) = static_cast(value.toInt()); + break; + case NodeValue::kBoolean: + *reinterpret_cast(dst) = value.toBool() ? 1 : 0; + break; + case NodeValue::kVec2: { + QVector2D v = value.toVec2(); + memcpy(dst, &v, sizeof(float) * 2); + break; + } + case NodeValue::kVec3: { + QVector3D v = value.toVec3(); + memcpy(dst, &v, sizeof(float) * 3); + break; + } + case NodeValue::kVec4: { + QVector4D v = value.toVec4(); + memcpy(dst, &v, sizeof(float) * 4); + break; + } + case NodeValue::kMatrix: { + QMatrix4x4 m = value.toMatrix(); + memcpy(dst, m.constData(), sizeof(float) * 16); + break; + } + case NodeValue::kColor: { + Color c = value.toColor(); + float col[4] = { static_cast(c.red()), + static_cast(c.green()), + static_cast(c.blue()), + static_cast(c.alpha()) }; + memcpy(dst, col, sizeof(float) * 4); + break; + } + case NodeValue::kCombo: + *reinterpret_cast(dst) = value.toInt(); + break; + default: + break; + } + break; + } + } + } + + // Handle special uniforms that may not be in job values + if (shader->ubo_size > 0) { + for (const UniformInfo &u : shader->uniforms) { + char *dst = base_ubo_data.data() + static_cast(u.offset); + if (u.name == QStringLiteral("ove_mvpmat")) { + QMatrix4x4 m = job.Get(QStringLiteral("ove_mvpmat")).toMatrix(); + memcpy(dst, m.constData(), sizeof(float) * 16); + } else if (u.name == QStringLiteral("ove_cropmatrix")) { + QMatrix4x4 m = job.Get(QStringLiteral("ove_cropmatrix")).toMatrix(); + memcpy(dst, m.constData(), sizeof(float) * 16); + } else if (u.name == QStringLiteral("ove_maintex_alpha")) { + *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_maintex_alpha")).toInt(); + } else if (u.name == QStringLiteral("ove_force_opaque")) { + *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_force_opaque")).toBool() ? 1 : 0; + } + } + } + + // Set texture-enable flags for shaders that declare uniform bool NAME_enabled. + if (shader->ubo_size > 0) { + for (const TextureBinding &tb : base_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 = base_ubo_data.data() + static_cast(u.offset); + *reinterpret_cast(dst) = tb.tex ? 1 : 0; + break; + } + } + } + } + + for (int iteration = 0; iteration < real_iteration_count; ++iteration) { + QVector pass_bindings = base_bindings; + QByteArray pass_ubo_data = base_ubo_data; + + // Set iteration number + if (shader->ubo_size > 0) { + for (const UniformInfo &u : shader->uniforms) { + if (u.name == QStringLiteral("ove_iteration")) { + char *dst = pass_ubo_data.data() + static_cast(u.offset); + *reinterpret_cast(dst) = iteration; + break; + } + } + } + + // Replace iterative input + VulkanTexture *pass_dest = dest_tex; + bool pass_clear = clear_destination; + if (iteration != real_iteration_count - 1) { + pass_dest = output_tex.native; + pass_clear = true; + } + + if (iteration > 0) { + const QString &iterative_input = job.GetIterativeInput(); + for (TextureBinding &tb : pass_bindings) { + if (tb.name == iterative_input) { + tb.tex = input_tex.native; + break; + } + } + } + + BlitPass(shader, pass_dest, pass_bindings, pass_ubo_data, + destination_params, pass_clear, iteration); + + if (iteration != real_iteration_count - 1) { + std::swap(output_tex, input_tex); + } + } +} + + } // namespace olive diff --git a/app/render/vulkan/vulkanrenderer.h b/app/render/vulkan/vulkanrenderer.h index 0815b498b..2a6accfb6 100644 --- a/app/render/vulkan/vulkanrenderer.h +++ b/app/render/vulkan/vulkanrenderer.h @@ -109,6 +109,12 @@ private: VkFormat PixelFormatToVkFormat(PixelFormat format, int channel_count) const; VkFormat PickRenderableFormat(PixelFormat format, int channel_count) const; bool IsColorAttachmentSupported(VkFormat format) const; + int GetVkFormatBytesPerPixel(VkFormat format) const; + float GetFormatMaxAlpha(PixelFormat format) const; + void CopyPixelsWithChannelConversion(const void *src, void *dst, + int width, int height, int depth, + int src_channels, int dst_channels, + PixelFormat format) const; VkDeviceSize AlignSize(VkDeviceSize size, VkDeviceSize alignment) const; uint32_t FindMemoryType(uint32_t type_filter, @@ -134,6 +140,18 @@ private: VkRenderPass GetOrCreateRenderPass(VkFormat format, bool clear); + struct TextureBinding { + QString name; + VulkanTexture *tex; + Texture::Interpolation interp; + }; + + void BlitPass(VulkanShader *shader, VulkanTexture *dest_tex, + const QVector &bindings, + const QByteArray &ubo_data, + const VideoParams &destination_params, + bool clear_destination, int iteration); + VkInstance instance_ = VK_NULL_HANDLE; VkPhysicalDevice physical_device_ = VK_NULL_HANDLE; uint32_t physical_device_count_ = 0; diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 7037cf1fe..f7730601c 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -429,18 +429,22 @@ void ViewerDisplayWidget::OnPaint() // This is a GPU texture, switch to it directly when possible. if (texture && texture->renderer() && texture->renderer() != renderer()) { - // Cross-renderer texture: download and re-upload - FramePtr frame = Frame::Create(); - frame->set_video_params(texture->params()); - if (frame->allocate()) { - texture->renderer()->DownloadFromTexture( - texture->id(), texture->params(), - frame->data(), frame->linesize_pixels()); - texture_ = renderer()->CreateTexture( - frame->video_params(), frame->data(), - frame->linesize_pixels()); - } else { + if (texture->renderer()->IsOpenGL() && renderer()->IsOpenGL()) { texture_ = texture; + } else { + // Cross-backend texture: download and re-upload + FramePtr frame = Frame::Create(); + frame->set_video_params(texture->params()); + if (frame->allocate()) { + texture->renderer()->DownloadFromTexture( + texture->id(), texture->params(), + frame->data(), frame->linesize_pixels()); + texture_ = renderer()->CreateTexture( + frame->video_params(), frame->data(), + frame->linesize_pixels()); + } else { + texture_ = texture; + } } } else { texture_ = texture; diff --git a/docs/zh/render-backend-dynamic-plan.md b/docs/zh/render-backend-dynamic-plan.md index 5dd2a9fef..96bc3b80c 100644 --- a/docs/zh/render-backend-dynamic-plan.md +++ b/docs/zh/render-backend-dynamic-plan.md @@ -59,7 +59,7 @@ - 动态适配器:`DynamicRenderer`、`renderbackend_c.h`。 - 必要的 value/config/工具:`node/value`、`node/param`、`node/valuedatabase`、`config/config`、`common/filefunctions`、`common/qtutils`、`common/avframeptr`。 - `oakgl`/`oakvulkan` 现在只链接 `libolive-rendercore`,不再链接完整 `libolive-editor`。 -- `liboakgl.so` / `liboakvulkan.so` 体积从约 21 MB 降至约 600 KB。 +- `liboakgl.so` / `liboakvulkan.so` 不再链接完整 editor 对象库;实际体积取决于构建类型、符号表和系统依赖链接方式,当前 debug 构建仍会显著大于 release/strip 后体积。 - 为隔离依赖做的头文件清理: - `renderer.h` 移除 `node/node.h`、`render/colorprocessor.h`、`render/job/colortransformjob.h`、`job/pluginjob.h`,改为前向声明。 - `videoparams.h` 移除 `ofxImageEffect.h`,OFX 字符串 setter 实现下移到 `videoparams.cpp`。 @@ -70,7 +70,7 @@ 剩余优化空间: - 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。 -## 阶段 3:Vulkan 后端(原型实现,运行时验证待完成) +## 阶段 3:Vulkan 后端(offscreen 核心已实现,运行时依赖可用 Vulkan ICD) - 新增 Vulkan 后端库 `liboakvulkan.so`(当系统安装了 Vulkan 头文件/库时构建;无 Vulkan 环境时 CMake 自动跳过)。 - 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线;代码已合入,并在本机 NVIDIA Vulkan 驱动上通过了基础端到端渲染测试。 @@ -99,11 +99,19 @@ - framebuffer / sampler 缓存:每张纹理延迟创建并复用 framebuffer;按插值模式复用 linear/nearest sampler。 - 单通道纹理 swizzle:image view 组件映射为 R→RGB、A=1,匹配 OpenGL 灰度行为。 - 纹理启用标志:为声明 `NAME_enabled` 的 shader 自动设置 0/1。 -- **已知限制 / 待完善**: +- **已修复 / 已实现**: - 链接边界已最小化,`liboakvulkan.so` 现在只依赖 `libolive-rendercore`。 - - 单通道/3-channel 格式的上传/下载 CPU 侧对齐、回退格式与请求格式不一致时的数据转换仍待完善。 - - `Blit` 尚未实现 iterative/pin-pong 多 pass(如 blur/glow 等依赖 `ShaderJob::GetIterationCount` 的效果目前只渲染第一 pass)。 - - 尚未在 viewer、proxy、thumbnail/cache、导出等完整渲染路径上验证 Vulkan 输出一致性;需要在真实 GPU 上手工测试并记录结果。 + - 单通道/3-channel 格式的上传/下载 CPU 侧对齐:当 GPU 回退格式(如 3→4 channel)与请求格式不一致时,staging buffer 按实际 `VkFormat` 大小分配,并在 CPU 侧进行通道数转换(alpha 填最大值)。 + - `Blit` 已实现 iterative/pin-pong 多 pass:根据 `ShaderJob::GetIterationCount` / `GetIterativeInput` 创建临时 ping-pong 纹理,每 pass 更新 `ove_iteration` 并替换迭代输入;最后一 pass 写入目标纹理。 + - null-destination Blit 实现为渲染到临时 offscreen texture,保证调用不崩溃。 + - 新增自动化测试: + - `VulkanNullDestinationBlitDoesNotCrash` + - `VulkanIterativeBlitPingPong`(2 pass 折半,验证 ping-pong 结果) + - `VulkanUploadDownloadThreeChannel`(验证 3-channel RGB 上传/下载与回退格式转换) +- **当前验证状态**: + - 自动化测试已覆盖 Vulkan 后端加载、texture upload/download、Blit、null-destination fallback、iterative ping-pong、3-channel upload/download fallback;这些测试会在运行环境存在可用 Vulkan ICD 时执行。 + - 当前开发环境可找到 Vulkan loader/headers,但运行时 loader 只发现不可用的 NVIDIA ICD,`vkCreateInstance` 报 `Found no drivers`;因此 Vulkan 用例会按设计 SKIP,不能作为 Vulkan 渲染通过的证据。 + - Viewer / proxy / 导出的完整交互流程仍需具备显示环境和可用 Vulkan runtime 的项目做最终验证;当前已在代码路径层面确认 backend-neutral viewer readback、proxy/export 渲染入口均使用 `Renderer` 抽象,无硬编码 OpenGL 依赖。 ## 阶段 4:Viewer 双后端(backend-neutral 路径已落地,Vulkan viewer 为原型) @@ -146,7 +154,8 @@ - [x] OpenGL 后端库可单独构建、加载、初始化、销毁。 - [x] 用户能在配置中选择 OpenGL/Vulkan。 - [x] Vulkan 不可用时自动回退到 OpenGL,不崩溃;`RenderManager::backend()` 会在 `DynamicRenderer` 内部回退后同步为实际运行后端。 -- [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积从约 21 MB 降至约 600 KB。 -- [x] Vulkan / backend-neutral viewer readback display 路径已搭建(offscreen texture → download → QImage → QPainter);单 pass Blit 已在真实 Vulkan 驱动上验证,完整 UI/导出流程待验证。 +- [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积需按 release/strip 构建重新记录。 +- [ ] Vulkan / backend-neutral viewer readback display 路径已搭建(offscreen texture → download → QImage → QPainter);仍需在可用 Vulkan runtime 和显示环境下验证完整 Viewer/proxy/导出流程。 - [x] OpenFX 插件渲染边界已处理:`PluginRenderer` 后端无关化,非 OpenGL 渲染器自动回退 CPU 路径,动态 OpenGL 后端通过 C ABI 支持 OFX OpenGL 输出绑定。 -- [ ] 手工测试计划覆盖 viewer、proxy、scope、导出等完整路径。 +- [x] 自动化测试覆盖 device init、texture create/upload/download(含 3-channel fallback)、shader compilation、Blit with destination、null-destination fallback、iterative shaders;无可用 Vulkan ICD 时相关用例按设计 SKIP。 +- [ ] 手工测试计划覆盖 viewer、proxy、scope、导出等完整路径;`ScopeBase` 当前在 backend-neutral 时仍是安全跳过,不是完整 Vulkan scope display。 diff --git a/tests/gtest/dynamic_render_backend_test.cpp b/tests/gtest/dynamic_render_backend_test.cpp index ad03cdbee..9ad797035 100644 --- a/tests/gtest/dynamic_render_backend_test.cpp +++ b/tests/gtest/dynamic_render_backend_test.cpp @@ -146,3 +146,186 @@ TEST(DynamicRenderBackend, VulkanUploadBlitDownload) EXPECT_EQ(static_cast(dst_data[3]), 255u); #endif } + +TEST(DynamicRenderBackend, VulkanNullDestinationBlitDoesNotCrash) +{ +#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 = 32; + 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(255); + src_data[i * 4 + 3] = static_cast(255); + } + src->Upload(src_data.data(), kSize * 4); + + 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())); + + // Null-destination Blit has no render target; it should simply not crash. + renderer.Blit(shader, job, params, true); + SUCCEED(); +#endif +} + +TEST(DynamicRenderBackend, VulkanIterativeBlitPingPong) +{ +#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 = 32; + 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()); + + // Start with a fully red texture. + QByteArray src_data(kSize * kSize * 4, 0); + for (int i = 0; i < kSize * kSize; ++i) { + src_data[i * 4 + 0] = static_cast(255); + src_data[i * 4 + 3] = static_cast(255); + } + src->Upload(src_data.data(), kSize * 4); + + olive::TexturePtr dst = renderer.CreateTexture(params); + ASSERT_NE(dst, nullptr); + ASSERT_FALSE(dst->IsDummy()); + + // Shader that samples the iterative input and scales RGB by 0.5 each pass. + 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" + " vec4 c = texture(ove_maintex, ove_texcoord);\n" + " frag_color = vec4(c.rgb * 0.5, c.a);\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())); + job.SetIterations(2, QStringLiteral("ove_maintex")); + + renderer.BlitToTexture(shader, job, dst.get(), true); + + QByteArray dst_data(kSize * kSize * 4, 0); + dst->Download(dst_data.data(), kSize * 4); + + // After two halving passes, red is 255 * 0.5 * 0.5. UNORM conversion floors + // the intermediate value, so the result is 63 rather than 64. + EXPECT_EQ(static_cast(dst_data[0]), 63u); + EXPECT_EQ(static_cast(dst_data[1]), 0u); + EXPECT_EQ(static_cast(dst_data[2]), 0u); + EXPECT_EQ(static_cast(dst_data[3]), 255u); +#endif +} + +TEST(DynamicRenderBackend, VulkanUploadDownloadThreeChannel) +{ +#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 = 16; + olive::VideoParams params(kSize, kSize, olive::PixelFormat::U8, + olive::VideoParams::kRGBChannelCount); + + olive::TexturePtr tex = renderer.CreateTexture(params); + ASSERT_NE(tex, nullptr); + ASSERT_FALSE(tex->IsDummy()); + + QByteArray src_data(kSize * kSize * 3, 0); + for (int i = 0; i < kSize * kSize; ++i) { + src_data[i * 3 + 0] = static_cast(255); + src_data[i * 3 + 1] = static_cast(128); + src_data[i * 3 + 2] = static_cast(64); + } + tex->Upload(src_data.data(), kSize * 3); + + QByteArray dst_data(kSize * kSize * 3, 0); + tex->Download(dst_data.data(), kSize * 3); + + EXPECT_EQ(static_cast(dst_data[0]), 255u); + EXPECT_EQ(static_cast(dst_data[1]), 128u); + EXPECT_EQ(static_cast(dst_data[2]), 64u); +#endif +} diff --git a/tests/gtest/main.cpp b/tests/gtest/main.cpp index bbff6102d..63f451796 100644 --- a/tests/gtest/main.cpp +++ b/tests/gtest/main.cpp @@ -12,6 +12,9 @@ int main(int argc, char **argv) .filePath(QStringLiteral( "app/render/ocioconf/config.ocio")))); } + if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) { + qputenv("QT_QPA_PLATFORM", "offscreen"); + } QApplication app(argc, argv); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();