From 38c5afb13d1f85daaa8ae215f3bc376ed10d3ae2 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 7 Jun 2026 18:02:34 +0800 Subject: [PATCH] Add Vulkan backend placeholder with dynamic fallback --- app/CMakeLists.txt | 18 +- app/render/backend/dynamicrenderer.cpp | 55 ++++++ app/render/backend/dynamicrenderer.h | 3 + app/render/backend/renderbackend_c.h | 1 + app/render/opengl/openglbackend_c.cpp | 6 + app/render/vulkan/vulkanbackend_c.cpp | 182 ++++++++++++++++++++ docs/zh/render-backend-dynamic-plan.md | 4 + tests/gtest/CMakeLists.txt | 2 +- tests/gtest/dynamic_render_backend_test.cpp | 11 ++ 9 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 app/render/vulkan/vulkanbackend_c.cpp diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 7146c9747..9dc24a912 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -117,6 +117,22 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) + + add_library(oakvulkan SHARED + render/vulkan/vulkanbackend_c.cpp + ) + target_include_directories(oakvulkan PRIVATE + ${CMAKE_SOURCE_DIR}/app + ) + target_link_libraries(oakvulkan PRIVATE Qt${QT_VERSION_MAJOR}::Core) + target_compile_definitions(oakvulkan PRIVATE ${OLIVE_DEFINITIONS}) + target_compile_options(oakvulkan PRIVATE ${OLIVE_COMPILE_OPTIONS}) + 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} + ) endif() add_library(oakgl-cabi-check OBJECT @@ -142,7 +158,7 @@ target_include_directories(olive-editor PUBLIC pluginSupport) target_link_libraries(olive-editor PUBLIC OfxHost) add_dependencies(olive-editor oakgl-cabi-check) if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) - add_dependencies(olive-editor oakgl) + add_dependencies(olive-editor oakgl oakvulkan) endif() # Add render worker process (olive-render-worker). diff --git a/app/render/backend/dynamicrenderer.cpp b/app/render/backend/dynamicrenderer.cpp index a9708ccbd..5b7df3d19 100644 --- a/app/render/backend/dynamicrenderer.cpp +++ b/app/render/backend/dynamicrenderer.cpp @@ -86,11 +86,27 @@ bool DynamicRenderer::Load() } handle_ = create_(this->parent()); + if (!handle_) { + library_.unload(); + return false; + } + if (is_available_ && !is_available_(handle_)) { + qWarning() << "Render backend is not available" << backend_ + << library_.fileName(); + if (backend_ == QStringLiteral("vulkan")) { + return FallbackToOpenGL(); + } + destroy_(handle_); + handle_ = nullptr; + library_.unload(); + return false; + } return handle_ != nullptr; } bool DynamicRenderer::ResolveFunctions() { + ResetFunctions(); #define RESOLVE(member, type, symbol) \ member = reinterpret_cast(library_.resolve(symbol)); \ if (!member) return false @@ -126,9 +142,48 @@ bool DynamicRenderer::ResolveFunctions() RESOLVE(opengl_context_, OakBackendOpenGLContextFn, "oak_renderer_opengl_context"); #undef RESOLVE + is_available_ = reinterpret_cast( + library_.resolve("oak_renderer_is_available")); return true; } +bool DynamicRenderer::FallbackToOpenGL() +{ + if (handle_ && destroy_) { + destroy_(handle_); + handle_ = nullptr; + } + if (library_.isLoaded()) { + library_.unload(); + } + ResetFunctions(); + backend_ = QStringLiteral("opengl"); + return Load(); +} + +void DynamicRenderer::ResetFunctions() +{ + create_ = nullptr; + destroy_ = nullptr; + is_available_ = nullptr; + init_ = nullptr; + init_with_context_ = nullptr; + post_init_ = nullptr; + post_destroy_ = nullptr; + destroy_internal_ = nullptr; + clear_destination_ = nullptr; + create_native_texture_ = nullptr; + destroy_native_texture_ = nullptr; + create_native_shader_ = nullptr; + destroy_native_shader_ = nullptr; + upload_to_texture_ = nullptr; + download_from_texture_ = nullptr; + flush_ = nullptr; + get_pixel_from_texture_ = nullptr; + blit_ = nullptr; + opengl_context_ = nullptr; +} + bool DynamicRenderer::Init() { return Load() && init_(handle_); diff --git a/app/render/backend/dynamicrenderer.h b/app/render/backend/dynamicrenderer.h index 2b07beb68..5d83c31c9 100644 --- a/app/render/backend/dynamicrenderer.h +++ b/app/render/backend/dynamicrenderer.h @@ -52,6 +52,8 @@ protected: private: bool ResolveFunctions(); + bool FallbackToOpenGL(); + void ResetFunctions(); QString LibraryFilename() const; QString backend_; @@ -60,6 +62,7 @@ private: OakBackendCreateFn create_ = nullptr; OakBackendDestroyFn destroy_ = nullptr; + OakBackendIsAvailableFn is_available_ = nullptr; OakBackendInitFn init_ = nullptr; OakBackendInitWithContextFn init_with_context_ = nullptr; OakBackendPostInitFn post_init_ = nullptr; diff --git a/app/render/backend/renderbackend_c.h b/app/render/backend/renderbackend_c.h index 0db5cde3b..31bfc9869 100644 --- a/app/render/backend/renderbackend_c.h +++ b/app/render/backend/renderbackend_c.h @@ -17,6 +17,7 @@ typedef void *OakRenderBackendHandle; typedef OakRenderBackendHandle (*OakBackendCreateFn)(void *parent); typedef void (*OakBackendDestroyFn)(OakRenderBackendHandle handle); +typedef bool (*OakBackendIsAvailableFn)(OakRenderBackendHandle handle); typedef bool (*OakBackendInitFn)(OakRenderBackendHandle handle); typedef void (*OakBackendInitWithContextFn)(OakRenderBackendHandle handle, void *context); diff --git a/app/render/opengl/openglbackend_c.cpp b/app/render/opengl/openglbackend_c.cpp index 174172761..59a61b092 100644 --- a/app/render/opengl/openglbackend_c.cpp +++ b/app/render/opengl/openglbackend_c.cpp @@ -43,6 +43,12 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy(OakRenderBackendHandle handl delete Renderer(handle); } +OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available( + OakRenderBackendHandle handle) +{ + return handle != nullptr; +} + OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle) { return Renderer(handle)->Init(); diff --git a/app/render/vulkan/vulkanbackend_c.cpp b/app/render/vulkan/vulkanbackend_c.cpp new file mode 100644 index 000000000..d1f286452 --- /dev/null +++ b/app/render/vulkan/vulkanbackend_c.cpp @@ -0,0 +1,182 @@ +#include "render/backend/renderbackend_c.h" + +#include +#include + +namespace { + +struct VulkanBackend { + bool warned = false; +}; + +VulkanBackend *Backend(OakRenderBackendHandle handle) +{ + return static_cast(handle); +} + +void WarnUnavailable(OakRenderBackendHandle handle, const char *function) +{ + auto *backend = Backend(handle); + if (!backend || backend->warned) { + return; + } + backend->warned = true; + qWarning() << "Vulkan render backend is a C ABI placeholder; function" + << function << "is not implemented yet"; +} + +} // namespace + +OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent) +{ + Q_UNUSED(parent) + return new VulkanBackend(); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy(OakRenderBackendHandle handle) +{ + delete Backend(handle); +} + +OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available( + OakRenderBackendHandle handle) +{ + Q_UNUSED(handle) + return false; +} + +OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle) +{ + WarnUnavailable(handle, "init"); + return false; +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context( + OakRenderBackendHandle handle, void *context) +{ + Q_UNUSED(context) + WarnUnavailable(handle, "init_with_context"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_init( + OakRenderBackendHandle handle) +{ + Q_UNUSED(handle) +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_destroy( + OakRenderBackendHandle handle) +{ + Q_UNUSED(handle) +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_internal( + OakRenderBackendHandle handle) +{ + Q_UNUSED(handle) +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination( + OakRenderBackendHandle handle, void *texture, double r, double g, double b, + double a) +{ + Q_UNUSED(texture) + Q_UNUSED(r) + Q_UNUSED(g) + Q_UNUSED(b) + Q_UNUSED(a) + WarnUnavailable(handle, "clear_destination"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture( + OakRenderBackendHandle handle, int width, int height, int depth, int format, + int channel_count, const void *data, int linesize, void *out_variant) +{ + Q_UNUSED(width) + Q_UNUSED(height) + Q_UNUSED(depth) + Q_UNUSED(format) + Q_UNUSED(channel_count) + Q_UNUSED(data) + Q_UNUSED(linesize) + static_cast(out_variant)->clear(); + WarnUnavailable(handle, "create_native_texture"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture( + OakRenderBackendHandle handle, const void *variant) +{ + Q_UNUSED(variant) + WarnUnavailable(handle, "destroy_native_texture"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader( + OakRenderBackendHandle handle, const void *shader_code, void *out_variant) +{ + Q_UNUSED(shader_code) + static_cast(out_variant)->clear(); + WarnUnavailable(handle, "create_native_shader"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader( + OakRenderBackendHandle handle, const void *variant) +{ + Q_UNUSED(variant) + WarnUnavailable(handle, "destroy_native_shader"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture( + OakRenderBackendHandle handle, const void *variant, const void *video_params, + const void *data, int linesize) +{ + Q_UNUSED(variant) + Q_UNUSED(video_params) + Q_UNUSED(data) + Q_UNUSED(linesize) + WarnUnavailable(handle, "upload_to_texture"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture( + OakRenderBackendHandle handle, const void *variant, const void *video_params, + void *data, int linesize) +{ + Q_UNUSED(variant) + Q_UNUSED(video_params) + Q_UNUSED(data) + Q_UNUSED(linesize) + WarnUnavailable(handle, "download_from_texture"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle) +{ + Q_UNUSED(handle) +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture( + OakRenderBackendHandle handle, void *texture, const void *point, + void *out_color) +{ + Q_UNUSED(texture) + Q_UNUSED(point) + Q_UNUSED(out_color) + WarnUnavailable(handle, "get_pixel_from_texture"); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( + OakRenderBackendHandle handle, const void *shader, void *job, + void *destination, const void *destination_params, bool clear_destination) +{ + Q_UNUSED(shader) + Q_UNUSED(job) + Q_UNUSED(destination) + Q_UNUSED(destination_params) + Q_UNUSED(clear_destination) + WarnUnavailable(handle, "blit"); +} + +OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context( + OakRenderBackendHandle handle) +{ + Q_UNUSED(handle) + return nullptr; +} diff --git a/docs/zh/render-backend-dynamic-plan.md b/docs/zh/render-backend-dynamic-plan.md index c9d51ff47..4a1d159d6 100644 --- a/docs/zh/render-backend-dynamic-plan.md +++ b/docs/zh/render-backend-dynamic-plan.md @@ -59,6 +59,8 @@ 已新增 `DynamicRenderBackend.LoadsExperimentalOpenGLBackend` smoke test:默认构建跳过,实验构建会实际加载 `liboakgl` 并验证 create/destroy C ABI 路径。 +已新增可选 `oak_renderer_is_available` C ABI:后端库可以在被加载和 create 后报告自身是否可用。OpenGL 后端返回可用;Vulkan 占位后端返回不可用,适配器随后卸载它并回退 OpenGL。这把“后端存在”和“后端可用于渲染”分开,避免后续最小化链接边界时把不可用实现误接入渲染路径。 + 因此动态后端成为默认路径前,仍需要把 OpenGL 后端库的链接边界收敛到最小集合: - 后端库只拥有 OpenGL/Vulkan native 操作和必要的后端私有状态。 @@ -69,6 +71,8 @@ ## 阶段 3:Vulkan 后端 - 新增 Vulkan 后端库。 +- 已新增 `liboakvulkan.so` 实验占位库:内部使用 C++,对外导出与 OpenGL 后端一致的 C ABI;当前 `oak_renderer_is_available` 返回 `false`,请求 Vulkan 时由 `DynamicRenderer` 自动回退 OpenGL。 +- 已新增 `DynamicRenderBackend.FallsBackWhenExperimentalVulkanUnavailable` smoke test:默认构建跳过,实验构建验证 Vulkan 占位后端可加载且不会被误用为可渲染后端。 - 实现 Vulkan instance/device/swapchain 或 offscreen image 管理。 - 实现 texture 创建、上传、下载、shader 编译/缓存、blit、clear、flush。 - 提供 OpenGL 辅助格式转换函数的 Vulkan 替代版本。 diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index 65c9082e6..e0f345854 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -83,7 +83,7 @@ target_compile_options( if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) target_compile_definitions(olive-gtest PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND) - add_dependencies(olive-gtest oakgl) + add_dependencies(olive-gtest oakgl oakvulkan) endif() if (MSVC) diff --git a/tests/gtest/dynamic_render_backend_test.cpp b/tests/gtest/dynamic_render_backend_test.cpp index 7e67a35bd..ff056a32b 100644 --- a/tests/gtest/dynamic_render_backend_test.cpp +++ b/tests/gtest/dynamic_render_backend_test.cpp @@ -12,3 +12,14 @@ TEST(DynamicRenderBackend, LoadsExperimentalOpenGLBackend) EXPECT_EQ(renderer.OpenGLContext(), nullptr); #endif } + +TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable) +{ +#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()); + EXPECT_EQ(renderer.OpenGLContext(), nullptr); +#endif +}