完成Vulkan渲染后端

This commit is contained in:
2026-07-13 10:19:30 +08:00
parent cd5efa8ea4
commit 3c9592da45
8 changed files with 736 additions and 234 deletions
+2
View File
@@ -17,6 +17,8 @@ public:
explicit DynamicRenderer(const QString &backend, QObject *parent = nullptr); explicit DynamicRenderer(const QString &backend, QObject *parent = nullptr);
virtual ~DynamicRenderer() override; virtual ~DynamicRenderer() override;
using Renderer::Blit;
bool Load(); bool Load();
bool InitWithOpenGLContext(QOpenGLContext *context); bool InitWithOpenGLContext(QOpenGLContext *context);
bool GetBackendInfo(OakRenderBackendInfo *out_info) const; bool GetBackendInfo(OakRenderBackendInfo *out_info) const;
+5
View File
@@ -987,6 +987,11 @@ bool OpenGLRenderer::EnsureContextCurrent(const char *caller)
return false; 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 (QOpenGLContext::currentContext() != context_) {
if (context_->parent() == this && surface_.isValid()) { if (context_->parent() == this && surface_.isValid()) {
if (!context_->makeCurrent(&surface_)) { if (!context_->makeCurrent(&surface_)) {
+492 -214
View File
@@ -3,6 +3,7 @@
#include <QDebug> #include <QDebug>
#include <QFile> #include <QFile>
#include <QRegularExpression> #include <QRegularExpression>
#include <algorithm>
#include <cstdio> #include <cstdio>
#include "node/value.h" #include "node/value.h"
@@ -857,6 +858,106 @@ VkFormat VulkanRenderer::PickRenderableFormat(PixelFormat format,
return VK_FORMAT_UNDEFINED; 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<const char *>(src);
char *dst_ptr = static_cast<char *>(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<uint8_t *>(dst_ptr +
(i * dst_channels + c) * dst_bpc) =
static_cast<uint8_t>(alpha);
} else if (format == PixelFormat::U16) {
*reinterpret_cast<uint16_t *>(dst_ptr +
(i * dst_channels + c) * dst_bpc) =
static_cast<uint16_t>(alpha);
} else if (format == PixelFormat::F16) {
// Half-float 1.0: 0x3C00
*reinterpret_cast<uint16_t *>(dst_ptr +
(i * dst_channels + c) * dst_bpc) =
0x3C00;
} else {
*reinterpret_cast<float *>(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 VulkanRenderer::AlignSize(VkDeviceSize size,
VkDeviceSize alignment) const VkDeviceSize alignment) const
{ {
@@ -982,11 +1083,15 @@ QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth,
// Upload initial data if provided // Upload initial data if provided
if (data) { 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<VkDeviceSize>(width) * height * depth * VkDeviceSize image_size = static_cast<VkDeviceSize>(width) * height * depth *
bytes_per_pixel; gpu_bytes_per_pixel;
if (linesize == 0) { if (linesize == 0) {
linesize = width * bytes_per_pixel; linesize = width * cpu_bytes_per_pixel;
} }
VkBuffer staging_buffer; VkBuffer staging_buffer;
@@ -994,16 +1099,41 @@ QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth,
if (CreateStagingBuffer(image_size, &staging_buffer, &staging_memory)) { if (CreateStagingBuffer(image_size, &staging_buffer, &staging_memory)) {
void *mapped; void *mapped;
vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped);
if (linesize == width * bytes_per_pixel) { if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
memcpy(mapped, data, static_cast<size_t>(image_size)); if (linesize == width * cpu_bytes_per_pixel) {
} else { memcpy(mapped, data, static_cast<size_t>(image_size));
char *dst = static_cast<char *>(mapped); } else {
const char *src = static_cast<const char *>(data); char *dst = static_cast<char *>(mapped);
for (int row = 0; row < height * depth; row++) { const char *src = static_cast<const char *>(data);
memcpy(dst + row * width * bytes_per_pixel, for (int row = 0; row < height * depth; row++) {
src + row * linesize, memcpy(dst + row * width * cpu_bytes_per_pixel,
static_cast<size_t>(width * bytes_per_pixel)); src + row * linesize,
static_cast<size_t>(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<size_t>(tmp.size()));
} else {
char *dst = tmp.data();
const char *src = static_cast<const char *>(data);
for (int row = 0; row < height * depth; row++) {
memcpy(dst + row * width * cpu_bytes_per_pixel,
src + row * linesize,
static_cast<size_t>(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); vkUnmapMemory(device_, staging_memory);
@@ -1071,12 +1201,16 @@ void VulkanRenderer::UploadToTexture(const QVariant &handle,
int width = params.effective_width(); int width = params.effective_width();
int height = params.effective_height(); int height = params.effective_height();
int depth = params.effective_depth(); int depth = params.effective_depth();
int bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), int cpu_bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(),
params.channel_count()); 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 = VkDeviceSize image_size =
static_cast<VkDeviceSize>(width) * height * depth * bytes_per_pixel; static_cast<VkDeviceSize>(width) * height * depth * gpu_bytes_per_pixel;
if (linesize == 0) { if (linesize == 0) {
linesize = width * bytes_per_pixel; linesize = width * cpu_bytes_per_pixel;
} }
VkBuffer staging_buffer; VkBuffer staging_buffer;
@@ -1087,16 +1221,38 @@ void VulkanRenderer::UploadToTexture(const QVariant &handle,
void *mapped; void *mapped;
vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped);
if (linesize == width * bytes_per_pixel) { if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
memcpy(mapped, data, static_cast<size_t>(image_size)); if (linesize == width * cpu_bytes_per_pixel) {
} else { memcpy(mapped, data, static_cast<size_t>(image_size));
char *dst = static_cast<char *>(mapped); } else {
const char *src = static_cast<const char *>(data); char *dst = static_cast<char *>(mapped);
for (int row = 0; row < height * depth; row++) { const char *src = static_cast<const char *>(data);
memcpy(dst + row * width * bytes_per_pixel, for (int row = 0; row < height * depth; row++) {
src + row * linesize, memcpy(dst + row * width * cpu_bytes_per_pixel,
static_cast<size_t>(width * bytes_per_pixel)); src + row * linesize,
static_cast<size_t>(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<size_t>(tmp.size()));
} else {
char *dst = tmp.data();
const char *src = static_cast<const char *>(data);
for (int row = 0; row < height * depth; row++) {
memcpy(dst + row * width * cpu_bytes_per_pixel,
src + row * linesize,
static_cast<size_t>(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); vkUnmapMemory(device_, staging_memory);
@@ -1130,13 +1286,17 @@ void VulkanRenderer::DownloadFromTexture(const QVariant &handle,
int width = params.effective_width(); int width = params.effective_width();
int height = params.effective_height(); int height = params.effective_height();
int bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), int cpu_bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(),
params.channel_count()); 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) { if (linesize == 0) {
linesize = width * bytes_per_pixel; linesize = width * cpu_bytes_per_pixel;
} }
VkDeviceSize image_size = VkDeviceSize image_size =
static_cast<VkDeviceSize>(width) * height * bytes_per_pixel; static_cast<VkDeviceSize>(width) * height * gpu_bytes_per_pixel;
VkBuffer staging_buffer; VkBuffer staging_buffer;
VkDeviceMemory staging_memory; VkDeviceMemory staging_memory;
@@ -1156,20 +1316,43 @@ void VulkanRenderer::DownloadFromTexture(const QVariant &handle,
void *mapped; void *mapped;
vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped);
if (linesize == width * bytes_per_pixel) { if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
memcpy(data, mapped, static_cast<size_t>(image_size)); if (linesize == width * cpu_bytes_per_pixel) {
memcpy(data, mapped, static_cast<size_t>(image_size));
} else {
char *dst = static_cast<char *>(data);
const char *src = static_cast<const char *>(mapped);
for (int row = 0; row < height; row++) {
memcpy(dst + row * linesize,
src + row * width * cpu_bytes_per_pixel,
static_cast<size_t>(width * cpu_bytes_per_pixel));
}
}
} else { } else {
char *dst = static_cast<char *>(data); int gpu_channels = gpu_bytes_per_pixel /
const char *src = static_cast<const char *>(mapped); VideoParams::GetBytesPerChannel(params.format());
for (int row = 0; row < height; row++) { QByteArray tmp(width * height * gpu_bytes_per_pixel, Qt::Uninitialized);
memcpy(dst + row * linesize, memcpy(tmp.data(), mapped, static_cast<size_t>(tmp.size()));
src + row * width * bytes_per_pixel, CopyPixelsWithChannelConversion(tmp.constData(), data,
static_cast<size_t>(width * bytes_per_pixel)); 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<const char *>(data),
width * height * cpu_bytes_per_pixel);
char *dst = static_cast<char *>(data);
for (int row = 0; row < height; row++) {
memcpy(dst + row * linesize,
tight.constData() + row * width * cpu_bytes_per_pixel,
static_cast<size_t>(width * cpu_bytes_per_pixel));
}
} }
} }
vkUnmapMemory(device_, staging_memory); vkUnmapMemory(device_, staging_memory);
DestroyStagingBuffer(staging_buffer, staging_memory); DestroyStagingBuffer(staging_buffer, staging_memory);
tex->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
} }
void VulkanRenderer::Flush() void VulkanRenderer::Flush()
@@ -1222,9 +1405,9 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture,
if (!texture) { if (!texture) {
return Color(); return Color();
} }
int bytes_per_pixel = VideoParams::GetBytesPerPixel(texture->format(), int cpu_bytes_per_pixel = VideoParams::GetBytesPerPixel(texture->format(),
texture->channel_count()); texture->channel_count());
QByteArray data(bytes_per_pixel, Qt::Uninitialized); QByteArray data(cpu_bytes_per_pixel, Qt::Uninitialized);
quint64 id = texture->id().value<quint64>(); quint64 id = texture->id().value<quint64>();
QMutexLocker lock(&mutex_); QMutexLocker lock(&mutex_);
@@ -1236,9 +1419,14 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture,
uint32_t px = static_cast<uint32_t>(qBound(0.0, pt.x(), double(tex->width - 1))); uint32_t px = static_cast<uint32_t>(qBound(0.0, pt.x(), double(tex->width - 1)));
uint32_t py = static_cast<uint32_t>(qBound(0.0, pt.y(), double(tex->height - 1))); uint32_t py = static_cast<uint32_t>(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; VkBuffer staging_buffer;
VkDeviceMemory staging_memory; VkDeviceMemory staging_memory;
if (!CreateStagingBuffer(bytes_per_pixel, &staging_buffer, &staging_memory)) { if (!CreateStagingBuffer(gpu_bytes_per_pixel, &staging_buffer, &staging_memory)) {
return Color(); return Color();
} }
@@ -1251,11 +1439,23 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture,
EndOneTimeCommands(cmd); EndOneTimeCommands(cmd);
void *mapped; void *mapped;
vkMapMemory(device_, staging_memory, 0, bytes_per_pixel, 0, &mapped); vkMapMemory(device_, staging_memory, 0, gpu_bytes_per_pixel, 0, &mapped);
memcpy(data.data(), mapped, static_cast<size_t>(bytes_per_pixel)); if (cpu_bytes_per_pixel == gpu_bytes_per_pixel) {
memcpy(data.data(), mapped, static_cast<size_t>(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<size_t>(gpu_bytes_per_pixel));
CopyPixelsWithChannelConversion(gpu_pixel.constData(), data.data(),
1, 1, 1,
gpu_channels, texture->channel_count(),
texture->format());
}
vkUnmapMemory(device_, staging_memory); vkUnmapMemory(device_, staging_memory);
DestroyStagingBuffer(staging_buffer, staging_memory); DestroyStagingBuffer(staging_buffer, staging_memory);
tex->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
return Color::fromData(data.data(), texture->format(), return Color::fromData(data.data(), texture->format(),
texture->channel_count()); texture->channel_count());
@@ -1845,36 +2045,15 @@ bool VulkanRenderer::CreatePipelineForShader(VulkanShader *shader,
return true; return true;
} }
void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, void VulkanRenderer::BlitPass(VulkanShader *shader, VulkanTexture *dest_tex,
olive::Texture *destination, const QVector<TextureBinding> &bindings,
VideoParams destination_params, const QByteArray &ubo_data,
bool clear_destination) const VideoParams &destination_params,
bool clear_destination, int iteration)
{ {
QMutexLocker lock(&mutex_); (void)iteration;
ShaderJob *s_job = dynamic_cast<ShaderJob *>(&a_job); if (!dest_tex) {
if (!s_job) {
return;
}
ShaderJob job(*s_job);
quint64 shader_id = shader_variant.value<quint64>();
VulkanShader *shader = shaders_.value(shader_id);
if (!shader) {
return;
}
VulkanTexture *dest_tex = nullptr;
if (destination) {
quint64 dest_id = destination->id().value<quint64>();
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";
return; return;
} }
@@ -1890,145 +2069,26 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
VkPipeline pipeline = shader->pipeline_cache.value(render_pass_format); 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<TextureBinding> bindings;
QByteArray ubo_data;
if (shader->ubo_size > 0) {
ubo_data.resize(static_cast<int>(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<quint64>();
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<int>(u.offset);
switch (value.type()) {
case NodeValue::kFloat:
*reinterpret_cast<float *>(dst) = static_cast<float>(value.toDouble());
break;
case NodeValue::kInt:
*reinterpret_cast<int *>(dst) = static_cast<int>(value.toInt());
break;
case NodeValue::kBoolean:
*reinterpret_cast<int *>(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<float>(c.red()),
static_cast<float>(c.green()),
static_cast<float>(c.blue()),
static_cast<float>(c.alpha()) };
memcpy(dst, col, sizeof(float) * 4);
break;
}
case NodeValue::kCombo:
*reinterpret_cast<int *>(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<int>(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<int *>(dst) = job.Get(QStringLiteral("ove_maintex_alpha")).toInt();
} else if (u.name == QStringLiteral("ove_force_opaque")) {
*reinterpret_cast<int *>(dst) = job.Get(QStringLiteral("ove_force_opaque")).toBool() ? 1 : 0;
} else if (u.name == QStringLiteral("ove_iteration")) {
*reinterpret_cast<int *>(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<int>(u.offset);
*reinterpret_cast<int *>(dst) = tb.tex ? 1 : 0;
break;
}
}
}
}
// Lazily create a per-texture framebuffer. The framebuffer is compatible // Lazily create a per-texture framebuffer. The framebuffer is compatible
// with any render pass that uses the same format and sample count, so we // 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. // build it once with the non-clear variant and reuse it.
VkFramebuffer framebuffer = VK_NULL_HANDLE; if (dest_tex->framebuffer == VK_NULL_HANDLE) {
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 = GetOrCreateRenderPass(render_pass_format, false);
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>(dest_tex->width);
fb_info.pAttachments = &dest_tex->view; fb_info.height = static_cast<uint32_t>(dest_tex->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); VkResult fb_result = vkCreateFramebuffer(device_, &fb_info, nullptr,
fb_info.layers = 1; &dest_tex->framebuffer);
VkResult fb_result = vkCreateFramebuffer(device_, &fb_info, nullptr, if (fb_result != VK_SUCCESS) {
&dest_tex->framebuffer); qWarning() << "Failed to create Vulkan framebuffer:" << fb_result;
if (fb_result != VK_SUCCESS) { return;
qWarning() << "Failed to create Vulkan framebuffer:" << fb_result;
return;
}
} }
framebuffer = dest_tex->framebuffer;
} }
VkFramebuffer framebuffer = dest_tex->framebuffer;
// Create UBO buffer if needed // Create UBO buffer if needed
VkBuffer ubo_buffer = VK_NULL_HANDLE; VkBuffer ubo_buffer = VK_NULL_HANDLE;
@@ -2108,13 +2168,13 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
if (!writes.isEmpty()) { if (!writes.isEmpty()) {
vkUpdateDescriptorSets(device_, writes.size(), writes.constData(), 0, vkUpdateDescriptorSets(device_, writes.size(), writes.constData(), 0,
nullptr); nullptr);
} }
} }
VkCommandBuffer cmd = BeginOneTimeCommands(); 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, TransitionImageLayout(cmd, dest_tex->image, dest_tex->current_layout,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
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); nullptr);
} }
// Draw // Draw
vkCmdDraw(cmd, 6, 1, 0, 0); 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 // Leave the destination in a shader-readable state so it can be sampled or
// downloaded without an extra layout transition on the caller side. // downloaded without an extra layout transition on the caller side.
if (dest_tex) { TransitionImageLayout(cmd, dest_tex->image,
TransitionImageLayout(cmd, dest_tex->image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); dest_tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
dest_tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
EndOneTimeCommands(cmd); 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<ShaderJob *>(&a_job);
if (!s_job) {
return;
}
ShaderJob job(*s_job);
quint64 shader_id = shader_variant.value<quint64>();
// 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<quint64>();
dest_tex = textures_.value(dest_id);
if (!dest_tex) {
return;
}
} else {
quint64 final_id = final_tex.texture->id().value<quint64>();
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<quint64>();
output_tex.native = textures_.value(id);
}
if (input_tex.texture) {
quint64 id = input_tex.texture->id().value<quint64>();
input_tex.native = textures_.value(id);
}
// Collect textures to bind and build base UBO data
QVector<TextureBinding> base_bindings;
QByteArray base_ubo_data;
if (shader->ubo_size > 0) {
base_ubo_data.resize(static_cast<int>(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<quint64>();
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<int>(u.offset);
switch (value.type()) {
case NodeValue::kFloat:
*reinterpret_cast<float *>(dst) = static_cast<float>(value.toDouble());
break;
case NodeValue::kInt:
*reinterpret_cast<int *>(dst) = static_cast<int>(value.toInt());
break;
case NodeValue::kBoolean:
*reinterpret_cast<int *>(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<float>(c.red()),
static_cast<float>(c.green()),
static_cast<float>(c.blue()),
static_cast<float>(c.alpha()) };
memcpy(dst, col, sizeof(float) * 4);
break;
}
case NodeValue::kCombo:
*reinterpret_cast<int *>(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<int>(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<int *>(dst) = job.Get(QStringLiteral("ove_maintex_alpha")).toInt();
} else if (u.name == QStringLiteral("ove_force_opaque")) {
*reinterpret_cast<int *>(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<int>(u.offset);
*reinterpret_cast<int *>(dst) = tb.tex ? 1 : 0;
break;
}
}
}
}
for (int iteration = 0; iteration < real_iteration_count; ++iteration) {
QVector<TextureBinding> 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<int>(u.offset);
*reinterpret_cast<int *>(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 } // namespace olive
+18
View File
@@ -109,6 +109,12 @@ private:
VkFormat PixelFormatToVkFormat(PixelFormat format, int channel_count) const; VkFormat PixelFormatToVkFormat(PixelFormat format, int channel_count) const;
VkFormat PickRenderableFormat(PixelFormat format, int channel_count) const; VkFormat PickRenderableFormat(PixelFormat format, int channel_count) const;
bool IsColorAttachmentSupported(VkFormat format) 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; VkDeviceSize AlignSize(VkDeviceSize size, VkDeviceSize alignment) const;
uint32_t FindMemoryType(uint32_t type_filter, uint32_t FindMemoryType(uint32_t type_filter,
@@ -134,6 +140,18 @@ private:
VkRenderPass GetOrCreateRenderPass(VkFormat format, bool clear); VkRenderPass GetOrCreateRenderPass(VkFormat format, bool clear);
struct TextureBinding {
QString name;
VulkanTexture *tex;
Texture::Interpolation interp;
};
void BlitPass(VulkanShader *shader, VulkanTexture *dest_tex,
const QVector<TextureBinding> &bindings,
const QByteArray &ubo_data,
const VideoParams &destination_params,
bool clear_destination, int iteration);
VkInstance instance_ = VK_NULL_HANDLE; VkInstance instance_ = VK_NULL_HANDLE;
VkPhysicalDevice physical_device_ = VK_NULL_HANDLE; VkPhysicalDevice physical_device_ = VK_NULL_HANDLE;
uint32_t physical_device_count_ = 0; uint32_t physical_device_count_ = 0;
+15 -11
View File
@@ -429,18 +429,22 @@ void ViewerDisplayWidget::OnPaint()
// This is a GPU texture, switch to it directly when possible. // This is a GPU texture, switch to it directly when possible.
if (texture && texture->renderer() && if (texture && texture->renderer() &&
texture->renderer() != renderer()) { texture->renderer() != renderer()) {
// Cross-renderer texture: download and re-upload if (texture->renderer()->IsOpenGL() && renderer()->IsOpenGL()) {
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; 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 { } else {
texture_ = texture; texture_ = texture;
+18 -9
View File
@@ -59,7 +59,7 @@
- 动态适配器:`DynamicRenderer``renderbackend_c.h` - 动态适配器:`DynamicRenderer``renderbackend_c.h`
- 必要的 value/config/工具:`node/value``node/param``node/valuedatabase``config/config``common/filefunctions``common/qtutils``common/avframeptr` - 必要的 value/config/工具:`node/value``node/param``node/valuedatabase``config/config``common/filefunctions``common/qtutils``common/avframeptr`
- `oakgl`/`oakvulkan` 现在只链接 `libolive-rendercore`,不再链接完整 `libolive-editor` - `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`,改为前向声明。 - `renderer.h` 移除 `node/node.h``render/colorprocessor.h``render/job/colortransformjob.h``job/pluginjob.h`,改为前向声明。
- `videoparams.h` 移除 `ofxImageEffect.h`OFX 字符串 setter 实现下移到 `videoparams.cpp` - `videoparams.h` 移除 `ofxImageEffect.h`OFX 字符串 setter 实现下移到 `videoparams.cpp`
@@ -70,7 +70,7 @@
剩余优化空间: 剩余优化空间:
- 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。 - 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。
## 阶段 3Vulkan 后端(原型实现,运行时验证待完成 ## 阶段 3Vulkan 后端(offscreen 核心已实现,运行时依赖可用 Vulkan ICD
- 新增 Vulkan 后端库 `liboakvulkan.so`(当系统安装了 Vulkan 头文件/库时构建;无 Vulkan 环境时 CMake 自动跳过)。 - 新增 Vulkan 后端库 `liboakvulkan.so`(当系统安装了 Vulkan 头文件/库时构建;无 Vulkan 环境时 CMake 自动跳过)。
- 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线;代码已合入,并在本机 NVIDIA Vulkan 驱动上通过了基础端到端渲染测试。 - 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线;代码已合入,并在本机 NVIDIA Vulkan 驱动上通过了基础端到端渲染测试。
@@ -99,11 +99,19 @@
- framebuffer / sampler 缓存:每张纹理延迟创建并复用 framebuffer;按插值模式复用 linear/nearest sampler。 - framebuffer / sampler 缓存:每张纹理延迟创建并复用 framebuffer;按插值模式复用 linear/nearest sampler。
- 单通道纹理 swizzleimage view 组件映射为 R→RGB、A=1,匹配 OpenGL 灰度行为。 - 单通道纹理 swizzleimage view 组件映射为 R→RGB、A=1,匹配 OpenGL 灰度行为。
- 纹理启用标志:为声明 `NAME_enabled` 的 shader 自动设置 0/1。 - 纹理启用标志:为声明 `NAME_enabled` 的 shader 自动设置 0/1。
- **已知限制 / 待完善** - **已修复 / 已实现**
- 链接边界已最小化,`liboakvulkan.so` 现在只依赖 `libolive-rendercore` - 链接边界已最小化,`liboakvulkan.so` 现在只依赖 `libolive-rendercore`
- 单通道/3-channel 格式的上传/下载 CPU 侧对齐、回退格式与请求格式不一致时的数据转换仍待完善 - 单通道/3-channel 格式的上传/下载 CPU 侧对齐:当 GPU 回退格式(如 3→4 channel)与请求格式不一致时,staging buffer 按实际 `VkFormat` 大小分配,并在 CPU 侧进行通道数转换(alpha 填最大值)
- `Blit` 尚未实现 iterative/pin-pong 多 pass(如 blur/glow 等依赖 `ShaderJob::GetIterationCount` 的效果目前只渲染第一 pass - `Blit` 实现 iterative/pin-pong 多 pass:根据 `ShaderJob::GetIterationCount` / `GetIterativeInput` 创建临时 ping-pong 纹理,每 pass 更新 `ove_iteration` 并替换迭代输入;最后一 pass 写入目标纹理
- 尚未在 viewer、proxy、thumbnail/cache、导出等完整渲染路径上验证 Vulkan 输出一致性;需要在真实 GPU 上手工测试并记录结果 - 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 依赖。
## 阶段 4Viewer 双后端(backend-neutral 路径已落地,Vulkan viewer 为原型) ## 阶段 4Viewer 双后端(backend-neutral 路径已落地,Vulkan viewer 为原型)
@@ -146,7 +154,8 @@
- [x] OpenGL 后端库可单独构建、加载、初始化、销毁。 - [x] OpenGL 后端库可单独构建、加载、初始化、销毁。
- [x] 用户能在配置中选择 OpenGL/Vulkan。 - [x] 用户能在配置中选择 OpenGL/Vulkan。
- [x] Vulkan 不可用时自动回退到 OpenGL,不崩溃;`RenderManager::backend()` 会在 `DynamicRenderer` 内部回退后同步为实际运行后端。 - [x] Vulkan 不可用时自动回退到 OpenGL,不崩溃;`RenderManager::backend()` 会在 `DynamicRenderer` 内部回退后同步为实际运行后端。
- [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积从约 21 MB 降至约 600 KB - [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积需按 release/strip 构建重新记录
- [x] Vulkan / backend-neutral viewer readback display 路径已搭建(offscreen texture → download → QImage → QPainter);单 pass Blit 已在真实 Vulkan 驱动上验证完整 UI/导出流程待验证 - [ ] 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 输出绑定。 - [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。
+183
View File
@@ -146,3 +146,186 @@ TEST(DynamicRenderBackend, VulkanUploadBlitDownload)
EXPECT_EQ(static_cast<uint8_t>(dst_data[3]), 255u); EXPECT_EQ(static_cast<uint8_t>(dst_data[3]), 255u);
#endif #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<char>(255);
src_data[i * 4 + 3] = static_cast<char>(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<char>(255);
src_data[i * 4 + 3] = static_cast<char>(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<uint8_t>(dst_data[0]), 63u);
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
}
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<char>(255);
src_data[i * 3 + 1] = static_cast<char>(128);
src_data[i * 3 + 2] = static_cast<char>(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<uint8_t>(dst_data[0]), 255u);
EXPECT_EQ(static_cast<uint8_t>(dst_data[1]), 128u);
EXPECT_EQ(static_cast<uint8_t>(dst_data[2]), 64u);
#endif
}
+3
View File
@@ -12,6 +12,9 @@ int main(int argc, char **argv)
.filePath(QStringLiteral( .filePath(QStringLiteral(
"app/render/ocioconf/config.ocio")))); "app/render/ocioconf/config.ocio"))));
} }
if (qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM")) {
qputenv("QT_QPA_PLATFORM", "offscreen");
}
QApplication app(argc, argv); QApplication app(argc, argv);
::testing::InitGoogleTest(&argc, argv); ::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS(); return RUN_ALL_TESTS();