Address ChatGPT Vulkan review items

- Add PickRenderableFormat / IsColorAttachmentSupported so 3-channel
  Vulkan formats fall back to 4-channel when unsupported.
- Make oakvulkan target and C ABI check conditional on Vulkan_FOUND;
  skip liboakvulkan build/dependencies when Vulkan headers/libs are
  absent.
- Update Preferences tooltip to reflect Vulkan is an experimental
  prototype that may fall back to OpenGL.
- Revise dynamic backend plan doc: phase 3/4/5 described as prototype
  frameworks with runtime validation pending, and list recent Vulkan
  fixes (init idempotency, descriptor/sampler lifetime, dynamic
  viewport/scissor, render pass clear, format probing).

Both OAK_ENABLE_DYNAMIC_RENDER_BACKEND=ON and OFF configurations
build and pass ctest (4/4).
This commit is contained in:
2026-07-13 10:19:29 +08:00
parent 225f8505c2
commit b4e39e5bcd
6 changed files with 201 additions and 101 deletions
+50 -44
View File
@@ -160,32 +160,32 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
ARCHIVE DESTINATION lib ARCHIVE DESTINATION lib
) )
add_library(oakvulkan SHARED
render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
)
target_link_libraries(oakvulkan PRIVATE libolive-rendercore)
if(Vulkan_FOUND) if(Vulkan_FOUND)
add_library(oakvulkan SHARED
render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
)
target_link_libraries(oakvulkan PRIVATE libolive-rendercore)
target_link_libraries(oakvulkan PRIVATE Vulkan::Vulkan) target_link_libraries(oakvulkan PRIVATE Vulkan::Vulkan)
target_compile_definitions(oakvulkan PRIVATE OAK_HAS_VULKAN) target_compile_definitions(oakvulkan PRIVATE OAK_HAS_VULKAN)
if(SHADERC_FOUND)
target_link_libraries(oakvulkan PRIVATE ${SHADERC_LIBRARIES})
target_include_directories(oakvulkan PRIVATE ${SHADERC_INCLUDE_DIRS})
target_compile_definitions(oakvulkan PRIVATE OAK_HAS_SHADERC)
endif()
set_target_properties(oakvulkan PROPERTIES
OUTPUT_NAME oakvulkan
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
install(TARGETS oakvulkan
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
endif() endif()
if(SHADERC_FOUND)
target_link_libraries(oakvulkan PRIVATE ${SHADERC_LIBRARIES})
target_include_directories(oakvulkan PRIVATE ${SHADERC_INCLUDE_DIRS})
target_compile_definitions(oakvulkan PRIVATE OAK_HAS_SHADERC)
endif()
set_target_properties(oakvulkan PROPERTIES
OUTPUT_NAME oakvulkan
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
install(TARGETS oakvulkan
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
endif() endif()
add_library(oakgl-cabi-check OBJECT add_library(oakgl-cabi-check OBJECT
@@ -201,29 +201,29 @@ target_link_libraries(oakgl-cabi-check PRIVATE ${OLIVE_LIBRARIES} OfxHost)
target_compile_definitions(oakgl-cabi-check PRIVATE ${OLIVE_DEFINITIONS}) target_compile_definitions(oakgl-cabi-check PRIVATE ${OLIVE_DEFINITIONS})
target_compile_options(oakgl-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS}) target_compile_options(oakgl-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS})
add_library(oakvulkan-cabi-check OBJECT
render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
)
target_include_directories(oakvulkan-cabi-check PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/third_party/openfx/include
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
${OLIVE_INCLUDE_DIRS}
)
target_link_libraries(oakvulkan-cabi-check PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets)
if(Vulkan_FOUND) if(Vulkan_FOUND)
add_library(oakvulkan-cabi-check OBJECT
render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
)
target_include_directories(oakvulkan-cabi-check PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/third_party/openfx/include
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
${OLIVE_INCLUDE_DIRS}
)
target_link_libraries(oakvulkan-cabi-check PRIVATE Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets)
target_link_libraries(oakvulkan-cabi-check PRIVATE Vulkan::Vulkan) target_link_libraries(oakvulkan-cabi-check PRIVATE Vulkan::Vulkan)
target_compile_definitions(oakvulkan-cabi-check PRIVATE OAK_HAS_VULKAN) target_compile_definitions(oakvulkan-cabi-check PRIVATE OAK_HAS_VULKAN)
if(SHADERC_FOUND)
target_link_libraries(oakvulkan-cabi-check PRIVATE ${SHADERC_LIBRARIES})
target_include_directories(oakvulkan-cabi-check PRIVATE ${SHADERC_INCLUDE_DIRS})
target_compile_definitions(oakvulkan-cabi-check PRIVATE OAK_HAS_SHADERC)
endif()
target_compile_definitions(oakvulkan-cabi-check PRIVATE ${OLIVE_DEFINITIONS})
target_compile_options(oakvulkan-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS})
endif() endif()
if(SHADERC_FOUND)
target_link_libraries(oakvulkan-cabi-check PRIVATE ${SHADERC_LIBRARIES})
target_include_directories(oakvulkan-cabi-check PRIVATE ${SHADERC_INCLUDE_DIRS})
target_compile_definitions(oakvulkan-cabi-check PRIVATE OAK_HAS_SHADERC)
endif()
target_compile_definitions(oakvulkan-cabi-check PRIVATE ${OLIVE_DEFINITIONS})
target_compile_options(oakvulkan-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS})
# Add application # Add application
add_executable(olive-editor add_executable(olive-editor
@@ -233,9 +233,12 @@ add_executable(olive-editor
) )
target_include_directories(olive-editor PUBLIC pluginSupport) target_include_directories(olive-editor PUBLIC pluginSupport)
target_link_libraries(olive-editor PUBLIC OfxHost) target_link_libraries(olive-editor PUBLIC OfxHost)
add_dependencies(olive-editor oakgl-cabi-check oakvulkan-cabi-check) add_dependencies(olive-editor oakgl-cabi-check)
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
add_dependencies(olive-editor oakgl oakvulkan) add_dependencies(olive-editor oakgl)
if (TARGET oakvulkan)
add_dependencies(olive-editor oakvulkan)
endif()
endif() endif()
# Add render worker process (olive-render-worker). # Add render worker process (olive-render-worker).
@@ -252,7 +255,10 @@ target_include_directories(olive-render-worker PUBLIC pluginSupport)
target_link_libraries(olive-render-worker PUBLIC OfxHost) target_link_libraries(olive-render-worker PUBLIC OfxHost)
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
target_compile_definitions(olive-render-worker PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND) target_compile_definitions(olive-render-worker PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
add_dependencies(olive-render-worker oakgl oakvulkan) add_dependencies(olive-render-worker oakgl)
if (TARGET oakvulkan)
add_dependencies(olive-render-worker oakvulkan)
endif()
endif() endif()
# Create docs if doxygen was found # Create docs if doxygen was found
if(DOXYGEN_FOUND) if(DOXYGEN_FOUND)
@@ -104,8 +104,8 @@ PreferencesBehaviorTab::PreferencesBehaviorTab()
new QTreeWidgetItem({ tr("Graphics Backend") }); new QTreeWidgetItem({ tr("Graphics Backend") });
graphics_backend_item->setToolTip( graphics_backend_item->setToolTip(
0, tr("Selects the graphics API Oak should request on next launch. " 0, tr("Selects the graphics API Oak should request on next launch. "
"Vulkan is experimental and currently falls back to the OpenGL " "Vulkan is experimental: on most systems it will fall back to "
"renderer for timeline/viewer rendering.")); "OpenGL or use a prototype Vulkan path that is not yet fully validated."));
rendering_group->addChild(graphics_backend_item); rendering_group->addChild(graphics_backend_item);
graphics_backend_combobox_ = new QComboBox(); graphics_backend_combobox_ = new QComboBox();
graphics_backend_combobox_->addItem(tr("OpenGL"), QStringLiteral("opengl")); graphics_backend_combobox_->addItem(tr("OpenGL"), QStringLiteral("opengl"));
+114 -33
View File
@@ -45,6 +45,7 @@ struct VulkanRenderer::VulkanShader {
bool pipeline_created = false; bool pipeline_created = false;
QVector<UniformInfo> uniforms; QVector<UniformInfo> uniforms;
VkDeviceSize ubo_size = 0; VkDeviceSize ubo_size = 0;
int sampler_count = 0;
}; };
static const float kBlitVertices[] = { static const float kBlitVertices[] = {
@@ -68,12 +69,18 @@ VulkanRenderer::~VulkanRenderer()
bool VulkanRenderer::Init() bool VulkanRenderer::Init()
{ {
if (instance_ != VK_NULL_HANDLE) {
return true;
}
return CreateInstance() && CreateDevice() && CreateCommandPool() && return CreateInstance() && CreateDevice() && CreateCommandPool() &&
CreateDescriptorPool(); CreateDescriptorPool();
} }
void VulkanRenderer::PostInit() void VulkanRenderer::PostInit()
{ {
if (vertex_buffer_ != VK_NULL_HANDLE) {
return;
}
CreateVertexBuffer(); CreateVertexBuffer();
} }
@@ -290,9 +297,10 @@ bool VulkanRenderer::CreateDescriptorPool()
return true; return true;
} }
VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format) VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format, bool clear)
{ {
auto it = render_pass_cache_.find(format); const quint64 key = (static_cast<quint64>(format) << 1) | (clear ? 1ULL : 0ULL);
auto it = render_pass_cache_.find(key);
if (it != render_pass_cache_.end()) { if (it != render_pass_cache_.end()) {
return it.value(); return it.value();
} }
@@ -300,7 +308,8 @@ VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format)
VkAttachmentDescription color_attachment = {}; VkAttachmentDescription color_attachment = {};
color_attachment.format = format; color_attachment.format = format;
color_attachment.samples = VK_SAMPLE_COUNT_1_BIT; color_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color_attachment.loadOp = clear ? VK_ATTACHMENT_LOAD_OP_CLEAR :
VK_ATTACHMENT_LOAD_OP_LOAD;
color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
@@ -331,7 +340,7 @@ VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format)
return VK_NULL_HANDLE; return VK_NULL_HANDLE;
} }
render_pass_cache_.insert(format, render_pass); render_pass_cache_.insert(key, render_pass);
return render_pass; return render_pass;
} }
@@ -639,6 +648,34 @@ VkFormat VulkanRenderer::PixelFormatToVkFormat(PixelFormat format,
return VK_FORMAT_UNDEFINED; return VK_FORMAT_UNDEFINED;
} }
bool VulkanRenderer::IsColorAttachmentSupported(VkFormat format) const
{
VkFormatProperties props;
vkGetPhysicalDeviceFormatProperties(physical_device_, format, &props);
return (props.optimalTilingFeatures &
VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
}
VkFormat VulkanRenderer::PickRenderableFormat(PixelFormat format,
int channel_count) const
{
VkFormat candidate = PixelFormatToVkFormat(format, channel_count);
if (candidate != VK_FORMAT_UNDEFINED && IsColorAttachmentSupported(candidate)) {
return candidate;
}
// 3-channel formats are often unsupported as color attachments; fallback
// to the 4-channel equivalent.
if (channel_count == 3) {
VkFormat rgba = PixelFormatToVkFormat(format, 4);
if (rgba != VK_FORMAT_UNDEFINED && IsColorAttachmentSupported(rgba)) {
return rgba;
}
}
return VK_FORMAT_UNDEFINED;
}
VkDeviceSize VulkanRenderer::AlignSize(VkDeviceSize size, VkDeviceSize VulkanRenderer::AlignSize(VkDeviceSize size,
VkDeviceSize alignment) const VkDeviceSize alignment) const
{ {
@@ -664,7 +701,7 @@ QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth,
{ {
QMutexLocker lock(&mutex_); QMutexLocker lock(&mutex_);
VkFormat vk_format = PixelFormatToVkFormat(format, channel_count); VkFormat vk_format = PickRenderableFormat(format, channel_count);
if (vk_format == VK_FORMAT_UNDEFINED) { if (vk_format == VK_FORMAT_UNDEFINED) {
qWarning() << "Unsupported pixel format for Vulkan texture"; qWarning() << "Unsupported pixel format for Vulkan texture";
return QVariant(); return QVariant();
@@ -1072,18 +1109,19 @@ VkDeviceSize VulkanRenderer::GetStd140Alignment(const QString &type) const
} }
QString VulkanRenderer::ConvertGlslUniformsToUbo(const QString &glsl, QString VulkanRenderer::ConvertGlslUniformsToUbo(const QString &glsl,
QVector<UniformInfo> *out_uniforms) QVector<UniformInfo> *out_uniforms,
int *out_sampler_count)
{ {
QString result = glsl; QString result = glsl;
QVector<UniformInfo> uniforms; QVector<UniformInfo> uniforms;
// Regex to match uniform declarations like: uniform vec4 color; // Regex to match uniform declarations like: uniform vec4 color;
// Exclude sampler types
QRegularExpression re(QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"), QRegularExpression re(QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"),
QRegularExpression::MultilineOption); QRegularExpression::MultilineOption);
QRegularExpression sampler_re(QStringLiteral(R"(sampler\d*D|samplerCube|sampler2DArray)")); QRegularExpression sampler_re(QStringLiteral(R"(sampler\d*D|samplerCube|sampler2DArray)"));
int offset = 0; int offset = 0;
int sampler_count = 0;
QRegularExpressionMatchIterator it = re.globalMatch(result); QRegularExpressionMatchIterator it = re.globalMatch(result);
QVector<QRegularExpressionMatch> matches; QVector<QRegularExpressionMatch> matches;
while (it.hasNext()) { while (it.hasNext()) {
@@ -1097,7 +1135,15 @@ QString VulkanRenderer::ConvertGlslUniformsToUbo(const QString &glsl,
QString name = m.captured(2); QString name = m.captured(2);
if (sampler_re.match(type).hasMatch()) { if (sampler_re.match(type).hasMatch()) {
continue; // Let shaderc auto-bind samplers // 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;
} }
UniformInfo info; UniformInfo info;
@@ -1144,6 +1190,9 @@ QString VulkanRenderer::ConvertGlslUniformsToUbo(const QString &glsl,
if (out_uniforms) { if (out_uniforms) {
*out_uniforms = uniforms; *out_uniforms = uniforms;
} }
if (out_sampler_count) {
*out_sampler_count = sampler_count;
}
return result; return result;
} }
@@ -1234,7 +1283,9 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code)
// Convert fragment shader uniforms to UBO before compiling // Convert fragment shader uniforms to UBO before compiling
QVector<UniformInfo> frag_uniforms; QVector<UniformInfo> frag_uniforms;
QString converted_frag = ConvertGlslUniformsToUbo(frag_code, &frag_uniforms); int sampler_count = 0;
QString converted_frag = ConvertGlslUniformsToUbo(frag_code, &frag_uniforms,
&sampler_count);
if (!CompileGlslToSpv(vert_code, VK_SHADER_STAGE_VERTEX_BIT, &vert_spv)) { if (!CompileGlslToSpv(vert_code, VK_SHADER_STAGE_VERTEX_BIT, &vert_spv)) {
return QVariant(); return QVariant();
@@ -1246,6 +1297,7 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code)
VulkanShader *sh = new VulkanShader(); VulkanShader *sh = new VulkanShader();
sh->id = next_shader_id_++; sh->id = next_shader_id_++;
sh->uniforms = frag_uniforms; sh->uniforms = frag_uniforms;
sh->sampler_count = sampler_count;
sh->ubo_size = 0; sh->ubo_size = 0;
for (const UniformInfo &u : frag_uniforms) { for (const UniformInfo &u : frag_uniforms) {
sh->ubo_size = qMax(sh->ubo_size, u.offset + u.size); sh->ubo_size = qMax(sh->ubo_size, u.offset + u.size);
@@ -1275,22 +1327,31 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code)
return QVariant(); return QVariant();
} }
// Create descriptor set layout: binding 0 = UBO, binding 1+ = samplers // Create descriptor set layout: binding 0 = UBO, binding 1..N = samplers
VkDescriptorSetLayoutBinding bindings[2] = {}; QVector<VkDescriptorSetLayoutBinding> bindings;
bindings[0].binding = 0; bindings.reserve(1 + sampler_count);
bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
bindings[0].descriptorCount = 1;
bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
bindings[1].binding = 1; VkDescriptorSetLayoutBinding ubo_binding = {};
bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; ubo_binding.binding = 0;
bindings[1].descriptorCount = 16; // max textures ubo_binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; ubo_binding.descriptorCount = 1;
ubo_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
bindings.append(ubo_binding);
for (int i = 0; i < sampler_count; ++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;
bindings.append(sampler_binding);
}
VkDescriptorSetLayoutCreateInfo ds_layout_info = {}; VkDescriptorSetLayoutCreateInfo ds_layout_info = {};
ds_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; ds_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
ds_layout_info.bindingCount = 2; ds_layout_info.bindingCount = static_cast<uint32_t>(bindings.size());
ds_layout_info.pBindings = bindings; ds_layout_info.pBindings = bindings.constData();
result = vkCreateDescriptorSetLayout(device_, &ds_layout_info, nullptr, result = vkCreateDescriptorSetLayout(device_, &ds_layout_info, nullptr,
&sh->descriptor_layout); &sh->descriptor_layout);
@@ -1437,6 +1498,13 @@ bool VulkanRenderer::CreatePipelineForShader(VulkanShader *shader,
color_blending.attachmentCount = 1; color_blending.attachmentCount = 1;
color_blending.pAttachments = &color_blend; color_blending.pAttachments = &color_blend;
VkDynamicState dynamic_states[] = { VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR };
VkPipelineDynamicStateCreateInfo dynamic_state = {};
dynamic_state.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic_state.dynamicStateCount = 2;
dynamic_state.pDynamicStates = dynamic_states;
VkGraphicsPipelineCreateInfo pipeline_info = {}; VkGraphicsPipelineCreateInfo pipeline_info = {};
pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipeline_info.stageCount = 2; pipeline_info.stageCount = 2;
@@ -1447,8 +1515,9 @@ bool VulkanRenderer::CreatePipelineForShader(VulkanShader *shader,
pipeline_info.pRasterizationState = &rasterizer; pipeline_info.pRasterizationState = &rasterizer;
pipeline_info.pMultisampleState = &multisampling; pipeline_info.pMultisampleState = &multisampling;
pipeline_info.pColorBlendState = &color_blending; pipeline_info.pColorBlendState = &color_blending;
pipeline_info.pDynamicState = &dynamic_state;
pipeline_info.layout = shader->pipeline_layout; pipeline_info.layout = shader->pipeline_layout;
pipeline_info.renderPass = GetOrCreateRenderPass(render_pass_format); pipeline_info.renderPass = GetOrCreateRenderPass(render_pass_format, false);
pipeline_info.subpass = 0; pipeline_info.subpass = 0;
VkPipeline new_pipeline = VK_NULL_HANDLE; VkPipeline new_pipeline = VK_NULL_HANDLE;
@@ -1493,7 +1562,7 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
} }
VkFormat render_pass_format = dest_tex ? dest_tex->vk_format : VK_FORMAT_R32G32B32A32_SFLOAT; VkFormat render_pass_format = dest_tex ? dest_tex->vk_format : VK_FORMAT_R32G32B32A32_SFLOAT;
VkRenderPass render_pass = GetOrCreateRenderPass(render_pass_format); VkRenderPass render_pass = GetOrCreateRenderPass(render_pass_format, clear_destination);
if (render_pass == VK_NULL_HANDLE) { if (render_pass == VK_NULL_HANDLE) {
return; return;
} }
@@ -1640,6 +1709,14 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
dest_tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; dest_tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
} }
for (const TextureBinding &tb : bindings) {
if (tb.tex && tb.tex->current_layout != VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
TransitionImageLayout(cmd, tb.tex->image, tb.tex->current_layout,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
tb.tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
}
}
VkRenderPassBeginInfo rp_begin = {}; VkRenderPassBeginInfo rp_begin = {};
rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rp_begin.renderPass = render_pass; rp_begin.renderPass = render_pass;
@@ -1682,6 +1759,10 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
// Create and update descriptor set // Create and update descriptor set
VkDescriptorSet descriptor_set = VK_NULL_HANDLE; 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()) { if (shader->ubo_size > 0 || !bindings.isEmpty()) {
VkDescriptorSetAllocateInfo ds_alloc = {}; VkDescriptorSetAllocateInfo ds_alloc = {};
ds_alloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; ds_alloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
@@ -1695,7 +1776,6 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
// UBO binding // UBO binding
if (ubo_buffer != VK_NULL_HANDLE) { if (ubo_buffer != VK_NULL_HANDLE) {
VkDescriptorBufferInfo buffer_info = {};
buffer_info.buffer = ubo_buffer; buffer_info.buffer = ubo_buffer;
buffer_info.offset = 0; buffer_info.offset = 0;
buffer_info.range = shader->ubo_size; buffer_info.range = shader->ubo_size;
@@ -1720,10 +1800,9 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
VkSampler sampler;
vkCreateSampler(device_, &sampler_info, nullptr, &sampler); vkCreateSampler(device_, &sampler_info, nullptr, &sampler);
QVector<VkDescriptorImageInfo> image_infos; image_infos.reserve(qMin(bindings.size(), 16));
for (int i = 0; i < bindings.size() && i < 16; i++) { for (int i = 0; i < bindings.size() && i < 16; i++) {
const TextureBinding &tb = bindings.at(i); const TextureBinding &tb = bindings.at(i);
VkDescriptorImageInfo img_info = {}; VkDescriptorImageInfo img_info = {};
@@ -1731,21 +1810,17 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE; img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE;
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
image_infos.append(img_info); image_infos.append(img_info);
}
if (!image_infos.isEmpty()) {
VkWriteDescriptorSet write = {}; VkWriteDescriptorSet write = {};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptor_set; write.dstSet = descriptor_set;
write.dstBinding = 1; write.dstBinding = 1 + i;
write.dstArrayElement = 0; write.dstArrayElement = 0;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.descriptorCount = static_cast<uint32_t>(image_infos.size()); write.descriptorCount = 1;
write.pImageInfo = image_infos.constData(); write.pImageInfo = &image_infos.last();
writes.append(write); writes.append(write);
} }
vkDestroySampler(device_, sampler, nullptr);
} }
if (!writes.isEmpty()) { if (!writes.isEmpty()) {
@@ -1766,6 +1841,12 @@ void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job,
EndOneTimeCommands(cmd); EndOneTimeCommands(cmd);
if (sampler != VK_NULL_HANDLE) {
vkDestroySampler(device_, sampler, nullptr);
}
if (descriptor_set != VK_NULL_HANDLE) {
vkFreeDescriptorSets(device_, descriptor_pool_, 1, &descriptor_set);
}
if (framebuffer != VK_NULL_HANDLE) { if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, framebuffer, nullptr); vkDestroyFramebuffer(device_, framebuffer, nullptr);
} }
+6 -3
View File
@@ -105,6 +105,8 @@ private:
uint32_t offset_x = 0, uint32_t offset_y = 0); uint32_t offset_x = 0, uint32_t offset_y = 0);
VkFormat PixelFormatToVkFormat(PixelFormat format, int channel_count) const; VkFormat PixelFormatToVkFormat(PixelFormat format, int channel_count) const;
VkFormat PickRenderableFormat(PixelFormat format, int channel_count) const;
bool IsColorAttachmentSupported(VkFormat 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,
@@ -114,7 +116,8 @@ private:
QByteArray *out_spv); QByteArray *out_spv);
QString ConvertGlslToVulkan(const QString &glsl, VkShaderStageFlagBits stage); QString ConvertGlslToVulkan(const QString &glsl, VkShaderStageFlagBits stage);
QString ConvertGlslUniformsToUbo(const QString &glsl, QString ConvertGlslUniformsToUbo(const QString &glsl,
QVector<UniformInfo> *out_uniforms); QVector<UniformInfo> *out_uniforms,
int *out_sampler_count = nullptr);
VkDeviceSize GetStd140Size(const QString &type) const; VkDeviceSize GetStd140Size(const QString &type) const;
VkDeviceSize GetStd140Alignment(const QString &type) const; VkDeviceSize GetStd140Alignment(const QString &type) const;
@@ -122,7 +125,7 @@ private:
const VideoParams &dest_params, const VideoParams &dest_params,
VkFormat render_pass_format); VkFormat render_pass_format);
VkRenderPass GetOrCreateRenderPass(VkFormat format); VkRenderPass GetOrCreateRenderPass(VkFormat format, bool clear);
VkInstance instance_ = VK_NULL_HANDLE; VkInstance instance_ = VK_NULL_HANDLE;
VkPhysicalDevice physical_device_ = VK_NULL_HANDLE; VkPhysicalDevice physical_device_ = VK_NULL_HANDLE;
@@ -133,7 +136,7 @@ private:
VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE; VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE;
VkSampler linear_sampler_ = VK_NULL_HANDLE; VkSampler linear_sampler_ = VK_NULL_HANDLE;
QHash<VkFormat, VkRenderPass> render_pass_cache_; QHash<quint64, VkRenderPass> render_pass_cache_;
VkBuffer vertex_buffer_ = VK_NULL_HANDLE; VkBuffer vertex_buffer_ = VK_NULL_HANDLE;
VkDeviceMemory vertex_buffer_memory_ = VK_NULL_HANDLE; VkDeviceMemory vertex_buffer_memory_ = VK_NULL_HANDLE;
+25 -18
View File
@@ -70,49 +70,55 @@
剩余优化空间: 剩余优化空间:
- 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。 - 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。
## 阶段 3Vulkan 后端 ## 阶段 3Vulkan 后端(原型实现,运行时验证待完成)
- 新增 Vulkan 后端库 `liboakvulkan.so` - 新增 Vulkan 后端库 `liboakvulkan.so`(当系统安装了 Vulkan 头文件/库时构建;无 Vulkan 环境时 CMake 自动跳过)
- 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线。 - 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线;代码已合入,但尚未在真实 Vulkan 驱动上完整跑通
- CMake 集成:根目录查找 `Vulkan``shaderc`(可选);`oakvulkan` 目标链接 `Vulkan::Vulkan``shaderc_shared` - CMake 集成:根目录查找 `Vulkan``shaderc`(可选);`oakvulkan` 目标链接 `Vulkan::Vulkan``shaderc_shared`;若 `Vulkan` 未找到则不构建该库,避免无 Vulkan 头文件时编译失败
- 实现 Vulkan instance/device/queue/command pool 管理。 - 实现 Vulkan instance/device/queue/command pool 管理(代码层完成)
- 实现 offscreen image/texture 管理(`CreateNativeTexture` / `DestroyNativeTexture`),支持 2D/3D、多种 pixel formatU8/U16/F16/F32 × 1/2/3/4 channel)。 - 实现 offscreen image/texture 管理(`CreateNativeTexture` / `DestroyNativeTexture`),支持 2D/3D、多种 pixel formatU8/U16/F16/F32 × 1/2/3/4 channel3-channel 格式会探测 `COLOR_ATTACHMENT` 支持并自动回退到 4-channel 等价格式
- 实现 staging buffer 上传/下载(`UploadToTexture` / `DownloadFromTexture`)。 - 实现 staging buffer 上传/下载(`UploadToTexture` / `DownloadFromTexture`)。
- 实现 `ClearDestination``vkCmdClearColorImage`)。 - 实现 `ClearDestination``vkCmdClearColorImage`)。
- 实现 `Flush``vkDeviceWaitIdle`)。 - 实现 `Flush``vkDeviceWaitIdle`)。
- 实现 GLSL → SPIR-V 运行时编译(通过 `shaderc`),支持自动 uniform binding。 - 实现 GLSL → SPIR-V 运行时编译(通过 `shaderc`),支持自动 uniform binding。
- 实现基础 graphics pipeline 用于 `Blit`(全屏 quad、顶点缓冲、固定 render pass、combined image sampler descriptor set)。 - 实现基础 graphics pipeline 用于 `Blit`(全屏 quad、顶点缓冲、按格式缓存的 render pass、combined image sampler descriptor set)。
- 提供 `GetPixelFromTexture`(基于 `DownloadFromTexture` 的简化实现)。 - 提供 `GetPixelFromTexture`(基于 `DownloadFromTexture` 的简化实现)。
- `oak_renderer_is_available` 现在会在首次检查时尝试 `Init()`,成功后报告 Vulkan 可用。 - `oak_renderer_is_available` 现在会在首次检查时尝试 `Init()`,成功后报告 Vulkan 可用。
- 测试更新: - 测试更新:
- `LoadsExperimentalVulkanBackendWhenAvailable`:验证 Vulkan 后端可加载、初始化、报告能力位。 - `LoadsExperimentalVulkanBackendWhenAvailable`:验证 Vulkan 后端可加载、初始化、报告能力位。
- `FallsBackWhenExperimentalVulkanUnavailable`:在 Vulkan 不可用的系统上验证回退 OpenGL;在 Vulkan 可用的系统上自动 SKIP。 - `FallsBackWhenExperimentalVulkanUnavailable`:在 Vulkan 不可用的系统上验证回退 OpenGL;在 Vulkan 可用的系统上自动 SKIP。
- **已修复的限制** - **已修复的明显问题(代码层)**
- `Blit` 中的 uniform/push constant 传递:已实现完整的 UBO 路径。`CreateNativeShader` 编译前自动将 GLSL 独立 uniform 转换为 `layout(set=0, binding=0) uniform UniformBuffer` 块;`Blit` 遍历 `ShaderJob` values 按 std140 布局填充 UBO 数据并绑定到 descriptor set - 初始化幂等性:`Init()` / `PostInit()` 可安全重复调用
- `GetPixelFromTexture`:已优化为仅下载 1×1 像素区域 - `Blit` 中的 descriptor/sampler 生命周期:sampler 与 descriptor set 在 `EndOneTimeCommands` 后统一释放
- RenderPass 格式缓存:已删除固定 `R32G32B32A32_SFLOAT` render pass,改为按 `VkFormat` 缓存;`CreatePipelineForShader` 按 (shader, render_pass_format) 缓存 pipeline - sampler binding:从数组绑定改为显式 `layout(set=0, binding=N)`,避免跨驱动 array-of-samplers 行为不一致
- image layout 跟踪:输入纹理在绘制前被过渡到 `SHADER_READ_ONLY_OPTIMAL`
- viewport/scissor:改为 dynamic state,避免 pipeline 缓存 key 遗漏视口尺寸。
- render pass clear`clear_destination` 为 true 时 `loadOp` 设为 `CLEAR`
- 格式支持探测:通过 `vkGetPhysicalDeviceFormatProperties` 检查 `COLOR_ATTACHMENT` 能力,3-channel 不支持时回退到 4-channel。
- **已知限制 / 待完善** - **已知限制 / 待完善**
- 链接边界已最小化,`liboakvulkan.so` 现在只依赖 `libolive-rendercore` - 链接边界已最小化,`liboakvulkan.so` 现在只依赖 `libolive-rendercore`
- 尚未在 proxy、thumbnail/cache 等完整渲染路径上验证 Vulkan 输出一致性。 - 尚未在真实 Vulkan 驱动/设备上验证渲染正确性。
- 尚未在 proxy、thumbnail/cache、导出等完整渲染路径上验证 Vulkan 输出一致性。
## 阶段 4Viewer 双后端(默认路径已切换 ## 阶段 4Viewer 双后端(backend-neutral 路径已落地,Vulkan viewer 为原型
- 当前 viewer display 基于 OpenGL widget 和 GL texture id。 - 当前 viewer display 基于 OpenGL widget 和 GL texture id。
- 默认构建下 Viewer 的 managed display 现在使用 `DynamicRenderer` 创建 renderer,并把现有 `QOpenGLContext` 传入动态后端;若动态后端加载失败则回退到 `OpenGLRenderer` - 默认构建下 Viewer 的 managed display 现在使用 `DynamicRenderer` 创建 renderer,并把现有 `QOpenGLContext` 传入动态后端;若动态后端加载失败则回退到 `OpenGLRenderer`
- `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认改为 `ON`,保留 `OFF` 作为应急开关。 - `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认改为 `ON`,保留 `OFF` 作为应急开关。
- 新增 backend-neutral viewer path 架: - 新增 backend-neutral viewer path 架:
- `ManagedDisplayWidget` 支持非 OpenGL inner widget(普通 `QWidget`),通过 `Renderer::IsOpenGL()` 判断。 - `ManagedDisplayWidget` 支持非 OpenGL inner widget(普通 `QWidget`),通过 `Renderer::IsOpenGL()` 判断。
- `RenderManager` 不再在 `requested_backend_ == kVulkan` 时强制 fallback。 - `RenderManager` 不再在 `requested_backend_ == kVulkan` 时强制 fallback。
- `ViewerDisplayWidget` 已移除 `glIsTexture()` 的直接 OpenGL 依赖,改为通用的跨 renderer texture 拷贝。 - `ViewerDisplayWidget` 已移除 `glIsTexture()` 的直接 OpenGL 依赖,改为通用的跨 renderer texture 拷贝。
- `ScopeBase` 在 backend-neutral 时安全跳过(TODO:完整 scope display 路径)。 - `ScopeBase` 在 backend-neutral 时安全跳过(TODO:完整 scope display 路径)。
- OpenGL 使用现有 `QOpenGLWidget/QOpenGLWindow` - OpenGL 使用现有 `QOpenGLWidget/QOpenGLWindow`
- Vulkan viewer 完整 readback display 路径(offscreen texture → download → QImage → QPainter)已实现 - Vulkan / backend-neutral viewer readback display 路径(offscreen texture → download → QImage → QPainter)已搭建
- 新增 `ManagedDisplayWidgetBackendNeutral`,在普通 `QWidget``paintEvent` 中转发到 `ManagedDisplayWidget::OnPaint` - 新增 `ManagedDisplayWidgetBackendNeutral`,在普通 `QWidget``paintEvent` 中转发到 `ManagedDisplayWidget::OnPaint`
- `ViewerDisplayWidget::OnPaint` 在 backend-neutral 模式下改用 `QPainter` 填充背景,将颜色管理后的画面渲染到 U8 RGBA offscreen texture,再 `Download` 到 CPU buffer,最后用 `QImage::Format_RGBA8888_Premultiplied` + `setDevicePixelRatio` 绘制到 inner widget。 - `ViewerDisplayWidget::OnPaint` 在 backend-neutral 模式下改用 `QPainter` 填充背景,将颜色管理后的画面渲染到 U8 RGBA offscreen texture,再 `Download` 到 CPU buffer,最后用 `QImage::Format_RGBA8888_Premultiplied` + `setDevicePixelRatio` 绘制到 inner widget。
- OpenGL 路径保持原有 `BlitColorManaged` 直接到 widget 不变。 - OpenGL 路径保持原有 `BlitColorManaged` 直接到 widget 不变。
- Viewer 只消费后端 texture handle 或 readback frame,不直接假设 GL texture id。 - Viewer 只消费后端 texture handle 或 readback frame,不直接假设 GL texture id。
- **状态说明**backend-neutral 代码已合并,但 Vulkan viewer 目前受限于 VulkanRenderer 原型状态,尚未在真实设备上验证端到端显示。
## 阶段 5OpenFX 处理边界(已完成 ## 阶段 5OpenFX 处理边界(边界框架已完成,Vulkan 路径待验证
- OpenFX 插件 OpenGL 渲染路径保留 OpenGL 依赖,不强行改写。 - OpenFX 插件 OpenGL 渲染路径保留 OpenGL 依赖,不强行改写。
- `PluginRenderer` 不再继承 `OpenGLRenderer`,改为持有通用的 `Renderer *` - `PluginRenderer` 不再继承 `OpenGLRenderer`,改为持有通用的 `Renderer *`
@@ -126,15 +132,16 @@
- 格式转换(`ConvertFrameIfNeeded``ConvertTextureForParams`)、readback`ReadbackTextureToFrame`)、upload 等辅助函数保持后端无关,通过 `Renderer` 接口调用,无需移入后端库。 - 格式转换(`ConvertFrameIfNeeded``ConvertTextureForParams`)、readback`ReadbackTextureToFrame`)、upload 等辅助函数保持后端无关,通过 `Renderer` 接口调用,无需移入后端库。
- `RenderProcessor::ProcessPluginJob` 不再要求 `render_ctx_` 实现 `OpenGLContextProvider`,任何 `Renderer` 都能驱动插件渲染。 - `RenderProcessor::ProcessPluginJob` 不再要求 `render_ctx_` 实现 `OpenGLContextProvider`,任何 `Renderer` 都能驱动插件渲染。
- 更新相关 gtest`PluginRenderer` 构造函数现在需要传入 renderer 指针,测试传入 `nullptr` 验证纯 CPU 路径。 - 更新相关 gtest`PluginRenderer` 构造函数现在需要传入 renderer 指针,测试传入 `nullptr` 验证纯 CPU 路径。
- **状态说明**:后端无关的边界框架和 OpenGL 动态路径已可编译并通过现有测试;Vulkan 下的 OFX CPU 回退路径代码已就位,但尚未在真实 Vulkan 后端上验证。
## 完成标准 ## 完成标准
- [x] 主程序默认不再直接 new `OpenGLRenderer`,而是通过 `DynamicRenderer` 动态加载 OpenGL/Vulkan 后端;加载失败时保留回退到 `OpenGLRenderer` 的安全路径。 - [x] 主程序默认不再直接 new `OpenGLRenderer`,而是通过 `DynamicRenderer` 动态加载 OpenGL/Vulkan 后端;加载失败时保留回退到 `OpenGLRenderer` 的安全路径。
- [x] `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认 `ON``liboakgl.so` / `liboakvulkan.so` 默认构建并安装。 - [x] `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认 `ON``liboakgl.so` 默认构建并安装;`liboakvulkan.so` 在检测到 Vulkan 开发库时构建并安装。
- [x] OpenGL 后端库可单独构建、加载、初始化、销毁。 - [x] OpenGL 后端库可单独构建、加载、初始化、销毁。
- [x] 用户能在配置中选择 OpenGL/Vulkan。 - [x] 用户能在配置中选择 OpenGL/Vulkan。
- [x] Vulkan 不可用时自动回退到 OpenGL,不崩溃。 - [x] Vulkan 不可用时自动回退到 OpenGL,不崩溃。
- [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积从约 21 MB 降至约 600 KB。 - [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积从约 21 MB 降至约 600 KB。
- [x] Vulkan viewer 完整 readback display 路径已实现offscreen texture → download → QImage → QPainter)。 - [x] Vulkan / backend-neutral viewer readback display 路径已搭建offscreen texture → download → QImage → QPainter,但端到端显示尚未在真实 Vulkan 设备上验证
- [x] OpenFX 插件渲染边界已处理:`PluginRenderer` 后端无关化,非 OpenGL 渲染器自动回退 CPU 路径,动态 OpenGL 后端通过 C ABI 支持 OFX OpenGL 输出绑定。 - [x] OpenFX 插件渲染边界已处理:`PluginRenderer` 后端无关化,非 OpenGL 渲染器自动回退 CPU 路径,动态 OpenGL 后端通过 C ABI 支持 OFX OpenGL 输出绑定。
- [ ] 手工测试计划覆盖 viewer、proxy、scope、导出等完整路径。 - [ ] 手工测试计划覆盖 viewer、proxy、scope、导出等完整路径。
+4 -1
View File
@@ -83,7 +83,10 @@ target_compile_options(
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
target_compile_definitions(olive-gtest PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND) target_compile_definitions(olive-gtest PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
add_dependencies(olive-gtest oakgl oakvulkan) add_dependencies(olive-gtest oakgl)
if (TARGET oakvulkan)
add_dependencies(olive-gtest oakvulkan)
endif()
endif() endif()
if (MSVC) if (MSVC)