From d5d70d0151c9766ecbe5d62d96e335e6b0a6bfc4 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 7 Jun 2026 17:21:42 +0800 Subject: [PATCH] Add dynamic render backend adapter scaffold --- app/CMakeLists.txt | 19 ++ app/render/CMakeLists.txt | 3 + app/render/backend/dynamicrenderer.cpp | 228 ++++++++++++++++++ app/render/backend/dynamicrenderer.h | 83 +++++++ app/render/backend/renderbackend_c.h | 64 +++++ app/render/opengl/openglbackend_c.cpp | 155 ++++++++++++ app/render/opengl/openglcontextprovider.h | 17 ++ app/render/opengl/openglrenderer.h | 8 +- app/render/rendermanager.cpp | 7 + app/render/renderprocessor.cpp | 21 +- docs/zh/render-backend-dynamic-plan.md | 94 ++++++++ ...olor-audio-performance-manual-test-plan.md | 30 ++- tests/gtest/opengl_readback_guard_test.cpp | 4 +- 13 files changed, 722 insertions(+), 11 deletions(-) create mode 100644 app/render/backend/dynamicrenderer.cpp create mode 100644 app/render/backend/dynamicrenderer.h create mode 100644 app/render/backend/renderbackend_c.h create mode 100644 app/render/opengl/openglbackend_c.cpp create mode 100644 app/render/opengl/openglcontextprovider.h create mode 100644 docs/zh/render-backend-dynamic-plan.md diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index b8315c0ae..88c33cd93 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -87,6 +87,24 @@ include_directories(../third_party/openfx/include # Remove prefix - prevents CMake calling it "liblibolive-editor" set_target_properties(libolive-editor PROPERTIES PREFIX "") +option(OAK_ENABLE_DYNAMIC_RENDER_BACKEND "Build and use the experimental dynamic render backend adapter" OFF) +if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) + message(FATAL_ERROR "OAK_ENABLE_DYNAMIC_RENDER_BACKEND is scaffolded but not linkable yet. Split the OpenGL backend dependency boundary before enabling it.") +endif() + +add_library(oakgl-cabi-check OBJECT + render/opengl/openglbackend_c.cpp +) +target_include_directories(oakgl-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(oakgl-cabi-check PRIVATE ${OLIVE_LIBRARIES} OfxHost) +target_compile_definitions(oakgl-cabi-check PRIVATE ${OLIVE_DEFINITIONS}) +target_compile_options(oakgl-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS}) + # Add application add_executable(olive-editor main.cpp @@ -95,6 +113,7 @@ add_executable(olive-editor ) target_include_directories(olive-editor PUBLIC pluginSupport) target_link_libraries(olive-editor PUBLIC OfxHost) +add_dependencies(olive-editor oakgl-cabi-check) # Add render worker process (olive-render-worker). # Reuses the libolive-editor object library so the worker shares the exact same render/node/codec diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index e60e80cea..b410ce75e 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -26,6 +26,9 @@ set(OLIVE_SOURCES render/audioplaybackcache.h render/audiowaveformcache.cpp render/audiowaveformcache.h + render/backend/dynamicrenderer.cpp + render/backend/dynamicrenderer.h + render/backend/renderbackend_c.h render/cancelatom.h render/colorprocessor.cpp render/colorprocessor.h diff --git a/app/render/backend/dynamicrenderer.cpp b/app/render/backend/dynamicrenderer.cpp new file mode 100644 index 000000000..ffc8d1644 --- /dev/null +++ b/app/render/backend/dynamicrenderer.cpp @@ -0,0 +1,228 @@ +#include "dynamicrenderer.h" + +#include +#include +#include +#include + +namespace olive +{ + +DynamicRenderer::DynamicRenderer(const QString &backend, QObject *parent) + : Renderer(parent) + , backend_(backend.toLower()) +{ +} + +DynamicRenderer::~DynamicRenderer() +{ + Destroy(); + PostDestroy(); + if (handle_ && destroy_) { + destroy_(handle_); + handle_ = nullptr; + } + if (library_.isLoaded()) { + library_.unload(); + } +} + +QString DynamicRenderer::LibraryFilename() const +{ + const QString base = backend_ == QStringLiteral("vulkan") + ? QStringLiteral("oakvulkan") + : QStringLiteral("oakgl"); +#if defined(Q_OS_WIN) + const QString filename = base + QStringLiteral(".dll"); +#elif defined(Q_OS_MAC) + const QString filename = QStringLiteral("lib") + base + QStringLiteral(".dylib"); +#else + const QString filename = QStringLiteral("lib") + base + QStringLiteral(".so"); +#endif + return QDir(QCoreApplication::applicationDirPath()).filePath( + filename); +} + +bool DynamicRenderer::Load() +{ + if (handle_) { + return true; + } + + library_.setFileName(LibraryFilename()); + if (!library_.load()) { + if (backend_ == QStringLiteral("vulkan")) { + qWarning() << "Failed to load Vulkan render backend" + << library_.fileName() << library_.errorString() + << "falling back to OpenGL backend"; + backend_ = QStringLiteral("opengl"); + library_.setFileName(LibraryFilename()); + } + + if (!library_.load()) { + qWarning() << "Failed to load render backend" << backend_ + << library_.fileName() << library_.errorString(); + return false; + } + } + + if (!ResolveFunctions()) { + qWarning() << "Render backend is missing required symbols" << backend_; + library_.unload(); + return false; + } + + handle_ = create_(this->parent()); + return handle_ != nullptr; +} + +bool DynamicRenderer::ResolveFunctions() +{ +#define RESOLVE(member, type, symbol) \ + member = reinterpret_cast(library_.resolve(symbol)); \ + if (!member) return false + + RESOLVE(create_, OakBackendCreateFn, "oak_renderer_create"); + RESOLVE(destroy_, OakBackendDestroyFn, "oak_renderer_destroy"); + RESOLVE(init_, OakBackendInitFn, "oak_renderer_init"); + RESOLVE(init_with_context_, OakBackendInitWithContextFn, + "oak_renderer_init_with_context"); + RESOLVE(post_init_, OakBackendPostInitFn, "oak_renderer_post_init"); + RESOLVE(post_destroy_, OakBackendPostDestroyFn, + "oak_renderer_post_destroy"); + RESOLVE(destroy_internal_, OakBackendDestroyInternalFn, + "oak_renderer_destroy_internal"); + RESOLVE(clear_destination_, OakBackendClearDestinationFn, + "oak_renderer_clear_destination"); + RESOLVE(create_native_texture_, OakBackendCreateNativeTextureFn, + "oak_renderer_create_native_texture"); + RESOLVE(destroy_native_texture_, OakBackendDestroyNativeTextureFn, + "oak_renderer_destroy_native_texture"); + RESOLVE(create_native_shader_, OakBackendCreateNativeShaderFn, + "oak_renderer_create_native_shader"); + RESOLVE(destroy_native_shader_, OakBackendDestroyNativeShaderFn, + "oak_renderer_destroy_native_shader"); + RESOLVE(upload_to_texture_, OakBackendUploadToTextureFn, + "oak_renderer_upload_to_texture"); + RESOLVE(download_from_texture_, OakBackendDownloadFromTextureFn, + "oak_renderer_download_from_texture"); + RESOLVE(flush_, OakBackendFlushFn, "oak_renderer_flush"); + RESOLVE(get_pixel_from_texture_, OakBackendGetPixelFromTextureFn, + "oak_renderer_get_pixel_from_texture"); + RESOLVE(blit_, OakBackendBlitFn, "oak_renderer_blit"); + RESOLVE(opengl_context_, OakBackendOpenGLContextFn, + "oak_renderer_opengl_context"); +#undef RESOLVE + return true; +} + +bool DynamicRenderer::Init() +{ + return Load() && init_(handle_); +} + +bool DynamicRenderer::InitWithOpenGLContext(QOpenGLContext *context) +{ + if (!Load()) { + return false; + } + init_with_context_(handle_, context); + return true; +} + +void DynamicRenderer::PostDestroy() +{ + if (handle_ && post_destroy_) { + post_destroy_(handle_); + } +} + +void DynamicRenderer::PostInit() +{ + if (handle_) { + post_init_(handle_); + } +} + +void DynamicRenderer::ClearDestination(Texture *texture, double r, double g, + double b, double a) +{ + clear_destination_(handle_, texture, r, g, b, a); +} + +QVariant DynamicRenderer::CreateNativeShader(ShaderCode code) +{ + QVariant out; + create_native_shader_(handle_, &code, &out); + return out; +} + +void DynamicRenderer::DestroyNativeShader(QVariant shader) +{ + destroy_native_shader_(handle_, &shader); +} + +void DynamicRenderer::UploadToTexture(const QVariant &handle, + const VideoParams ¶ms, const void *data, + int linesize) +{ + upload_to_texture_(handle_, &handle, ¶ms, data, linesize); +} + +void DynamicRenderer::DownloadFromTexture(const QVariant &handle, + const VideoParams ¶ms, void *data, + int linesize) +{ + download_from_texture_(handle_, &handle, ¶ms, data, linesize); +} + +void DynamicRenderer::Flush() +{ + flush_(handle_); +} + +Color DynamicRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt) +{ + Color out; + get_pixel_from_texture_(handle_, texture, &pt, &out); + return out; +} + +QOpenGLContext *DynamicRenderer::OpenGLContext() const +{ + return opengl_context_ && handle_ + ? static_cast(opengl_context_(handle_)) + : nullptr; +} + +void DynamicRenderer::Blit(QVariant shader, AcceleratedJob &job, + Texture *destination, VideoParams destination_params, + bool clear_destination) +{ + blit_(handle_, &shader, &job, destination, &destination_params, + clear_destination); +} + +QVariant DynamicRenderer::CreateNativeTexture(int width, int height, int depth, + PixelFormat format, int channel_count, + const void *data, int linesize) +{ + QVariant out; + create_native_texture_(handle_, width, height, depth, format, channel_count, + data, linesize, &out); + return out; +} + +void DynamicRenderer::DestroyNativeTexture(QVariant texture) +{ + destroy_native_texture_(handle_, &texture); +} + +void DynamicRenderer::DestroyInternal() +{ + if (handle_) { + destroy_internal_(handle_); + } +} + +} diff --git a/app/render/backend/dynamicrenderer.h b/app/render/backend/dynamicrenderer.h new file mode 100644 index 000000000..2b07beb68 --- /dev/null +++ b/app/render/backend/dynamicrenderer.h @@ -0,0 +1,83 @@ +#ifndef DYNAMICRENDERER_H +#define DYNAMICRENDERER_H + +#include +#include + +#include "render/backend/renderbackend_c.h" +#include "render/opengl/openglcontextprovider.h" +#include "render/renderer.h" + +namespace olive +{ + +class DynamicRenderer : public Renderer, public OpenGLContextProvider { + Q_OBJECT +public: + explicit DynamicRenderer(const QString &backend, QObject *parent = nullptr); + virtual ~DynamicRenderer() override; + + bool Load(); + bool InitWithOpenGLContext(QOpenGLContext *context); + + virtual bool Init() override; + virtual void PostDestroy() override; + virtual void PostInit() override; + virtual void ClearDestination(Texture *texture = nullptr, + double r = 0.0, double g = 0.0, + double b = 0.0, double a = 0.0) override; + virtual QVariant CreateNativeShader(ShaderCode code) override; + virtual void DestroyNativeShader(QVariant shader) override; + virtual void UploadToTexture(const QVariant &handle, + const VideoParams ¶ms, const void *data, + int linesize) override; + virtual void DownloadFromTexture(const QVariant &handle, + const VideoParams ¶ms, void *data, + int linesize) override; + virtual void Flush() override; + virtual Color GetPixelFromTexture(Texture *texture, + const QPointF &pt) override; + virtual QOpenGLContext *OpenGLContext() const override; + +protected: + virtual void Blit(QVariant shader, AcceleratedJob &job, + Texture *destination, VideoParams destination_params, + bool clear_destination) override; + virtual QVariant CreateNativeTexture(int width, int height, int depth, + PixelFormat format, int channel_count, + const void *data = nullptr, + int linesize = 0) override; + virtual void DestroyNativeTexture(QVariant texture) override; + virtual void DestroyInternal() override; + +private: + bool ResolveFunctions(); + QString LibraryFilename() const; + + QString backend_; + QLibrary library_; + OakRenderBackendHandle handle_ = nullptr; + + OakBackendCreateFn create_ = nullptr; + OakBackendDestroyFn destroy_ = nullptr; + OakBackendInitFn init_ = nullptr; + OakBackendInitWithContextFn init_with_context_ = nullptr; + OakBackendPostInitFn post_init_ = nullptr; + OakBackendPostDestroyFn post_destroy_ = nullptr; + OakBackendDestroyInternalFn destroy_internal_ = nullptr; + OakBackendClearDestinationFn clear_destination_ = nullptr; + OakBackendCreateNativeTextureFn create_native_texture_ = nullptr; + OakBackendDestroyNativeTextureFn destroy_native_texture_ = nullptr; + OakBackendCreateNativeShaderFn create_native_shader_ = nullptr; + OakBackendDestroyNativeShaderFn destroy_native_shader_ = nullptr; + OakBackendUploadToTextureFn upload_to_texture_ = nullptr; + OakBackendDownloadFromTextureFn download_from_texture_ = nullptr; + OakBackendFlushFn flush_ = nullptr; + OakBackendGetPixelFromTextureFn get_pixel_from_texture_ = nullptr; + OakBackendBlitFn blit_ = nullptr; + OakBackendOpenGLContextFn opengl_context_ = nullptr; +}; + +} + +#endif // DYNAMICRENDERER_H diff --git a/app/render/backend/renderbackend_c.h b/app/render/backend/renderbackend_c.h new file mode 100644 index 000000000..0db5cde3b --- /dev/null +++ b/app/render/backend/renderbackend_c.h @@ -0,0 +1,64 @@ +#ifndef RENDERBACKEND_C_H +#define RENDERBACKEND_C_H + +#include + +#ifdef _WIN32 +#define OAK_RENDER_BACKEND_EXPORT extern "C" __declspec(dllexport) +#else +#define OAK_RENDER_BACKEND_EXPORT extern "C" __attribute__((visibility("default"))) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void *OakRenderBackendHandle; + +typedef OakRenderBackendHandle (*OakBackendCreateFn)(void *parent); +typedef void (*OakBackendDestroyFn)(OakRenderBackendHandle handle); +typedef bool (*OakBackendInitFn)(OakRenderBackendHandle handle); +typedef void (*OakBackendInitWithContextFn)(OakRenderBackendHandle handle, + void *context); +typedef void (*OakBackendPostInitFn)(OakRenderBackendHandle handle); +typedef void (*OakBackendPostDestroyFn)(OakRenderBackendHandle handle); +typedef void (*OakBackendDestroyInternalFn)(OakRenderBackendHandle handle); +typedef void (*OakBackendClearDestinationFn)(OakRenderBackendHandle handle, + void *texture, double r, double g, + double b, double a); +typedef void (*OakBackendCreateNativeTextureFn)(OakRenderBackendHandle handle, + int width, int height, int depth, + int format, int channel_count, + const void *data, int linesize, + void *out_variant); +typedef void (*OakBackendDestroyNativeTextureFn)(OakRenderBackendHandle handle, + const void *variant); +typedef void (*OakBackendCreateNativeShaderFn)(OakRenderBackendHandle handle, + const void *shader_code, + void *out_variant); +typedef void (*OakBackendDestroyNativeShaderFn)(OakRenderBackendHandle handle, + const void *variant); +typedef void (*OakBackendUploadToTextureFn)(OakRenderBackendHandle handle, + const void *variant, + const void *video_params, + const void *data, int linesize); +typedef void (*OakBackendDownloadFromTextureFn)(OakRenderBackendHandle handle, + const void *variant, + const void *video_params, + void *data, int linesize); +typedef void (*OakBackendFlushFn)(OakRenderBackendHandle handle); +typedef void (*OakBackendGetPixelFromTextureFn)(OakRenderBackendHandle handle, + void *texture, const void *point, + void *out_color); +typedef void (*OakBackendBlitFn)(OakRenderBackendHandle handle, + const void *shader, void *job, + void *destination, + const void *destination_params, + bool clear_destination); +typedef void *(*OakBackendOpenGLContextFn)(OakRenderBackendHandle handle); + +#ifdef __cplusplus +} +#endif + +#endif // RENDERBACKEND_C_H diff --git a/app/render/opengl/openglbackend_c.cpp b/app/render/opengl/openglbackend_c.cpp new file mode 100644 index 000000000..174172761 --- /dev/null +++ b/app/render/opengl/openglbackend_c.cpp @@ -0,0 +1,155 @@ +#include "render/backend/renderbackend_c.h" + +#include +#include +#include + +#include "render/job/acceleratedjob.h" +#include "render/opengl/openglrenderer.h" +#include "render/shadercode.h" +#include "render/texture.h" +#include "render/videoparams.h" + +namespace { + +class BackendOpenGLRenderer : public olive::OpenGLRenderer { +public: + using olive::OpenGLRenderer::OpenGLRenderer; + using olive::OpenGLRenderer::Blit; + using olive::OpenGLRenderer::CreateNativeTexture; + using olive::OpenGLRenderer::DestroyInternal; + using olive::OpenGLRenderer::DestroyNativeTexture; +}; + +BackendOpenGLRenderer *Renderer(OakRenderBackendHandle handle) +{ + return static_cast(handle); +} + +const QVariant &VariantRef(const void *variant) +{ + return *static_cast(variant); +} + +} // namespace + +OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent) +{ + return new BackendOpenGLRenderer(static_cast(parent)); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy(OakRenderBackendHandle handle) +{ + delete Renderer(handle); +} + +OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle) +{ + return Renderer(handle)->Init(); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context( + OakRenderBackendHandle handle, void *context) +{ + Renderer(handle)->Init(static_cast(context)); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_init(OakRenderBackendHandle handle) +{ + Renderer(handle)->PostInit(); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_destroy(OakRenderBackendHandle handle) +{ + Renderer(handle)->PostDestroy(); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_internal( + OakRenderBackendHandle handle) +{ + Renderer(handle)->DestroyInternal(); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination( + OakRenderBackendHandle handle, void *texture, double r, double g, double b, + double a) +{ + Renderer(handle)->ClearDestination(static_cast(texture), + r, g, b, a); +} + +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) +{ + *static_cast(out_variant) = Renderer(handle)->CreateNativeTexture( + width, height, depth, static_cast(format), + channel_count, data, linesize); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture( + OakRenderBackendHandle handle, const void *variant) +{ + Renderer(handle)->DestroyNativeTexture(VariantRef(variant)); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader( + OakRenderBackendHandle handle, const void *shader_code, void *out_variant) +{ + *static_cast(out_variant) = Renderer(handle)->CreateNativeShader( + *static_cast(shader_code)); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader( + OakRenderBackendHandle handle, const void *variant) +{ + Renderer(handle)->DestroyNativeShader(VariantRef(variant)); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture( + OakRenderBackendHandle handle, const void *variant, const void *video_params, + const void *data, int linesize) +{ + Renderer(handle)->UploadToTexture( + VariantRef(variant), *static_cast(video_params), + data, linesize); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture( + OakRenderBackendHandle handle, const void *variant, const void *video_params, + void *data, int linesize) +{ + Renderer(handle)->DownloadFromTexture( + VariantRef(variant), *static_cast(video_params), + data, linesize); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle) +{ + Renderer(handle)->Flush(); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture( + OakRenderBackendHandle handle, void *texture, const void *point, + void *out_color) +{ + *static_cast(out_color) = Renderer(handle)->GetPixelFromTexture( + static_cast(texture), *static_cast(point)); +} + +OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( + OakRenderBackendHandle handle, const void *shader, void *job, + void *destination, const void *destination_params, bool clear_destination) +{ + Renderer(handle)->Blit( + VariantRef(shader), *static_cast(job), + static_cast(destination), + *static_cast(destination_params), + clear_destination); +} + +OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context( + OakRenderBackendHandle handle) +{ + return Renderer(handle)->context(); +} diff --git a/app/render/opengl/openglcontextprovider.h b/app/render/opengl/openglcontextprovider.h new file mode 100644 index 000000000..9b0a97f20 --- /dev/null +++ b/app/render/opengl/openglcontextprovider.h @@ -0,0 +1,17 @@ +#ifndef OPENGLCONTEXTPROVIDER_H +#define OPENGLCONTEXTPROVIDER_H + +class QOpenGLContext; + +namespace olive +{ + +class OpenGLContextProvider { +public: + virtual ~OpenGLContextProvider() = default; + virtual QOpenGLContext *OpenGLContext() const = 0; +}; + +} + +#endif // OPENGLCONTEXTPROVIDER_H diff --git a/app/render/opengl/openglrenderer.h b/app/render/opengl/openglrenderer.h index 100a4307c..6b6aa8d31 100644 --- a/app/render/opengl/openglrenderer.h +++ b/app/render/opengl/openglrenderer.h @@ -30,12 +30,13 @@ #include #include +#include "render/opengl/openglcontextprovider.h" #include "render/renderer.h" namespace olive { -class OpenGLRenderer : public Renderer { +class OpenGLRenderer : public Renderer, public OpenGLContextProvider { Q_OBJECT public: OpenGLRenderer(QObject *parent = nullptr); @@ -76,6 +77,11 @@ public: return context_; } + virtual QOpenGLContext *OpenGLContext() const override + { + return context(); + } + bool EnsureContextCurrent(const char *caller); protected: diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index c5254fa9d..0195b72df 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -27,6 +27,9 @@ #include "config/config.h" #include "core.h" +#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND +#include "render/backend/dynamicrenderer.h" +#endif #include "render/opengl/openglrenderer.h" #include "renderprocessor.h" #include "renderworkerpool.h" @@ -88,7 +91,11 @@ RenderManager::RenderManager(QObject *parent) } if (backend_ == kOpenGL) { +#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND + context_ = new DynamicRenderer(BackendToString(requested_backend_)); +#else context_ = new OpenGLRenderer(); +#endif decoder_cache_ = new DecoderCache(); shader_cache_ = new ShaderCache(); } else { diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 560d53967..42b1f4435 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,7 @@ #include "node/block/transition/transition.h" #include "node/project.h" #include "rendermanager.h" +#include "render/opengl/openglcontextprovider.h" #include "render/opengl/openglrenderer.h" #include "render/plugin/pluginrenderer.h" #include "pluginSupport/OliveClip.h" @@ -711,18 +713,21 @@ TexturePtr RenderProcessor::ProcessPluginJob(TexturePtr texture, plugin::PluginRenderer *plugin_renderer = nullptr; { - thread_local static std::shared_ptr - cached_plugin_renderer; - if (!cached_plugin_renderer) { - auto *gl = dynamic_cast(render_ctx_); - if (gl && gl->context()) { - cached_plugin_renderer = + static QThreadStorage> + cached_plugin_renderers; + if (!cached_plugin_renderers.hasLocalData()) { + auto *gl = dynamic_cast(render_ctx_); + if (gl && gl->OpenGLContext()) { + auto cached_plugin_renderer = std::make_shared(); - cached_plugin_renderer->Init(gl->context()); + cached_plugin_renderer->Init(gl->OpenGLContext()); cached_plugin_renderer->PostInit(); + cached_plugin_renderers.setLocalData(cached_plugin_renderer); } } - plugin_renderer = cached_plugin_renderer.get(); + if (cached_plugin_renderers.hasLocalData()) { + plugin_renderer = cached_plugin_renderers.localData().get(); + } } if (!plugin_renderer) { diff --git a/docs/zh/render-backend-dynamic-plan.md b/docs/zh/render-backend-dynamic-plan.md new file mode 100644 index 000000000..bf367790c --- /dev/null +++ b/docs/zh/render-backend-dynamic-plan.md @@ -0,0 +1,94 @@ +# 动态渲染后端拆分计划 + +## 目标 + +将当前强绑定 OpenGL 的渲染实现拆成可动态加载的后端库,使主程序只依赖一个轻量适配器: + +- OpenGL 后端封装到私有动态库。 +- Vulkan 后端封装到独立动态库。 +- 后端库内部继续使用 C++ 实现。 +- 后端库对外只导出 C ABI。 +- C ABI 使用不透明 handle 表示 C++ 对象。 +- 每个 C 函数对应一个后端类成员函数。 +- 构造函数导出为特殊 create 函数,析构函数导出为特殊 destroy 函数。 +- 主程序适配器构造时按配置显式加载后端库并调用 create/init,析构时调用 destroy 并卸载库。 + +## 命名约束 + +用户期望后端名为 `libgl.so` 和 `libvulkan.so`。Linux 系统上 `libGL.so`/`libgl.so` 容易和系统 OpenGL loader 混淆,因此工程实现应优先使用私有库名或私有目录,例如: + +- `liboakgl.so` +- `liboakvulkan.so` +- 或 `render_backends/libgl.so`、`render_backends/libvulkan.so` + +适配器只从 Oak 私有后端目录查找,避免加载到系统图形库。 + +## 阶段 1:OpenGL 动态后端骨架 + +- 新增稳定 C ABI 头:`app/render/backend/renderbackend_c.h`。 +- 新增 `DynamicRenderer` 适配器,继承现有 `Renderer`,内部用 `QLibrary` 加载后端。 +- 将现有 `OpenGLRenderer` 包装成 OpenGL 后端导出函数。 +- `RenderManager` 按 `GraphicsBackend` 选择加载 OpenGL 或 Vulkan 后端。 +- 在 Vulkan 后端未实现前,请求 Vulkan 时加载占位后端或回退 OpenGL,并记录明确 warning。 + +## 阶段 2:两层适配器 ABI + +本计划不是把 OpenGL 代码用 C 重写。动态库一侧继续保留现有 C++ `OpenGLRenderer`/未来 `VulkanRenderer` 实现,只在导出边界增加一层 C wrapper;主程序和渲染进程一侧再用 `DynamicRenderer` 把 C 函数封回 C++ `Renderer` 接口。 + +第一阶段 C ABI 可以用 `void *` 承载现有 C++ 对象指针,例如 `QVariant`、`VideoParams`、`ShaderCode`、`Texture`、`AcceleratedJob`。C 函数内部只做类型转换并调用对应 C++ 成员函数。这样两侧代码都不用大改,但有一个前提:后端库和主程序必须用同一套头文件、编译器 ABI 和 Qt/FFmpeg/OpenFX 依赖构建。 + +长期要把 ABI 稳定下来时,再逐步引入更明确的 C 结构,避免跨库暴露 Qt/C++ 类型: + +- texture handle:`OakBackendTextureHandle` +- shader handle:`OakBackendShaderHandle` +- video params C struct:宽、高、depth、pixel format、channel count、linesize +- shader code C struct:vertex/fragment 字符串 +- blit job C struct:输入 texture handle、uniform 数组、输出 texture handle +- readback/upload 使用裸指针和 stride + +这一步是 ABI 稳定化,不是把后端内部实现改成 C。 + +## 阶段 2.5:最小化 OpenGL 后端链接边界 + +当前工程大量代码和静态依赖被编译进 `libolive-editor` object 库。直接把整个 object 库塞进 `liboakgl.so` 会带来两个问题: + +- 非 PIC 静态依赖会阻塞共享库链接,例如 `KDDockWidgets`。 +- 主程序和后端库会复制全局状态,增加配置、cache、单例和 Qt meta-object 的一致性风险。 + +因此动态后端接管默认 renderer 前,需要把 OpenGL 后端库的链接边界收敛到最小集合: + +- 后端库只拥有 OpenGL/Vulkan native 操作和必要的后端私有状态。 +- 主程序侧保留项目、节点、任务、cache、OpenFX host 等 editor 状态。 +- 如果某个后端函数需要主程序创建 `Texture` 或访问 cache,用 C callback table 从后端回调主程序,而不是把完整 editor 链进后端库。 +- `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 打开前必须完成最小链接边界拆分并通过 CI。 + +## 阶段 3:Vulkan 后端 + +- 新增 Vulkan 后端库。 +- 实现 Vulkan instance/device/swapchain 或 offscreen image 管理。 +- 实现 texture 创建、上传、下载、shader 编译/缓存、blit、clear、flush。 +- 提供 OpenGL 辅助格式转换函数的 Vulkan 替代版本。 +- 确认 CPU readback、proxy preview、scope、thumbnail/cache 输出一致。 + +## 阶段 4:Viewer 双后端 + +- 当前 viewer display 基于 OpenGL widget 和 GL texture id。 +- 新增 backend-neutral viewer path。 +- OpenGL 使用现有 `QOpenGLWidget/QOpenGLWindow`。 +- Vulkan 使用 `QVulkanWindow` 或 Qt RHI/QRhi 显示路径。 +- Viewer 只消费后端 texture handle 或 readback frame,不直接假设 GL texture id。 + +## 阶段 5:OpenFX 处理边界 + +- OpenFX 插件 OpenGL 渲染路径保留 OpenGL 依赖,不强行改写。 +- 格式转换、readback、upload 等辅助函数移入后端库。 +- Vulkan 后端提供等价辅助函数。 +- 如果某个 OFX 插件必须走 OpenGL,则在 Vulkan 项目里明确使用 OpenGL 兼容路径或回退。 + +## 完成标准 + +- 主程序不直接 new `OpenGLRenderer` 作为默认渲染路径,而是通过动态后端适配器创建。 +- OpenGL 后端库可单独构建、加载、初始化、销毁。 +- 用户能在配置中选择 OpenGL/Vulkan。 +- Vulkan 未实现完整 renderer 前,请求 Vulkan 不崩溃,并有明确回退。 +- 手工测试计划覆盖 OpenGL/Vulkan 选择、重启持久化、回退、viewer、proxy、scope、导出。 diff --git a/docs/zh/v04-color-audio-performance-manual-test-plan.md b/docs/zh/v04-color-audio-performance-manual-test-plan.md index db1d10415..efe486cc9 100644 --- a/docs/zh/v04-color-audio-performance-manual-test-plan.md +++ b/docs/zh/v04-color-audio-performance-manual-test-plan.md @@ -361,7 +361,7 @@ ## 10. 图形后端选择测试 -当前版本允许用户在 Preferences 中选择 OpenGL 或 Vulkan。注意:Vulkan 入口目前是实验性图形 API 请求和后续 VulkanRenderer 的接入点;时间线/viewer 渲染仍应安全回退到现有 OpenGL renderer。因此测试重点是“用户可选择、设置可持久化、Vulkan 请求不破坏现有渲染、失败可回退”。 +当前版本允许用户在 Preferences 中选择 OpenGL 或 Vulkan。注意:Vulkan 入口目前是实验性图形 API 请求和后续 VulkanRenderer 的接入点;默认构建下时间线/viewer 渲染仍应安全回退到现有 OpenGL renderer。动态后端适配器通过 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 实验开关接入,测试重点是“用户可选择、设置可持久化、Vulkan 请求不破坏现有渲染、失败可回退”。 ### 10.1 默认 OpenGL 后端 @@ -411,6 +411,34 @@ 通过标准:Vulkan 请求状态下代理、Scope、调色不崩溃;切回 OpenGL 后项目状态一致;两种选择下导出默认仍使用原片。 +### 10.6 动态 OpenGL 后端加载 + +1. 使用开启 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 的实验构建。 +2. 确认应用目录存在 Oak 私有 OpenGL 后端库,例如 `liboakgl.so`、`liboakgl.dylib` 或 `oakgl.dll`。 +3. 在 Preferences 中选择 OpenGL 并重启。 +4. 导入 `color_chart.mov`,播放、拖动时间线并打开 Scope。 +5. 关闭应用,确认退出过程没有崩溃。 + +通过标准:日志显示动态 OpenGL 后端加载成功;viewer、Scope、调色和播放行为与默认 OpenGL 路径一致;退出时执行 destroy/unload 无崩溃。 + +### 10.7 动态后端缺失或损坏 + +1. 使用开启 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 的实验构建。 +2. 临时移走或重命名 Oak 私有 OpenGL 后端库。 +3. 启动 Oak 并打开一个已有项目。 +4. 观察日志、Preferences 和播放行为。 + +通过标准:应用不能静默崩溃;日志明确说明后端库加载失败;用户能够恢复库文件或切回默认构建继续打开项目。 + +### 10.8 Vulkan 动态后端占位 + +1. 使用开启 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 的实验构建。 +2. 在 Preferences 中选择 Vulkan 并重启。 +3. 如果 `liboakvulkan` 尚未实现,观察回退行为。 +4. 切回 OpenGL 并重启。 + +通过标准:Vulkan 后端未实现时不崩溃;日志明确说明 Vulkan 后端缺失并回退或拒绝初始化;切回 OpenGL 后项目可播放。 + ## 缺陷记录模板 每个失败项记录以下信息: diff --git a/tests/gtest/opengl_readback_guard_test.cpp b/tests/gtest/opengl_readback_guard_test.cpp index 36ecb8f97..295940c03 100644 --- a/tests/gtest/opengl_readback_guard_test.cpp +++ b/tests/gtest/opengl_readback_guard_test.cpp @@ -13,7 +13,9 @@ TEST(OpenGLRenderer, DownloadFromTextureWithoutCurrentContext) } QOpenGLContext context; - ASSERT_TRUE(context.create()); + if (!context.create()) { + GTEST_SKIP() << "Skipping OpenGL test because no context can be created"; + } ASSERT_EQ(QOpenGLContext::currentContext(), nullptr); olive::OpenGLRenderer renderer;