feat(render): dynamic OpenGL/Vulkan backend split and backend-neutral viewer

- Extract libolive-rendercore static library to minimize backend link boundary.
- Add DynamicRenderer adapter with C ABI (oakgl/oakvulkan shared libs).
- Make OAK_ENABLE_DYNAMIC_RENDER_BACKEND default ON with OpenGL fallback.
- Implement VulkanRenderer prototype (textures, shaders, UBO blit, readback).
- Add backend-neutral viewer readback path (offscreen -> QImage -> QPainter).
- Refactor PluginRenderer to be renderer-agnostic; OFX plugins fall back to CPU
  path on non-OpenGL backends while preserving OpenGL render path.
- Add Renderer::AttachOutputTexture/DetachOutputTexture and C ABI forwards.
- Update docs/zh/render-backend-dynamic-plan.md for Phase 3/4/5.
This commit is contained in:
2026-07-13 10:19:29 +08:00
parent 5fc8232671
commit 225f8505c2
42 changed files with 3042 additions and 495 deletions
+4 -2
View File
@@ -38,6 +38,8 @@ jobs:
mingw-w64-ucrt-x86_64-fmt mingw-w64-ucrt-x86_64-fmt
mingw-w64-ucrt-x86_64-expat mingw-w64-ucrt-x86_64-expat
mingw-w64-ucrt-x86_64-portaudio mingw-w64-ucrt-x86_64-portaudio
mingw-w64-ucrt-x86_64-vulkan-headers
mingw-w64-ucrt-x86_64-vulkan-loader
mingw-w64-ucrt-x86_64-nsis mingw-w64-ucrt-x86_64-nsis
mingw-w64-ucrt-x86_64-ntldd mingw-w64-ucrt-x86_64-ntldd
@@ -91,7 +93,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
brew update brew update
brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat dylibbundler brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat molten-vk vulkan-headers dylibbundler
- name: Build OpenTimelineIO - name: Build OpenTimelineIO
run: | run: |
@@ -261,7 +263,7 @@ jobs:
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \ libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \ libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev \ portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers libxkbcommon-dev \
wget fuse3 desktop-file-utils wget fuse3 desktop-file-utils
- name: Build - name: Build
+46 -2
View File
@@ -35,7 +35,7 @@ jobs:
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \ libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \ libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers vulkan-validationlayers-dev libxkbcommon-dev
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# macOS dependencies # macOS dependencies
@@ -44,7 +44,7 @@ jobs:
if: runner.os == 'macOS' if: runner.os == 'macOS'
run: | run: |
brew update brew update
brew install ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat brew install ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat molten-vk vulkan-headers
echo "$(brew --prefix qt@6)/bin" >> "$GITHUB_PATH" echo "$(brew --prefix qt@6)/bin" >> "$GITHUB_PATH"
echo "CMAKE_PREFIX_PATH=$(brew --prefix qt@6)" >> "$GITHUB_ENV" echo "CMAKE_PREFIX_PATH=$(brew --prefix qt@6)" >> "$GITHUB_ENV"
@@ -84,6 +84,8 @@ jobs:
mingw-w64-ucrt-x86_64-fmt mingw-w64-ucrt-x86_64-fmt
mingw-w64-ucrt-x86_64-expat mingw-w64-ucrt-x86_64-expat
mingw-w64-ucrt-x86_64-portaudio mingw-w64-ucrt-x86_64-portaudio
mingw-w64-ucrt-x86_64-vulkan-headers
mingw-w64-ucrt-x86_64-vulkan-loader
mingw-w64-ucrt-x86_64-make mingw-w64-ucrt-x86_64-make
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -226,3 +228,45 @@ jobs:
if: runner.os == 'Windows' if: runner.os == 'Windows'
shell: msys2 {0} shell: msys2 {0}
run: ./build/tests/gtest/olive-gtest --gtest_filter="ViewerSmoke*" run: ./build/tests/gtest/olive-gtest --gtest_filter="ViewerSmoke*"
# ------------------------------------------------------------------
# Experimental dynamic render backend (Linux only)
# ------------------------------------------------------------------
build-test-dynamic-backend:
runs-on: warp-ubuntu-latest-x64-16x
env:
CMAKE_BUILD_TYPE: Release
QT_QPA_PLATFORM: offscreen
steps:
- uses: actions/checkout@v4
with:
submodules: 'true'
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
ninja-build pkg-config nasm \
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers vulkan-validationlayers-dev libxkbcommon-dev
- name: Configure
run: |
cmake -S . -B build -G Ninja \
-DBUILD_TESTS=ON \
-DBUILD_QT6=ON \
-DOAK_ENABLE_DYNAMIC_RENDER_BACKEND=ON \
-DOCIO_LOCATION=/usr \
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
- name: Build
run: cmake --build build --config ${{ env.CMAKE_BUILD_TYPE }}
- name: Test
run: ctest --test-dir build --output-on-failure -C ${{ env.CMAKE_BUILD_TYPE }}
- name: Dynamic Render Backend Smoke Tests
run: ./build/tests/gtest/olive-gtest --gtest_filter="DynamicRenderBackend*"
+12
View File
@@ -104,6 +104,18 @@ endif()
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
list(APPEND OLIVE_LIBRARIES OpenGL::GL) list(APPEND OLIVE_LIBRARIES OpenGL::GL)
# Optional: Link Vulkan and shaderc (for the Vulkan render backend)
find_package(Vulkan)
find_package(PkgConfig)
if(PkgConfig_FOUND)
pkg_check_modules(SHADERC shaderc)
endif()
if(Vulkan_FOUND)
message(STATUS "Vulkan found: ${Vulkan_LIBRARIES}")
else()
message(STATUS " Vulkan not found. The Vulkan render backend will be disabled.")
endif()
# Link OpenColorIO # Link OpenColorIO
find_package(OpenColorIO 2.1.1 REQUIRED) find_package(OpenColorIO 2.1.1 REQUIRED)
list(APPEND OLIVE_LIBRARIES ${OCIO_LIBRARIES}) list(APPEND OLIVE_LIBRARIES ${OCIO_LIBRARIES})
+86 -15
View File
@@ -87,7 +87,7 @@ include_directories(../third_party/openfx/include
# Remove prefix - prevents CMake calling it "liblibolive-editor" # Remove prefix - prevents CMake calling it "liblibolive-editor"
set_target_properties(libolive-editor PROPERTIES PREFIX "") set_target_properties(libolive-editor PROPERTIES PREFIX "")
option(OAK_ENABLE_DYNAMIC_RENDER_BACKEND "Build and use the experimental dynamic render backend adapter" OFF) option(OAK_ENABLE_DYNAMIC_RENDER_BACKEND "Build and use the dynamic render backend adapter" ON)
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
set_target_properties(libolive-editor PROPERTIES POSITION_INDEPENDENT_CODE ON) set_target_properties(libolive-editor PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_compile_definitions(libolive-editor PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND) target_compile_definitions(libolive-editor PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
@@ -98,41 +98,94 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
endif() endif()
endforeach() endforeach()
add_library(oakgl SHARED # Render core library: the minimal set of code required by the OpenGL/Vulkan backend
render/opengl/openglbackend_c.cpp # libraries. Keeping this separate from libolive-editor prevents the backends from
$<TARGET_OBJECTS:libolive-editor> # dragging in editor-wide state (project, task, cache, UI, etc.).
add_library(libolive-rendercore STATIC
common/avframeptr.h
common/define.h
common/filefunctions.cpp
common/filefunctions.h
common/qtutils.cpp
common/qtutils.h
config/config.cpp
config/config.h
node/param.cpp
node/param.h
node/splitvalue.h
node/value.cpp
node/value.h
node/valuedatabase.cpp
node/valuedatabase.h
render/backend/dynamicrenderer.cpp
render/backend/dynamicrenderer.h
render/backend/renderbackend_c.h
render/job/acceleratedjob.cpp
render/job/acceleratedjob.h
render/job/shaderjob.h
render/renderer.cpp
render/renderer.h
render/shadercode.h
render/texture.cpp
render/texture.h
render/videoparams.cpp
render/videoparams.h
) )
target_include_directories(oakgl PRIVATE set_target_properties(libolive-rendercore PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(libolive-rendercore PUBLIC
${CMAKE_SOURCE_DIR}/app ${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/third_party/openfx/include ${CMAKE_SOURCE_DIR}/third_party/openfx/include
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include ${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
${OLIVE_INCLUDE_DIRS} ${OLIVE_INCLUDE_DIRS}
) )
target_link_libraries(oakgl PRIVATE ${OLIVE_LIBRARIES} OfxHost) target_link_libraries(libolive-rendercore PUBLIC ${OLIVE_LIBRARIES} OfxHost)
target_compile_definitions(oakgl PRIVATE ${OLIVE_DEFINITIONS}) target_compile_definitions(libolive-rendercore PUBLIC ${OLIVE_DEFINITIONS})
target_compile_options(oakgl PRIVATE ${OLIVE_COMPILE_OPTIONS}) target_compile_options(libolive-rendercore PUBLIC ${OLIVE_COMPILE_OPTIONS})
add_library(oakgl SHARED
render/opengl/openglbackend_c.cpp
render/opengl/openglrenderer.cpp
render/opengl/openglrenderer.h
)
target_link_libraries(oakgl PRIVATE libolive-rendercore)
set_target_properties(oakgl PROPERTIES set_target_properties(oakgl PROPERTIES
OUTPUT_NAME oakgl OUTPUT_NAME oakgl
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
) )
install(TARGETS oakgl
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
add_library(oakvulkan SHARED add_library(oakvulkan SHARED
render/vulkan/vulkanbackend_c.cpp render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
) )
target_include_directories(oakvulkan PRIVATE target_link_libraries(oakvulkan PRIVATE libolive-rendercore)
${CMAKE_SOURCE_DIR}/app if(Vulkan_FOUND)
) target_link_libraries(oakvulkan PRIVATE Vulkan::Vulkan)
target_link_libraries(oakvulkan PRIVATE Qt${QT_VERSION_MAJOR}::Core) target_compile_definitions(oakvulkan PRIVATE OAK_HAS_VULKAN)
target_compile_definitions(oakvulkan PRIVATE ${OLIVE_DEFINITIONS}) endif()
target_compile_options(oakvulkan PRIVATE ${OLIVE_COMPILE_OPTIONS}) 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 set_target_properties(oakvulkan PROPERTIES
OUTPUT_NAME oakvulkan OUTPUT_NAME oakvulkan
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
ARCHIVE_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
@@ -150,11 +203,25 @@ target_compile_options(oakgl-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS})
add_library(oakvulkan-cabi-check OBJECT add_library(oakvulkan-cabi-check OBJECT
render/vulkan/vulkanbackend_c.cpp render/vulkan/vulkanbackend_c.cpp
render/vulkan/vulkanrenderer.cpp
render/vulkan/vulkanrenderer.h
) )
target_include_directories(oakvulkan-cabi-check PRIVATE target_include_directories(oakvulkan-cabi-check PRIVATE
${CMAKE_SOURCE_DIR}/app ${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) 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)
target_link_libraries(oakvulkan-cabi-check PRIVATE Vulkan::Vulkan)
target_compile_definitions(oakvulkan-cabi-check PRIVATE OAK_HAS_VULKAN)
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_definitions(oakvulkan-cabi-check PRIVATE ${OLIVE_DEFINITIONS})
target_compile_options(oakvulkan-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS}) target_compile_options(oakvulkan-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS})
@@ -183,6 +250,10 @@ add_executable(olive-render-worker
) )
target_include_directories(olive-render-worker PUBLIC pluginSupport) 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)
target_compile_definitions(olive-render-worker PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
add_dependencies(olive-render-worker oakgl oakvulkan)
endif()
# Create docs if doxygen was found # Create docs if doxygen was found
if(DOXYGEN_FOUND) if(DOXYGEN_FOUND)
set(DOXYGEN_PROJECT_NAME "Oak Video Editor") set(DOXYGEN_PROJECT_NAME "Oak Video Editor")
+48
View File
@@ -0,0 +1,48 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef AVFRAMEPTR_H
#define AVFRAMEPTR_H
extern "C" {
#include <libavutil/frame.h>
}
#include <memory>
namespace olive
{
using AVFramePtr = std::shared_ptr<AVFrame>;
inline AVFramePtr CreateAVFramePtr(AVFrame *f)
{
return std::shared_ptr<AVFrame>(f, [](AVFrame *g) { av_frame_free(&g); });
}
inline AVFramePtr CreateAVFramePtr()
{
return CreateAVFramePtr(av_frame_alloc());
}
}
#endif // AVFRAMEPTR_H
+1 -10
View File
@@ -30,6 +30,7 @@ extern "C" {
#include <olive/core/core.h> #include <olive/core/core.h>
#include "common/avframeptr.h"
#include "render/videoparams.h" #include "render/videoparams.h"
namespace olive namespace olive
@@ -85,16 +86,6 @@ public:
static AVPixelFormat ConvertJPEGSpaceToRegularSpace(AVPixelFormat f); static AVPixelFormat ConvertJPEGSpaceToRegularSpace(AVPixelFormat f);
}; };
using AVFramePtr = std::shared_ptr<AVFrame>;
inline AVFramePtr CreateAVFramePtr(AVFrame *f)
{
return std::shared_ptr<AVFrame>(f, [](AVFrame *g) { av_frame_free(&g); });
}
inline AVFramePtr CreateAVFramePtr()
{
return CreateAVFramePtr(av_frame_alloc());
}
} }
#endif // FFMPEGABSTRACTION_H #endif // FFMPEGABSTRACTION_H
+2
View File
@@ -30,6 +30,7 @@ set(OLIVE_SOURCES
render/backend/dynamicrenderer.h render/backend/dynamicrenderer.h
render/backend/renderbackend_c.h render/backend/renderbackend_c.h
render/cancelatom.h render/cancelatom.h
render/colormanagement.cpp
render/colorprocessor.cpp render/colorprocessor.cpp
render/colorprocessor.h render/colorprocessor.h
render/colorprocessorcache.h render/colorprocessorcache.h
@@ -39,6 +40,7 @@ set(OLIVE_SOURCES
render/framehashcache.h render/framehashcache.h
render/framemanager.cpp render/framemanager.cpp
render/framemanager.h render/framemanager.h
render/interlacetexture.cpp
render/loopmode.h render/loopmode.h
render/managedcolor.cpp render/managedcolor.cpp
render/managedcolor.h render/managedcolor.h
+28
View File
@@ -45,6 +45,8 @@ QString DynamicRenderer::LibraryFilename() const
const QStringList candidates = { const QStringList candidates = {
app_dir.filePath(filename), app_dir.filePath(filename),
app_dir.filePath(QDir(QStringLiteral("render_backends")).filePath(filename)), app_dir.filePath(QDir(QStringLiteral("render_backends")).filePath(filename)),
app_dir.filePath(QDir(QStringLiteral("../lib")).filePath(filename)),
app_dir.filePath(QDir(QStringLiteral("../../lib")).filePath(filename)),
app_dir.filePath(QDir(QStringLiteral("../app")).filePath(filename)), app_dir.filePath(QDir(QStringLiteral("../app")).filePath(filename)),
app_dir.filePath(QDir(QStringLiteral("../../app")).filePath(filename)) app_dir.filePath(QDir(QStringLiteral("../../app")).filePath(filename))
}; };
@@ -139,6 +141,10 @@ bool DynamicRenderer::ResolveFunctions()
RESOLVE(get_pixel_from_texture_, OakBackendGetPixelFromTextureFn, RESOLVE(get_pixel_from_texture_, OakBackendGetPixelFromTextureFn,
"oak_renderer_get_pixel_from_texture"); "oak_renderer_get_pixel_from_texture");
RESOLVE(blit_, OakBackendBlitFn, "oak_renderer_blit"); RESOLVE(blit_, OakBackendBlitFn, "oak_renderer_blit");
RESOLVE(attach_output_texture_, OakBackendAttachOutputTextureFn,
"oak_renderer_attach_output_texture");
RESOLVE(detach_output_texture_, OakBackendDetachOutputTextureFn,
"oak_renderer_detach_output_texture");
RESOLVE(opengl_context_, OakBackendOpenGLContextFn, RESOLVE(opengl_context_, OakBackendOpenGLContextFn,
"oak_renderer_opengl_context"); "oak_renderer_opengl_context");
#undef RESOLVE #undef RESOLVE
@@ -184,6 +190,8 @@ void DynamicRenderer::ResetFunctions()
flush_ = nullptr; flush_ = nullptr;
get_pixel_from_texture_ = nullptr; get_pixel_from_texture_ = nullptr;
blit_ = nullptr; blit_ = nullptr;
attach_output_texture_ = nullptr;
detach_output_texture_ = nullptr;
opengl_context_ = nullptr; opengl_context_ = nullptr;
} }
@@ -271,6 +279,11 @@ QOpenGLContext *DynamicRenderer::OpenGLContext() const
: nullptr; : nullptr;
} }
bool DynamicRenderer::IsOpenGL() const
{
return backend_ == QStringLiteral("opengl");
}
void DynamicRenderer::Blit(QVariant shader, AcceleratedJob &job, void DynamicRenderer::Blit(QVariant shader, AcceleratedJob &job,
Texture *destination, VideoParams destination_params, Texture *destination, VideoParams destination_params,
bool clear_destination) bool clear_destination)
@@ -301,4 +314,19 @@ void DynamicRenderer::DestroyInternal()
} }
} }
void DynamicRenderer::AttachOutputTexture(Texture *texture)
{
if (attach_output_texture_ && texture) {
QVariant id = texture->id();
attach_output_texture_(handle_, &id);
}
}
void DynamicRenderer::DetachOutputTexture()
{
if (detach_output_texture_) {
detach_output_texture_(handle_);
}
}
} }
+8
View File
@@ -44,6 +44,12 @@ public:
const QPointF &pt) override; const QPointF &pt) override;
virtual QOpenGLContext *OpenGLContext() const override; virtual QOpenGLContext *OpenGLContext() const override;
virtual bool IsOpenGL() const override;
virtual void AttachOutputTexture(Texture *texture) override;
virtual void DetachOutputTexture() override;
protected: protected:
virtual void Blit(QVariant shader, AcceleratedJob &job, virtual void Blit(QVariant shader, AcceleratedJob &job,
Texture *destination, VideoParams destination_params, Texture *destination, VideoParams destination_params,
@@ -84,6 +90,8 @@ private:
OakBackendFlushFn flush_ = nullptr; OakBackendFlushFn flush_ = nullptr;
OakBackendGetPixelFromTextureFn get_pixel_from_texture_ = nullptr; OakBackendGetPixelFromTextureFn get_pixel_from_texture_ = nullptr;
OakBackendBlitFn blit_ = nullptr; OakBackendBlitFn blit_ = nullptr;
OakBackendAttachOutputTextureFn attach_output_texture_ = nullptr;
OakBackendDetachOutputTextureFn detach_output_texture_ = nullptr;
OakBackendOpenGLContextFn opengl_context_ = nullptr; OakBackendOpenGLContextFn opengl_context_ = nullptr;
}; };
+6 -1
View File
@@ -27,7 +27,9 @@ enum OakRenderBackendCapability {
OAK_RENDER_BACKEND_CAP_SHADERS = 1ULL << 1, OAK_RENDER_BACKEND_CAP_SHADERS = 1ULL << 1,
OAK_RENDER_BACKEND_CAP_BLIT = 1ULL << 2, OAK_RENDER_BACKEND_CAP_BLIT = 1ULL << 2,
OAK_RENDER_BACKEND_CAP_READBACK = 1ULL << 3, OAK_RENDER_BACKEND_CAP_READBACK = 1ULL << 3,
OAK_RENDER_BACKEND_CAP_VIEWER_CONTEXT = 1ULL << 4 OAK_RENDER_BACKEND_CAP_VIEWER_CONTEXT = 1ULL << 4,
OAK_RENDER_BACKEND_CAP_INSTANCE = 1ULL << 5,
OAK_RENDER_BACKEND_CAP_DEVICE = 1ULL << 6
}; };
struct OakRenderBackendInfo { struct OakRenderBackendInfo {
@@ -81,6 +83,9 @@ typedef void (*OakBackendBlitFn)(OakRenderBackendHandle handle,
void *destination, void *destination,
const void *destination_params, const void *destination_params,
bool clear_destination); bool clear_destination);
typedef void (*OakBackendAttachOutputTextureFn)(OakRenderBackendHandle handle,
const void *texture_id);
typedef void (*OakBackendDetachOutputTextureFn)(OakRenderBackendHandle handle);
typedef void *(*OakBackendOpenGLContextFn)(OakRenderBackendHandle handle); typedef void *(*OakBackendOpenGLContextFn)(OakRenderBackendHandle handle);
#ifdef __cplusplus #ifdef __cplusplus
+225
View File
@@ -0,0 +1,225 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "renderer.h"
#include "common/filefunctions.h"
#include "node/node.h"
#include "render/colorprocessor.h"
#include "render/job/colortransformjob.h"
#include "render/job/shaderjob.h"
namespace olive
{
bool Renderer::GetColorContext(const ColorTransformJob &color_job,
Renderer::ColorContext *ctx)
{
QMutexLocker locker(&color_cache_mutex_);
ColorContext &color_ctx = *ctx;
QString proc_id = color_job.id();
if (color_cache_.contains(proc_id)) {
color_ctx = color_cache_.value(proc_id);
return true;
} else {
// Create shader description
QString ocio_func_name;
if (color_job.GetFunctionName().isEmpty()) {
ocio_func_name = "OCIODisplay";
} else {
ocio_func_name = color_job.GetFunctionName();
}
auto shader_desc = OCIO::GpuShaderDesc::CreateShaderDesc();
shader_desc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_ES_3_0);
shader_desc->setFunctionName(ocio_func_name.toUtf8());
shader_desc->setResourcePrefix("ocio_");
// Generate shader
color_job.GetColorProcessor()
->GetProcessor()
->getDefaultGPUProcessor()
->extractGpuShaderInfo(shader_desc);
ShaderCode code;
if (const Node *shader_src = color_job.CustomShaderSource()) {
// Use shader code from associated node
code = shader_src->GetShaderCode(
{ color_job.CustomShaderID(), shader_desc->getShaderText() });
} else {
// Generate shader code using OCIO stub and our auto-generated name
code = FileFunctions::ReadFileAsString(
QStringLiteral(":/shaders/colormanage.frag"));
code.set_frag_code(
code.frag_code().arg(shader_desc->getShaderText()));
}
// Try to compile shader
color_ctx.compiled_shader = CreateNativeShader(code);
if (color_ctx.compiled_shader.isNull()) {
return false;
}
color_ctx.lut3d_textures.resize(shader_desc->getNum3DTextures());
for (unsigned int i = 0; i < shader_desc->getNum3DTextures(); i++) {
const char *tex_name = nullptr;
const char *sampler_name = nullptr;
unsigned int edge_len = 0;
OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR;
shader_desc->get3DTexture(i, tex_name, sampler_name, edge_len,
interpolation);
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
!edge_len) {
qCritical() << "3D LUT texture data is corrupted";
return false;
}
const float *values = nullptr;
shader_desc->get3DTextureValues(i, values);
if (!values) {
qCritical() << "3D LUT texture values are missing";
return false;
}
// Allocate 3D LUT
color_ctx.lut3d_textures[i].texture = CreateTexture(
VideoParams(edge_len, edge_len, edge_len, PixelFormat::F32,
VideoParams::kRGBChannelCount),
values);
color_ctx.lut3d_textures[i].name = sampler_name;
color_ctx.lut3d_textures[i].interpolation =
(interpolation == OCIO::INTERP_NEAREST) ? Texture::kNearest :
Texture::kLinear;
}
color_ctx.lut1d_textures.resize(shader_desc->getNumTextures());
for (unsigned int i = 0; i < shader_desc->getNumTextures(); i++) {
const char *tex_name = nullptr;
const char *sampler_name = nullptr;
unsigned int width = 0, height = 0;
OCIO::GpuShaderDesc::TextureType channel =
OCIO::GpuShaderDesc::TEXTURE_RGB_CHANNEL;
OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR;
#if OCIO_VERSION_MAJOR > 2 || (OCIO_VERSION_MAJOR == 2 && OCIO_VERSION_MINOR >= 3)
OCIO::GpuShaderDesc::TextureDimensions dimensions =
OCIO::GpuShaderDesc::TEXTURE_2D;
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
channel, dimensions, interpolation);
#else
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
channel, interpolation);
#endif
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
!width) {
qCritical() << "1D LUT texture data is corrupted";
return false;
}
const float *values = nullptr;
shader_desc->getTextureValues(i, values);
if (!values) {
qCritical() << "1D LUT texture values are missing";
return false;
}
// Allocate 1D LUT
color_ctx.lut1d_textures[i].texture = CreateTexture(
VideoParams(width, height, PixelFormat::F32,
(channel ==
OCIO::GpuShaderDesc::TEXTURE_RED_CHANNEL) ?
1 :
VideoParams::kRGBChannelCount),
values);
color_ctx.lut1d_textures[i].name = sampler_name;
color_ctx.lut1d_textures[i].interpolation =
(interpolation == OCIO::INTERP_NEAREST) ? Texture::kNearest :
Texture::kLinear;
}
color_cache_.insert(proc_id, color_ctx);
return true;
}
}
void Renderer::BlitColorManaged(const ColorTransformJob &color_job,
Texture *destination, const VideoParams &params)
{
ColorContext color_ctx;
if (!GetColorContext(color_job, &color_ctx)) {
ShaderJob fallback_job;
fallback_job.Insert(QStringLiteral("ove_maintex"),
color_job.GetInputTexture());
fallback_job.Insert(
QStringLiteral("ove_mvpmat"),
NodeValue(NodeValue::kMatrix, color_job.GetTransformMatrix()));
if (destination) {
BlitToTexture(GetDefaultShader(), fallback_job, destination,
color_job.IsClearDestinationEnabled());
} else {
Blit(GetDefaultShader(), fallback_job, params,
color_job.IsClearDestinationEnabled());
}
return;
}
ShaderJob job;
job.Insert(QStringLiteral("ove_maintex"), color_job.GetInputTexture());
job.Insert(QStringLiteral("ove_mvpmat"),
NodeValue(NodeValue::kMatrix, color_job.GetTransformMatrix()));
job.Insert(QStringLiteral("ove_cropmatrix"),
NodeValue(NodeValue::kMatrix,
color_job.GetCropMatrix().inverted()));
job.Insert(QStringLiteral("ove_maintex_alpha"),
NodeValue(NodeValue::kInt,
int(color_job.GetInputAlphaAssociation())));
job.Insert(QStringLiteral("ove_force_opaque"),
NodeValue(NodeValue::kBoolean, color_job.GetForceOpaque()));
job.Insert(color_job.GetValues());
foreach (const ColorContext::LUT &l, color_ctx.lut3d_textures) {
job.Insert(l.name, NodeValue(NodeValue::kTexture,
QVariant::fromValue(l.texture)));
job.SetInterpolation(l.name, l.interpolation);
}
foreach (const ColorContext::LUT &l, color_ctx.lut1d_textures) {
job.Insert(l.name, NodeValue(NodeValue::kTexture,
QVariant::fromValue(l.texture)));
job.SetInterpolation(l.name, l.interpolation);
}
if (destination) {
BlitToTexture(color_ctx.compiled_shader, job, destination,
color_job.IsClearDestinationEnabled());
} else {
Blit(color_ctx.compiled_shader, job, params,
color_job.IsClearDestinationEnabled());
}
}
}
+59
View File
@@ -0,0 +1,59 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include "renderer.h"
#include "common/filefunctions.h"
#include "node/value.h"
#include "render/job/shaderjob.h"
namespace olive
{
TexturePtr Renderer::InterlaceTexture(TexturePtr top, TexturePtr bottom,
const VideoParams &params)
{
color_cache_mutex_.lock();
if (interlace_texture_.isNull()) {
interlace_texture_ =
CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(
QStringLiteral(":/shaders/interlace.frag"))));
}
color_cache_mutex_.unlock();
ShaderJob job;
job.Insert(QStringLiteral("top_tex_in"),
NodeValue(NodeValue::kTexture, QVariant::fromValue(top)));
job.Insert(QStringLiteral("bottom_tex_in"),
NodeValue(NodeValue::kTexture, QVariant::fromValue(bottom)));
job.Insert(QStringLiteral("resolution_in"),
NodeValue(NodeValue::kVec2,
QVector2D(params.effective_width(),
params.effective_height())));
TexturePtr output = CreateTexture(params);
BlitToTexture(interlace_texture_, job, output.get());
return output;
}
}
+14
View File
@@ -19,6 +19,8 @@ public:
using olive::OpenGLRenderer::CreateNativeTexture; using olive::OpenGLRenderer::CreateNativeTexture;
using olive::OpenGLRenderer::DestroyInternal; using olive::OpenGLRenderer::DestroyInternal;
using olive::OpenGLRenderer::DestroyNativeTexture; using olive::OpenGLRenderer::DestroyNativeTexture;
using olive::OpenGLRenderer::AttachTextureAsDestination;
using olive::OpenGLRenderer::DetachTextureAsDestination;
}; };
BackendOpenGLRenderer *Renderer(OakRenderBackendHandle handle) BackendOpenGLRenderer *Renderer(OakRenderBackendHandle handle)
@@ -176,3 +178,15 @@ OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context(
{ {
return Renderer(handle)->context(); return Renderer(handle)->context();
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_attach_output_texture(
OakRenderBackendHandle handle, const void *texture_id)
{
Renderer(handle)->AttachTextureAsDestination(VariantRef(texture_id));
}
OAK_RENDER_BACKEND_EXPORT void oak_renderer_detach_output_texture(
OakRenderBackendHandle handle)
{
Renderer(handle)->DetachTextureAsDestination();
}
+14
View File
@@ -27,7 +27,9 @@
#include <QOpenGLExtraFunctions> #include <QOpenGLExtraFunctions>
#include <QRegularExpression> #include <QRegularExpression>
#include "common/filefunctions.h"
#include "config/config.h" #include "config/config.h"
#include "render/job/shaderjob.h"
namespace olive namespace olive
{ {
@@ -406,6 +408,18 @@ void OpenGLRenderer::Flush()
} }
} }
void OpenGLRenderer::AttachOutputTexture(olive::Texture *texture)
{
if (texture) {
AttachTextureAsDestination(texture->id());
}
}
void OpenGLRenderer::DetachOutputTexture()
{
DetachTextureAsDestination();
}
Color OpenGLRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt) Color OpenGLRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt)
{ {
AttachTextureAsDestination(texture->id()); AttachTextureAsDestination(texture->id());
+9
View File
@@ -82,6 +82,15 @@ public:
return context(); return context();
} }
virtual bool IsOpenGL() const override
{
return true;
}
virtual void AttachOutputTexture(olive::Texture *texture) override;
virtual void DetachOutputTexture() override;
bool EnsureContextCurrent(const char *caller); bool EnsureContextCurrent(const char *caller);
protected: protected:
+9 -7
View File
@@ -1432,9 +1432,10 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
auto *olive_instance = auto *olive_instance =
dynamic_cast<olive::plugin::OlivePluginInstance *>(instance); dynamic_cast<olive::plugin::OlivePluginInstance *>(instance);
const bool use_opengl = const bool use_opengl =
supports_opengl && destination && destination->renderer() && supports_opengl && renderer_ && renderer_->IsOpenGL() && destination &&
destination->id().isValid(); destination->renderer() == renderer_ && destination->id().isValid();
if (olive_instance) { if (olive_instance) {
olive_instance->setOpenGLEnabled(use_opengl);
olive_instance->setVideoParam(destination_params); olive_instance->setVideoParam(destination_params);
// Ensure all clip instances inherit the project's params so that // Ensure all clip instances inherit the project's params so that
// getAspectRatio/getFrameRate etc. return valid values before // getAspectRatio/getFrameRate etc. return valid values before
@@ -1798,7 +1799,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
renderScale, true, interactive); renderScale, true, interactive);
return; return;
} }
AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params, this); AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params, renderer_);
const AVPixelFormat expected_fmt = const AVPixelFormat expected_fmt =
GetDestinationAVPixelFormat(destination_params); GetDestinationAVPixelFormat(destination_params);
destination->handleFrame(converted); destination->handleFrame(converted);
@@ -1837,15 +1838,16 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
// Purpose: Attach output texture for OFX GL rendering. // Purpose: Attach output texture for OFX GL rendering.
void olive::plugin::PluginRenderer::AttachOutputTexture(olive::TexturePtr texture) void olive::plugin::PluginRenderer::AttachOutputTexture(olive::TexturePtr texture)
{ {
if (!texture) { if (renderer_) {
return; renderer_->AttachOutputTexture(texture.get());
} }
AttachTextureAsDestination(texture->id());
} }
// 作用:解除 OFX 的 GL 输出绑定。 // 作用:解除 OFX 的 GL 输出绑定。
// Purpose: Detach OFX GL output binding. // Purpose: Detach OFX GL output binding.
void olive::plugin::PluginRenderer::DetachOutputTexture() void olive::plugin::PluginRenderer::DetachOutputTexture()
{ {
DetachTextureAsDestination(); if (renderer_) {
renderer_->DetachOutputTexture();
}
} }
+19 -11
View File
@@ -23,17 +23,12 @@
#ifndef PLUGINRENDERER_H #ifndef PLUGINRENDERER_H
#define PLUGINRENDERER_H #define PLUGINRENDERER_H
#include <QOpenGLExtraFunctions>
#include <QOpenGLBuffer> #include <QObject>
#include <QOpenGLFunctions>
#include <QOpenGLShader>
#include <QOpenGLVertexArrayObject>
#include <QThread>
#include <QOffscreenSurface>
#include "render/renderer.h" #include "render/renderer.h"
#include "render/job/pluginjob.h" #include "render/job/pluginjob.h"
#include "render/opengl/openglrenderer.h"
namespace olive namespace olive
{ {
namespace plugin{ namespace plugin{
@@ -44,11 +39,22 @@ int BytesToPixels(int byte_linesize, const olive::VideoParams &params);
} }
// 作用:OFX 插件渲染器,负责 CPU/GL 路径下的插件调用和纹理桥接。 // 作用:OFX 插件渲染器,负责 CPU/GL 路径下的插件调用和纹理桥接。
// Purpose: OFX plugin renderer that drives CPU/GL render paths and texture bridging. // Purpose: OFX plugin renderer that drives CPU/GL render paths and texture bridging.
class PluginRenderer : public olive::OpenGLRenderer{ //
// 不再继承 OpenGLRenderer,而是持有一个通用的 Renderer 指针。这样当主渲染器
// 是 Vulkan 或动态加载的后端时,插件仍可通过 CPU readback/upload 路径工作;
// 仅当底层渲染器真正支持 OpenGL 时才走 OFX OpenGL 渲染路径。
class PluginRenderer : public QObject {
Q_OBJECT Q_OBJECT
public: public:
PluginRenderer(QObject *parent=nullptr):OpenGLRenderer(parent){}; explicit PluginRenderer(olive::Renderer *renderer, QObject *parent = nullptr)
virtual ~PluginRenderer() override{}; : QObject(parent), renderer_(renderer) {}
virtual ~PluginRenderer() override {}
olive::Renderer *renderer() const
{
return renderer_;
}
// 作用:将目标纹理绑定为插件输出。 // 作用:将目标纹理绑定为插件输出。
// Purpose: Attach destination texture as OFX output. // Purpose: Attach destination texture as OFX output.
void AttachOutputTexture(olive::TexturePtr texture); void AttachOutputTexture(olive::TexturePtr texture);
@@ -62,6 +68,8 @@ public:
olive::VideoParams destination_params, olive::VideoParams destination_params,
bool clear_destination, bool interactive); bool clear_destination, bool interactive);
private:
olive::Renderer *renderer_;
}; };
} }
} }
+1 -221
View File
@@ -116,34 +116,6 @@ void Renderer::DestroyTexture(Texture *texture)
} }
} }
TexturePtr Renderer::InterlaceTexture(TexturePtr top, TexturePtr bottom,
const VideoParams &params)
{
color_cache_mutex_.lock();
if (interlace_texture_.isNull()) {
interlace_texture_ =
CreateNativeShader(ShaderCode(FileFunctions::ReadFileAsString(
QStringLiteral(":/shaders/interlace.frag"))));
}
color_cache_mutex_.unlock();
ShaderJob job;
job.Insert(QStringLiteral("top_tex_in"),
NodeValue(NodeValue::kTexture, QVariant::fromValue(top)));
job.Insert(QStringLiteral("bottom_tex_in"),
NodeValue(NodeValue::kTexture, QVariant::fromValue(bottom)));
job.Insert(QStringLiteral("resolution_in"),
NodeValue(NodeValue::kVec2,
QVector2D(params.effective_width(),
params.effective_height())));
TexturePtr output = CreateTexture(params);
BlitToTexture(interlace_texture_, job, output.get());
return output;
}
QVariant Renderer::GetDefaultShader() QVariant Renderer::GetDefaultShader()
{ {
QMutexLocker locker(&color_cache_mutex_); QMutexLocker locker(&color_cache_mutex_);
@@ -191,142 +163,6 @@ TexturePtr Renderer::CreateTextureFromNativeHandle(const QVariant &v,
return std::make_shared<Texture>(this, v, params, lifetime_); return std::make_shared<Texture>(this, v, params, lifetime_);
} }
bool Renderer::GetColorContext(const ColorTransformJob &color_job,
Renderer::ColorContext *ctx)
{
QMutexLocker locker(&color_cache_mutex_);
ColorContext &color_ctx = *ctx;
QString proc_id = color_job.id();
if (color_cache_.contains(proc_id)) {
color_ctx = color_cache_.value(proc_id);
return true;
} else {
// Create shader description
QString ocio_func_name;
if (color_job.GetFunctionName().isEmpty()) {
ocio_func_name = "OCIODisplay";
} else {
ocio_func_name = color_job.GetFunctionName();
}
auto shader_desc = OCIO::GpuShaderDesc::CreateShaderDesc();
shader_desc->setLanguage(OCIO::GPU_LANGUAGE_GLSL_ES_3_0);
shader_desc->setFunctionName(ocio_func_name.toUtf8());
shader_desc->setResourcePrefix("ocio_");
// Generate shader
color_job.GetColorProcessor()
->GetProcessor()
->getDefaultGPUProcessor()
->extractGpuShaderInfo(shader_desc);
ShaderCode code;
if (const Node *shader_src = color_job.CustomShaderSource()) {
// Use shader code from associated node
code = shader_src->GetShaderCode(
{ color_job.CustomShaderID(), shader_desc->getShaderText() });
} else {
// Generate shader code using OCIO stub and our auto-generated name
code = FileFunctions::ReadFileAsString(
QStringLiteral(":shaders/colormanage.frag"));
code.set_frag_code(
code.frag_code().arg(shader_desc->getShaderText()));
}
// Try to compile shader
color_ctx.compiled_shader = CreateNativeShader(code);
if (color_ctx.compiled_shader.isNull()) {
return false;
}
color_ctx.lut3d_textures.resize(shader_desc->getNum3DTextures());
for (unsigned int i = 0; i < shader_desc->getNum3DTextures(); i++) {
const char *tex_name = nullptr;
const char *sampler_name = nullptr;
unsigned int edge_len = 0;
OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR;
shader_desc->get3DTexture(i, tex_name, sampler_name, edge_len,
interpolation);
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
!edge_len) {
qCritical() << "3D LUT texture data is corrupted";
return false;
}
const float *values = nullptr;
shader_desc->get3DTextureValues(i, values);
if (!values) {
qCritical() << "3D LUT texture values are missing";
return false;
}
// Allocate 3D LUT
color_ctx.lut3d_textures[i].texture = CreateTexture(
VideoParams(edge_len, edge_len, edge_len, PixelFormat::F32,
VideoParams::kRGBChannelCount),
values);
color_ctx.lut3d_textures[i].name = sampler_name;
color_ctx.lut3d_textures[i].interpolation =
(interpolation == OCIO::INTERP_NEAREST) ? Texture::kNearest :
Texture::kLinear;
}
color_ctx.lut1d_textures.resize(shader_desc->getNumTextures());
for (unsigned int i = 0; i < shader_desc->getNumTextures(); i++) {
const char *tex_name = nullptr;
const char *sampler_name = nullptr;
unsigned int width = 0, height = 0;
OCIO::GpuShaderDesc::TextureType channel =
OCIO::GpuShaderDesc::TEXTURE_RGB_CHANNEL;
OCIO::Interpolation interpolation = OCIO::INTERP_LINEAR;
#if OCIO_VERSION_MAJOR > 2 || (OCIO_VERSION_MAJOR == 2 && OCIO_VERSION_MINOR >= 3)
OCIO::GpuShaderDesc::TextureDimensions dimensions =
OCIO::GpuShaderDesc::TEXTURE_2D;
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
channel, dimensions, interpolation);
#else
shader_desc->getTexture(i, tex_name, sampler_name, width, height,
channel, interpolation);
#endif
if (!tex_name || !*tex_name || !sampler_name || !*sampler_name ||
!width) {
qCritical() << "1D LUT texture data is corrupted";
return false;
}
const float *values = nullptr;
shader_desc->getTextureValues(i, values);
if (!values) {
qCritical() << "1D LUT texture values are missing";
return false;
}
// Allocate 1D LUT
color_ctx.lut1d_textures[i].texture = CreateTexture(
VideoParams(width, height, PixelFormat::F32,
(channel ==
OCIO::GpuShaderDesc::TEXTURE_RED_CHANNEL) ?
1 :
VideoParams::kRGBChannelCount),
values);
color_ctx.lut1d_textures[i].name = sampler_name;
color_ctx.lut1d_textures[i].interpolation =
(interpolation == OCIO::INTERP_NEAREST) ? Texture::kNearest :
Texture::kLinear;
}
color_cache_.insert(proc_id, color_ctx);
return true;
}
}
void Renderer::ClearOldTextures() void Renderer::ClearOldTextures()
{ {
QMutexLocker locker(&texture_cache_lock_); QMutexLocker locker(&texture_cache_lock_);
@@ -342,60 +178,4 @@ void Renderer::ClearOldTextures()
} }
} }
void Renderer::BlitColorManaged(const ColorTransformJob &color_job, } // namespace olive
Texture *destination, const VideoParams &params)
{
ColorContext color_ctx;
if (!GetColorContext(color_job, &color_ctx)) {
ShaderJob fallback_job;
fallback_job.Insert(QStringLiteral("ove_maintex"),
color_job.GetInputTexture());
fallback_job.Insert(
QStringLiteral("ove_mvpmat"),
NodeValue(NodeValue::kMatrix, color_job.GetTransformMatrix()));
if (destination) {
BlitToTexture(GetDefaultShader(), fallback_job, destination,
color_job.IsClearDestinationEnabled());
} else {
Blit(GetDefaultShader(), fallback_job, params,
color_job.IsClearDestinationEnabled());
}
return;
}
ShaderJob job;
job.Insert(QStringLiteral("ove_maintex"), color_job.GetInputTexture());
job.Insert(QStringLiteral("ove_mvpmat"),
NodeValue(NodeValue::kMatrix, color_job.GetTransformMatrix()));
job.Insert(QStringLiteral("ove_cropmatrix"),
NodeValue(NodeValue::kMatrix,
color_job.GetCropMatrix().inverted()));
job.Insert(QStringLiteral("ove_maintex_alpha"),
NodeValue(NodeValue::kInt,
int(color_job.GetInputAlphaAssociation())));
job.Insert(QStringLiteral("ove_force_opaque"),
NodeValue(NodeValue::kBoolean, color_job.GetForceOpaque()));
job.Insert(color_job.GetValues());
foreach (const ColorContext::LUT &l, color_ctx.lut3d_textures) {
job.Insert(l.name, NodeValue(NodeValue::kTexture,
QVariant::fromValue(l.texture)));
job.SetInterpolation(l.name, l.interpolation);
}
foreach (const ColorContext::LUT &l, color_ctx.lut1d_textures) {
job.Insert(l.name, NodeValue(NodeValue::kTexture,
QVariant::fromValue(l.texture)));
job.SetInterpolation(l.name, l.interpolation);
}
if (destination) {
BlitToTexture(color_ctx.compiled_shader, job, destination,
color_job.IsClearDestinationEnabled());
} else {
Blit(color_ctx.compiled_shader, job, params,
color_job.IsClearDestinationEnabled());
}
}
}
+31 -4
View File
@@ -29,12 +29,15 @@
#include <memory> #include <memory>
#include "common/define.h" #include "common/define.h"
#include "node/node.h" #include "render/shadercode.h"
#include "render/colorprocessor.h"
#include "render/job/colortransformjob.h"
#include "render/videoparams.h" #include "render/videoparams.h"
#include "texture.h" #include "texture.h"
#include "job/pluginjob.h"
// Forward declarations to keep the render core header lightweight
namespace olive {
class ColorTransformJob;
class Node;
}
namespace olive namespace olive
{ {
@@ -117,6 +120,30 @@ public:
return lifetime_; return lifetime_;
} }
virtual bool IsOpenGL() const
{
return false;
}
/**
* @brief Attach a texture as the current output destination for OFX plugin
* OpenGL rendering.
*
* Default implementation is a no-op. OpenGL-based renderers override this
* to bind the texture as a framebuffer render target.
*/
virtual void AttachOutputTexture(olive::Texture *texture)
{
(void)texture;
}
/**
* @brief Detach the current OFX plugin OpenGL output texture.
*
* Default implementation is a no-op.
*/
virtual void DetachOutputTexture() {}
protected: protected:
virtual void Blit(QVariant shader, olive::AcceleratedJob& job, virtual void Blit(QVariant shader, olive::AcceleratedJob& job,
olive::Texture *destination, olive::Texture *destination,
+18 -8
View File
@@ -78,21 +78,31 @@ QString RenderManager::BackendToString(Backend backend)
} }
RenderManager::RenderManager(QObject *parent) RenderManager::RenderManager(QObject *parent)
: backend_(kOpenGL) : backend_(BackendFromString(
, requested_backend_(BackendFromString(
OLIVE_CONFIG("GraphicsBackend").toString())) OLIVE_CONFIG("GraphicsBackend").toString()))
, requested_backend_(backend_)
, aggressive_gc_(0) , aggressive_gc_(0)
, worker_pool_(nullptr) , worker_pool_(nullptr)
{ {
if (requested_backend_ == kVulkan) { if (backend_ == kVulkan) {
qWarning() #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
<< "Vulkan graphics backend was requested, but the current render " qWarning() << "Vulkan backend requested but dynamic render backend is not enabled. Falling back to OpenGL.";
"pipeline still uses OpenGL. Falling back to OpenGL renderer."; backend_ = kOpenGL;
#endif
} }
if (backend_ == kOpenGL) { if (backend_ == kOpenGL || backend_ == kVulkan) {
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND #ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
context_ = new DynamicRenderer(BackendToString(requested_backend_)); auto *dynamic_renderer = new DynamicRenderer(BackendToString(requested_backend_));
if (!dynamic_renderer->Load()) {
qWarning() << "Failed to load dynamic render backend" << BackendToString(requested_backend_)
<< ", falling back to OpenGL";
delete dynamic_renderer;
backend_ = kOpenGL;
context_ = new OpenGLRenderer();
} else {
context_ = dynamic_renderer;
}
#else #else
context_ = new OpenGLRenderer(); context_ = new OpenGLRenderer();
#endif #endif
+3 -23
View File
@@ -33,8 +33,6 @@
#include "node/block/transition/transition.h" #include "node/block/transition/transition.h"
#include "node/project.h" #include "node/project.h"
#include "rendermanager.h" #include "rendermanager.h"
#include "render/opengl/openglcontextprovider.h"
#include "render/opengl/openglrenderer.h"
#include "render/plugin/pluginrenderer.h" #include "render/plugin/pluginrenderer.h"
#include "pluginSupport/OliveClip.h" #include "pluginSupport/OliveClip.h"
#include "pluginSupport/OliveHost.h" #include "pluginSupport/OliveHost.h"
@@ -711,26 +709,8 @@ TexturePtr RenderProcessor::ProcessPluginJob(TexturePtr texture,
return destination; return destination;
} }
plugin::PluginRenderer *plugin_renderer = nullptr; plugin::PluginRenderer plugin_renderer(render_ctx_);
{ if (!plugin_renderer.renderer()) {
static QThreadStorage<std::shared_ptr<plugin::PluginRenderer>>
cached_plugin_renderers;
if (!cached_plugin_renderers.hasLocalData()) {
auto *gl = dynamic_cast<OpenGLContextProvider *>(render_ctx_);
if (gl && gl->OpenGLContext()) {
auto cached_plugin_renderer =
std::make_shared<plugin::PluginRenderer>();
cached_plugin_renderer->Init(gl->OpenGLContext());
cached_plugin_renderer->PostInit();
cached_plugin_renderers.setLocalData(cached_plugin_renderer);
}
}
if (cached_plugin_renderers.hasLocalData()) {
plugin_renderer = cached_plugin_renderers.localData().get();
}
}
if (!plugin_renderer) {
return destination; return destination;
} }
@@ -782,7 +762,7 @@ TexturePtr RenderProcessor::ProcessPluginJob(TexturePtr texture,
} }
} }
plugin_renderer->RenderPlugin( plugin_renderer.RenderPlugin(
src, src,
*plugin_job, *plugin_job,
destination, destination,
+1
View File
@@ -21,6 +21,7 @@
#include "texture.h" #include "texture.h"
#include "render/job/acceleratedjob.h"
#include "renderer.h" #include "renderer.h"
namespace olive namespace olive
+1 -1
View File
@@ -22,7 +22,7 @@
#ifndef RENDERTEXTURE_H #ifndef RENDERTEXTURE_H
#define RENDERTEXTURE_H #define RENDERTEXTURE_H
#include "common/ffmpegutils.h" #include "common/avframeptr.h"
#include <atomic> #include <atomic>
#include <memory> #include <memory>
+12
View File
@@ -29,6 +29,7 @@ extern "C" {
#include <QtMath> #include <QtMath>
#include "core.h" #include "core.h"
#include "ofxImageEffect.h"
namespace olive namespace olive
{ {
@@ -120,6 +121,17 @@ VideoParams::VideoParams(int width, int height, int depth, PixelFormat format,
set_defaults_for_footage(); set_defaults_for_footage();
} }
void VideoParams::set_channel_count(const std::string &ofxComponent)
{
if (ofxComponent == kOfxImageComponentAlpha) {
channel_count_ = 1;
} else if (ofxComponent == kOfxImageComponentRGB) {
channel_count_ = kRGBChannelCount;
} else if (ofxComponent == kOfxImageComponentRGBA) {
channel_count_ = kRGBAChannelCount;
}
}
VideoParams::VideoParams(int width, int height, const rational &time_base, VideoParams::VideoParams(int width, int height, const rational &time_base,
PixelFormat format, int nb_channels, PixelFormat format, int nb_channels,
const rational &pixel_aspect_ratio, const rational &pixel_aspect_ratio,
+1 -13
View File
@@ -22,7 +22,6 @@
#ifndef VIDEOPARAMS_H #ifndef VIDEOPARAMS_H
#define VIDEOPARAMS_H #define VIDEOPARAMS_H
#include "ofxImageEffect.h"
#include <olive/core/core.h> #include <olive/core/core.h>
#include <QVector2D> #include <QVector2D>
#include <QXmlStreamReader> #include <QXmlStreamReader>
@@ -178,18 +177,7 @@ public:
{ {
channel_count_ = c; channel_count_ = c;
} }
void set_channel_count(std::string ofxComponent) void set_channel_count(const std::string &ofxComponent);
{
if (ofxComponent == kOfxImageComponentAlpha){
channel_count_ = 1;
}
else if (ofxComponent == kOfxImageComponentRGB){
channel_count_ = kRGBChannelCount;
}
else if(ofxComponent == kOfxImageComponentRGBA){
channel_count_ = kRGBAChannelCount;
}
}
const rational &pixel_aspect_ratio() const const rational &pixel_aspect_ratio() const
{ {
return pixel_aspect_ratio_; return pixel_aspect_ratio_;
+77 -68
View File
@@ -1,41 +1,47 @@
#include "render/backend/renderbackend_c.h" #include "render/backend/renderbackend_c.h"
#include <QDebug> #include <QDebug>
#include <QOpenGLContext>
#include <QPointF>
#include <QVariant> #include <QVariant>
#include "render/job/acceleratedjob.h"
#include "render/shadercode.h"
#include "render/texture.h"
#include "render/videoparams.h"
#include "render/vulkan/vulkanrenderer.h"
namespace { namespace {
struct VulkanBackend { class BackendVulkanRenderer : public olive::VulkanRenderer {
bool warned = false; public:
using olive::VulkanRenderer::VulkanRenderer;
using olive::VulkanRenderer::Blit;
using olive::VulkanRenderer::CreateNativeTexture;
using olive::VulkanRenderer::DestroyInternal;
using olive::VulkanRenderer::DestroyNativeTexture;
}; };
VulkanBackend *Backend(OakRenderBackendHandle handle) BackendVulkanRenderer *Renderer(OakRenderBackendHandle handle)
{ {
return static_cast<VulkanBackend *>(handle); return static_cast<BackendVulkanRenderer *>(handle);
} }
void WarnUnavailable(OakRenderBackendHandle handle, const char *function) const QVariant &VariantRef(const void *variant)
{ {
auto *backend = Backend(handle); return *static_cast<const QVariant *>(variant);
if (!backend || backend->warned) {
return;
}
backend->warned = true;
qWarning() << "Vulkan render backend is a C ABI placeholder; function"
<< function << "is not implemented yet";
} }
} // namespace } // namespace
OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent) OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent)
{ {
Q_UNUSED(parent) return new BackendVulkanRenderer(static_cast<QObject *>(parent));
return new VulkanBackend();
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy(OakRenderBackendHandle handle) OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy(OakRenderBackendHandle handle)
{ {
delete Backend(handle); delete Renderer(handle);
} }
OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info( OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info(
@@ -46,146 +52,134 @@ OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info(
} }
out_info->abi_version = 1; out_info->abi_version = 1;
out_info->kind = OAK_RENDER_BACKEND_VULKAN; out_info->kind = OAK_RENDER_BACKEND_VULKAN;
out_info->capabilities = 0; out_info->capabilities = OAK_RENDER_BACKEND_CAP_TEXTURES |
OAK_RENDER_BACKEND_CAP_SHADERS | OAK_RENDER_BACKEND_CAP_BLIT |
OAK_RENDER_BACKEND_CAP_READBACK;
out_info->name = "vulkan"; out_info->name = "vulkan";
out_info->status = "placeholder-unavailable"; out_info->status = Renderer(handle)->IsAvailable() ? "available" : "unavailable";
return true; return true;
} }
OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available( OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available(
OakRenderBackendHandle handle) OakRenderBackendHandle handle)
{ {
Q_UNUSED(handle) auto *r = Renderer(handle);
return false; if (!r || r->IsAvailable()) {
return r && r->IsAvailable();
}
// Try to initialize if not already available
if (r->Init()) {
r->PostInit();
}
return r->IsAvailable();
} }
OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle) OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle)
{ {
WarnUnavailable(handle, "init"); return Renderer(handle)->Init();
return false;
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context( OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context(
OakRenderBackendHandle handle, void *context) OakRenderBackendHandle handle, void *context)
{ {
Q_UNUSED(context) Q_UNUSED(context)
WarnUnavailable(handle, "init_with_context"); Renderer(handle)->Init();
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_init( OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_init(
OakRenderBackendHandle handle) OakRenderBackendHandle handle)
{ {
Q_UNUSED(handle) Renderer(handle)->PostInit();
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_destroy( OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_destroy(
OakRenderBackendHandle handle) OakRenderBackendHandle handle)
{ {
Q_UNUSED(handle) Renderer(handle)->PostDestroy();
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_internal( OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_internal(
OakRenderBackendHandle handle) OakRenderBackendHandle handle)
{ {
Q_UNUSED(handle) Renderer(handle)->DestroyInternal();
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination( OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination(
OakRenderBackendHandle handle, void *texture, double r, double g, double b, OakRenderBackendHandle handle, void *texture, double r, double g, double b,
double a) double a)
{ {
Q_UNUSED(texture) Renderer(handle)->ClearDestination(static_cast<olive::Texture *>(texture),
Q_UNUSED(r) r, g, b, a);
Q_UNUSED(g)
Q_UNUSED(b)
Q_UNUSED(a)
WarnUnavailable(handle, "clear_destination");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture( OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture(
OakRenderBackendHandle handle, int width, int height, int depth, int format, OakRenderBackendHandle handle, int width, int height, int depth, int format,
int channel_count, const void *data, int linesize, void *out_variant) int channel_count, const void *data, int linesize, void *out_variant)
{ {
Q_UNUSED(width) *static_cast<QVariant *>(out_variant) = Renderer(handle)->CreateNativeTexture(
Q_UNUSED(height) width, height, depth, static_cast<olive::PixelFormat::Format>(format),
Q_UNUSED(depth) channel_count, data, linesize);
Q_UNUSED(format)
Q_UNUSED(channel_count)
Q_UNUSED(data)
Q_UNUSED(linesize)
static_cast<QVariant *>(out_variant)->clear();
WarnUnavailable(handle, "create_native_texture");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture( OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture(
OakRenderBackendHandle handle, const void *variant) OakRenderBackendHandle handle, const void *variant)
{ {
Q_UNUSED(variant) Renderer(handle)->DestroyNativeTexture(VariantRef(variant));
WarnUnavailable(handle, "destroy_native_texture");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader( OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader(
OakRenderBackendHandle handle, const void *shader_code, void *out_variant) OakRenderBackendHandle handle, const void *shader_code, void *out_variant)
{ {
Q_UNUSED(shader_code) *static_cast<QVariant *>(out_variant) = Renderer(handle)->CreateNativeShader(
static_cast<QVariant *>(out_variant)->clear(); *static_cast<const olive::ShaderCode *>(shader_code));
WarnUnavailable(handle, "create_native_shader");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader( OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader(
OakRenderBackendHandle handle, const void *variant) OakRenderBackendHandle handle, const void *variant)
{ {
Q_UNUSED(variant) Renderer(handle)->DestroyNativeShader(VariantRef(variant));
WarnUnavailable(handle, "destroy_native_shader");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture( OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture(
OakRenderBackendHandle handle, const void *variant, const void *video_params, OakRenderBackendHandle handle, const void *variant, const void *video_params,
const void *data, int linesize) const void *data, int linesize)
{ {
Q_UNUSED(variant) Renderer(handle)->UploadToTexture(
Q_UNUSED(video_params) VariantRef(variant), *static_cast<const olive::VideoParams *>(video_params),
Q_UNUSED(data) data, linesize);
Q_UNUSED(linesize)
WarnUnavailable(handle, "upload_to_texture");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture( OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture(
OakRenderBackendHandle handle, const void *variant, const void *video_params, OakRenderBackendHandle handle, const void *variant, const void *video_params,
void *data, int linesize) void *data, int linesize)
{ {
Q_UNUSED(variant) Renderer(handle)->DownloadFromTexture(
Q_UNUSED(video_params) VariantRef(variant), *static_cast<const olive::VideoParams *>(video_params),
Q_UNUSED(data) data, linesize);
Q_UNUSED(linesize)
WarnUnavailable(handle, "download_from_texture");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle) OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle)
{ {
Q_UNUSED(handle) Renderer(handle)->Flush();
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture( OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture(
OakRenderBackendHandle handle, void *texture, const void *point, OakRenderBackendHandle handle, void *texture, const void *point,
void *out_color) void *out_color)
{ {
Q_UNUSED(texture) *static_cast<olive::Color *>(out_color) = Renderer(handle)->GetPixelFromTexture(
Q_UNUSED(point) static_cast<olive::Texture *>(texture), *static_cast<const QPointF *>(point));
Q_UNUSED(out_color)
WarnUnavailable(handle, "get_pixel_from_texture");
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit(
OakRenderBackendHandle handle, const void *shader, void *job, OakRenderBackendHandle handle, const void *shader, void *job,
void *destination, const void *destination_params, bool clear_destination) void *destination, const void *destination_params, bool clear_destination)
{ {
Q_UNUSED(shader) Renderer(handle)->Blit(
Q_UNUSED(job) VariantRef(shader), *static_cast<olive::AcceleratedJob *>(job),
Q_UNUSED(destination) static_cast<olive::Texture *>(destination),
Q_UNUSED(destination_params) *static_cast<const olive::VideoParams *>(destination_params),
Q_UNUSED(clear_destination) clear_destination);
WarnUnavailable(handle, "blit");
} }
OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context( OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context(
@@ -194,3 +188,18 @@ OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context(
Q_UNUSED(handle) Q_UNUSED(handle)
return nullptr; return nullptr;
} }
OAK_RENDER_BACKEND_EXPORT void oak_renderer_attach_output_texture(
OakRenderBackendHandle handle, const void *texture_id)
{
Q_UNUSED(handle)
Q_UNUSED(texture_id)
// Vulkan does not support OFX OpenGL render output attachment.
}
OAK_RENDER_BACKEND_EXPORT void oak_renderer_detach_output_texture(
OakRenderBackendHandle handle)
{
Q_UNUSED(handle)
// Vulkan does not support OFX OpenGL render output attachment.
}
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
/***************************************************************************
Oak Video Editor
Copyright (C) 2025 mikesolar
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***************************************************************************/
#ifndef VULKANRENDERER_H
#define VULKANRENDERER_H
#include <vulkan/vulkan.h>
#include <QMutex>
#include <QObject>
#include <QVariant>
#include "render/renderer.h"
namespace olive
{
class VulkanRenderer : public Renderer {
Q_OBJECT
public:
explicit VulkanRenderer(QObject *parent = nullptr);
virtual ~VulkanRenderer() override;
virtual bool Init() override;
virtual void PostInit() override;
virtual void PostDestroy() override;
virtual void ClearDestination(olive::Texture *texture = nullptr,
double r = 0.0, double g = 0.0,
double b = 0.0, double a = 0.0) override;
virtual QVariant CreateNativeShader(olive::ShaderCode code) override;
virtual void DestroyNativeShader(QVariant shader) override;
virtual void UploadToTexture(const QVariant &handle,
const VideoParams &params, const void *data,
int linesize) override;
virtual void DownloadFromTexture(const QVariant &handle,
const VideoParams &params, void *data,
int linesize) override;
virtual void Flush() override;
virtual Color GetPixelFromTexture(olive::Texture *texture,
const QPointF &pt) override;
bool IsAvailable() const
{
return device_ != VK_NULL_HANDLE;
}
protected:
virtual void Blit(QVariant shader, olive::AcceleratedJob &job,
olive::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:
struct VulkanTexture;
struct VulkanShader;
struct UniformInfo;
bool CreateInstance();
bool CreateDevice();
bool CreateCommandPool();
bool CreateDescriptorPool();
bool CreateVertexBuffer();
bool CreateLinearSampler();
bool CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer,
VkDeviceMemory *out_memory);
void DestroyStagingBuffer(VkBuffer buffer, VkDeviceMemory memory);
VkCommandBuffer BeginOneTimeCommands();
void EndOneTimeCommands(VkCommandBuffer cmd);
void TransitionImageLayout(VkCommandBuffer cmd, VkImage image,
VkImageLayout old_layout,
VkImageLayout new_layout);
void CopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer, VkImage image,
uint32_t width, uint32_t height, uint32_t depth);
void CopyImageToBuffer(VkCommandBuffer cmd, VkImage image, VkBuffer buffer,
uint32_t width, uint32_t height,
uint32_t offset_x = 0, uint32_t offset_y = 0);
VkFormat PixelFormatToVkFormat(PixelFormat format, int channel_count) const;
VkDeviceSize AlignSize(VkDeviceSize size, VkDeviceSize alignment) const;
uint32_t FindMemoryType(uint32_t type_filter,
VkMemoryPropertyFlags properties) const;
bool CompileGlslToSpv(const QString &glsl, VkShaderStageFlagBits stage,
QByteArray *out_spv);
QString ConvertGlslToVulkan(const QString &glsl, VkShaderStageFlagBits stage);
QString ConvertGlslUniformsToUbo(const QString &glsl,
QVector<UniformInfo> *out_uniforms);
VkDeviceSize GetStd140Size(const QString &type) const;
VkDeviceSize GetStd140Alignment(const QString &type) const;
bool CreatePipelineForShader(VulkanShader *shader,
const VideoParams &dest_params,
VkFormat render_pass_format);
VkRenderPass GetOrCreateRenderPass(VkFormat format);
VkInstance instance_ = VK_NULL_HANDLE;
VkPhysicalDevice physical_device_ = VK_NULL_HANDLE;
VkDevice device_ = VK_NULL_HANDLE;
VkQueue graphics_queue_ = VK_NULL_HANDLE;
uint32_t graphics_queue_family_ = UINT32_MAX;
VkCommandPool command_pool_ = VK_NULL_HANDLE;
VkDescriptorPool descriptor_pool_ = VK_NULL_HANDLE;
VkSampler linear_sampler_ = VK_NULL_HANDLE;
QHash<VkFormat, VkRenderPass> render_pass_cache_;
VkBuffer vertex_buffer_ = VK_NULL_HANDLE;
VkDeviceMemory vertex_buffer_memory_ = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties mem_properties_;
VkPhysicalDeviceProperties device_properties_;
QMutex mutex_;
// Texture handle counter
quint64 next_texture_id_ = 1;
QHash<quint64, VulkanTexture *> textures_;
// Shader handle counter
quint64 next_shader_id_ = 1;
QHash<quint64, VulkanShader *> shaders_;
static const int kMaxDescriptorSets = 1024;
};
}
#endif // VULKANRENDERER_H
+44 -5
View File
@@ -39,6 +39,9 @@
#include "render/ipc/frameslotpool.h" #include "render/ipc/frameslotpool.h"
#include "render/ipc/ipcmessage.h" #include "render/ipc/ipcmessage.h"
#include "render/ipc/sharedmemoryregion.h" #include "render/ipc/sharedmemoryregion.h"
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
#include "render/backend/dynamicrenderer.h"
#endif
#include "render/opengl/openglrenderer.h" #include "render/opengl/openglrenderer.h"
#include "render/rendermanager.h" #include "render/rendermanager.h"
#include "render/renderprocessor.h" #include "render/renderprocessor.h"
@@ -80,7 +83,7 @@ QJsonObject ErrorMessage(const QString &message, qint64 ticket_id = 0)
class RenderWorker { class RenderWorker {
public: public:
RenderWorker(olive::OpenGLRenderer *renderer, QFile *out) RenderWorker(olive::Renderer *renderer, QFile *out)
: renderer_(renderer) : renderer_(renderer)
, out_(out) , out_(out)
{ {
@@ -114,7 +117,16 @@ public:
hs.input_slot_data_bytes = 0; hs.input_slot_data_bytes = 0;
QJsonObject handshake = hs.ToJson(); QJsonObject handshake = hs.ToJson();
if (QOpenGLContext *ctx = renderer_->context()) { QOpenGLContext *ctx = nullptr;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
if (auto *dynamic_renderer = dynamic_cast<olive::DynamicRenderer *>(renderer_)) {
ctx = dynamic_renderer->OpenGLContext();
} else
#endif
{
ctx = static_cast<olive::OpenGLRenderer *>(renderer_)->context();
}
if (ctx) {
const QSurfaceFormat fmt = ctx->format(); const QSurfaceFormat fmt = ctx->format();
handshake["gl_major"] = fmt.majorVersion(); handshake["gl_major"] = fmt.majorVersion();
handshake["gl_minor"] = fmt.minorVersion(); handshake["gl_minor"] = fmt.minorVersion();
@@ -427,7 +439,7 @@ private:
return Write(ready.ToJson()); return Write(ready.ToJson());
} }
olive::OpenGLRenderer *renderer_; olive::Renderer *renderer_;
QFile *out_; QFile *out_;
bool shutdown_requested_ = false; bool shutdown_requested_ = false;
std::unique_ptr<olive::Project> project_; std::unique_ptr<olive::Project> project_;
@@ -459,15 +471,42 @@ int main(int argc, char *argv[])
return 1; return 1;
} }
auto *renderer = new olive::OpenGLRenderer(); olive::Renderer *renderer;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
auto *dynamic_renderer = new olive::DynamicRenderer(QStringLiteral("opengl"));
if (dynamic_renderer->Init()) {
dynamic_renderer->PostInit();
renderer = dynamic_renderer;
} else {
delete dynamic_renderer;
qWarning() << "Failed to initialize dynamic OpenGL backend, falling back to direct OpenGL renderer";
renderer = new olive::OpenGLRenderer();
if (!renderer->Init()) {
LogError(QStringLiteral("failed to initialize OpenGL renderer"));
delete renderer;
return 1;
}
renderer->PostInit();
}
#else
renderer = new olive::OpenGLRenderer();
if (!renderer->Init()) { if (!renderer->Init()) {
LogError(QStringLiteral("failed to initialize OpenGL renderer")); LogError(QStringLiteral("failed to initialize OpenGL renderer"));
delete renderer; delete renderer;
return 1; return 1;
} }
renderer->PostInit(); renderer->PostInit();
#endif
QOpenGLContext *ctx = renderer->context(); QOpenGLContext *ctx = nullptr;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
if (auto *loaded_renderer = dynamic_cast<olive::DynamicRenderer *>(renderer)) {
ctx = loaded_renderer->OpenGLContext();
} else
#endif
{
ctx = static_cast<olive::OpenGLRenderer *>(renderer)->context();
}
if (!ctx || !ctx->isValid()) { if (!ctx || !ctx->isValid()) {
LogError(QStringLiteral("OpenGL context is not valid after init")); LogError(QStringLiteral("OpenGL context is not valid after init"));
renderer->Destroy(); renderer->Destroy();
-1
View File
@@ -35,7 +35,6 @@
namespace olive namespace olive
{ {
const char *StyleManager::kDefaultStyle = "olive-dark";
QString StyleManager::current_style_; QString StyleManager::current_style_;
QMap<QString, QString> StyleManager::available_themes_; QMap<QString, QString> StyleManager::available_themes_;
+1 -1
View File
@@ -38,7 +38,7 @@ public:
static void SetStyle(const QString &style_path); static void SetStyle(const QString &style_path);
static const char *kDefaultStyle; inline static const char *kDefaultStyle = "olive-dark";
static const QMap<QString, QString> &available_themes() static const QMap<QString, QString> &available_themes()
{ {
+45 -26
View File
@@ -40,13 +40,33 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, color_manager_(nullptr) , color_manager_(nullptr)
, color_service_(nullptr) , color_service_(nullptr)
, is_backend_neutral_(false)
{ {
QHBoxLayout *layout = new QHBoxLayout(this); QHBoxLayout *layout = new QHBoxLayout(this);
layout->setSpacing(0); layout->setSpacing(0);
layout->setContentsMargins(0, 0, 0, 0); layout->setContentsMargins(0, 0, 0, 0);
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { // Create renderer
// Create OpenGL widget #ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
{
auto *dynamic_renderer = new DynamicRenderer(
RenderManager::BackendToString(
RenderManager::instance()->requested_backend()),
this);
if (!dynamic_renderer->Load()) {
qWarning() << "Failed to load dynamic render backend for viewer, falling back to OpenGL";
delete dynamic_renderer;
attached_renderer_ = new OpenGLRenderer(this);
} else {
attached_renderer_ = dynamic_renderer;
}
}
#else
attached_renderer_ = new OpenGLRenderer(this);
#endif
if (attached_renderer_->IsOpenGL()) {
// OpenGL path
inner_widget_ = new ManagedDisplayWidgetOpenGL(); inner_widget_ = new ManagedDisplayWidgetOpenGL();
inner_widget_->setAttribute(Qt::WA_TranslucentBackground, false); inner_widget_->setAttribute(Qt::WA_TranslucentBackground, false);
connect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_), connect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
@@ -64,16 +84,6 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
inner_widget_->installEventFilter(this); inner_widget_->installEventFilter(this);
// Create renderer bound to the widget's OpenGL context.
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
attached_renderer_ = new DynamicRenderer(
RenderManager::BackendToString(
RenderManager::instance()->requested_backend()),
this);
#else
attached_renderer_ = new OpenGLRenderer(this);
#endif
// Create widget wrapper for OpenGL window // Create widget wrapper for OpenGL window
#ifdef USE_QOPENGLWINDOW #ifdef USE_QOPENGLWINDOW
wrapper_ = QWidget::createWindowContainer( wrapper_ = QWidget::createWindowContainer(
@@ -83,19 +93,29 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
#endif #endif
layout->addWidget(wrapper_); layout->addWidget(wrapper_);
} else { } else {
inner_widget_ = nullptr; // Backend-neutral path (Vulkan, etc.)
wrapper_ = nullptr; is_backend_neutral_ = true;
auto *bn_widget = new ManagedDisplayWidgetBackendNeutral(this);
inner_widget_ = bn_widget;
inner_widget_->setAttribute(Qt::WA_OpaquePaintEvent);
inner_widget_->installEventFilter(this);
connect(bn_widget, &ManagedDisplayWidgetBackendNeutral::OnPaint, this,
&ManagedDisplayWidget::OnPaint, Qt::DirectConnection);
wrapper_ = inner_widget_;
layout->addWidget(wrapper_);
} }
} }
ManagedDisplayWidget::~ManagedDisplayWidget() ManagedDisplayWidget::~ManagedDisplayWidget()
{ {
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER; if (!is_backend_neutral_) {
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER;
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
disconnect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_), disconnect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
&ManagedDisplayWidgetOpenGL::OnDestroy, this, &ManagedDisplayWidgetOpenGL::OnDestroy, this,
&ManagedDisplayWidget::OnDestroy); &ManagedDisplayWidget::OnDestroy);
} else {
OnDestroy();
} }
} }
@@ -251,7 +271,7 @@ void ManagedDisplayWidget::SetColorTransform(const ColorTransform &transform)
void ManagedDisplayWidget::OnInit() void ManagedDisplayWidget::OnInit()
{ {
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { if (!is_backend_neutral_) {
QOpenGLContext *context = QOpenGLContext *context =
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->context(); static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->context();
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND #ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
@@ -264,6 +284,9 @@ void ManagedDisplayWidget::OnInit()
#endif #endif
static_cast<OpenGLRenderer *>(attached_renderer_)->Init(context); static_cast<OpenGLRenderer *>(attached_renderer_)->Init(context);
static_cast<OpenGLRenderer *>(attached_renderer_)->PostInit(); static_cast<OpenGLRenderer *>(attached_renderer_)->PostInit();
} else {
attached_renderer_->Init();
attached_renderer_->PostInit();
} }
} }
@@ -280,25 +303,21 @@ void ManagedDisplayWidget::ColorProcessorChangedEvent()
void ManagedDisplayWidget::makeCurrent() void ManagedDisplayWidget::makeCurrent()
{ {
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { if (!is_backend_neutral_) {
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->makeCurrent(); static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->makeCurrent();
} }
} }
void ManagedDisplayWidget::doneCurrent() void ManagedDisplayWidget::doneCurrent()
{ {
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { if (!is_backend_neutral_) {
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->doneCurrent(); static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->doneCurrent();
} }
} }
QPaintDevice *ManagedDisplayWidget::paint_device() const QPaintDevice *ManagedDisplayWidget::paint_device() const
{ {
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { return inner_widget_;
return static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_);
} else {
return nullptr;
}
} }
void ManagedDisplayWidget::SetInnerMouseTracking(bool e) void ManagedDisplayWidget::SetInnerMouseTracking(bool e)
@@ -320,8 +339,8 @@ VideoParams ManagedDisplayWidget::GetViewportParams() const
void ManagedDisplayWidget::update() void ManagedDisplayWidget::update()
{ {
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { if (inner_widget_) {
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->update(); inner_widget_->update();
} }
} }
@@ -101,6 +101,32 @@ private slots:
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER; \ MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER; \
} }
/**
* @brief Inner widget for backend-neutral rendering paths (e.g. Vulkan).
*
* It does not own a GL context; instead it forwards Qt paint events to the
* ManagedDisplayWidget so that the viewer can render via QPainter.
*/
class ManagedDisplayWidgetBackendNeutral : public QWidget {
Q_OBJECT
public:
ManagedDisplayWidgetBackendNeutral(QWidget *parent = nullptr)
: QWidget(parent)
{
}
signals:
void OnPaint();
protected:
virtual void paintEvent(QPaintEvent *event) override
{
QWidget::paintEvent(event);
emit OnPaint();
}
};
class ManagedDisplayWidget : public QWidget { class ManagedDisplayWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
@@ -221,6 +247,11 @@ protected:
void SetInnerMouseTracking(bool e); void SetInnerMouseTracking(bool e);
bool IsBackendNeutral() const
{
return is_backend_neutral_;
}
QRect GetInnerRect() const QRect GetInnerRect() const
{ {
return wrapper_ ? wrapper_->rect() : QRect(); return wrapper_ ? wrapper_->rect() : QRect();
@@ -285,6 +316,8 @@ private:
*/ */
ColorTransform color_transform_; ColorTransform color_transform_;
bool is_backend_neutral_ = false;
private slots: private slots:
/** /**
* @brief Sets all color settings to the defaults pertaining to this configuration * @brief Sets all color settings to the defaults pertaining to this configuration
+6
View File
@@ -22,6 +22,7 @@
#include "scopebase.h" #include "scopebase.h"
#include "config/config.h" #include "config/config.h"
#include "render/job/colortransformjob.h"
namespace olive namespace olive
{ {
@@ -68,6 +69,11 @@ void ScopeBase::OnInit()
void ScopeBase::OnPaint() void ScopeBase::OnPaint()
{ {
if (IsBackendNeutral()) {
// TODO: implement backend-neutral scope display
return;
}
// Clear display surface // Clear display surface
renderer()->ClearDestination(); renderer()->ClearDestination();
+106 -31
View File
@@ -375,19 +375,40 @@ bool ViewerDisplayWidget::eventFilter(QObject *o, QEvent *e)
void ViewerDisplayWidget::OnPaint() void ViewerDisplayWidget::OnPaint()
{ {
// Clear background to empty const bool backend_neutral = IsBackendNeutral();
QColor bg_color = show_widget_background_ ? palette().window().color() :
Qt::black; QPainter bg_painter;
renderer()->ClearDestination(nullptr, bg_color.redF(), bg_color.greenF(), bool bg_painter_active = false;
bg_color.blueF());
if (backend_neutral) {
// Backend-neutral path: draw background directly with QPainter. The
// image itself will be rendered offscreen, downloaded, and painted below.
bg_painter.begin(paint_device());
bg_painter_active = true;
bg_painter.fillRect(GetInnerRect(),
show_widget_background_ ? palette().window().color() :
Qt::black);
} else {
// Clear background to empty
QColor bg_color = show_widget_background_ ? palette().window().color() :
Qt::black;
renderer()->ClearDestination(nullptr, bg_color.redF(), bg_color.greenF(),
bg_color.blueF());
}
VideoParams device_params;
ColorTransformJob ctj;
bool have_ctj = false;
// We only draw if we have a pipeline // We only draw if we have a pipeline
if (push_mode_ != kPushNull) { if (push_mode_ != kPushNull) {
// Draw texture through color transform // Draw texture through color transform
VideoParams device_params = GetViewportParams(); device_params = GetViewportParams();
if (push_mode_ == kPushBlank) { if (push_mode_ == kPushBlank) {
DrawBlank(device_params); if (!backend_neutral) {
DrawBlank(device_params);
}
} else if (color_service()) { } else if (color_service()) {
if (FramePtr frame = load_frame_.value<FramePtr>()) { if (FramePtr frame = load_frame_.value<FramePtr>()) {
// This is a CPU frame, upload it now // This is a CPU frame, upload it now
@@ -408,27 +429,17 @@ 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()) {
bool copied = false; // Cross-renderer texture: download and re-upload
QOpenGLContext *ctx = QOpenGLContext::currentContext(); FramePtr frame = Frame::Create();
if (ctx) { frame->set_video_params(texture->params());
QOpenGLFunctions *funcs = ctx->functions(); if (frame->allocate()) {
GLuint tex_id = texture->id().value<GLuint>(); texture->renderer()->DownloadFromTexture(
if (funcs && tex_id && funcs->glIsTexture(tex_id)) { texture->id(), texture->params(),
FramePtr frame = Frame::Create(); frame->data(), frame->linesize_pixels());
frame->set_video_params(texture->params()); texture_ = renderer()->CreateTexture(
if (frame->allocate()) { frame->video_params(), frame->data(),
renderer()->DownloadFromTexture( frame->linesize_pixels());
texture->id(), texture->params(), } else {
frame->data(), frame->linesize_pixels());
texture_ = renderer()->CreateTexture(
frame->video_params(), frame->data(),
frame->linesize_pixels());
copied = true;
}
}
}
if (!copied) {
texture_ = texture; texture_ = texture;
} }
} else { } else {
@@ -445,7 +456,9 @@ void ViewerDisplayWidget::OnPaint()
TexturePtr texture_to_draw = texture_; TexturePtr texture_to_draw = texture_;
if (!texture_to_draw || texture_to_draw->IsDummy()) { if (!texture_to_draw || texture_to_draw->IsDummy()) {
DrawBlank(device_params); if (!backend_neutral) {
DrawBlank(device_params);
}
} else { } else {
if (deinterlace_) { if (deinterlace_) {
if (deinterlace_shader_.isNull()) { if (deinterlace_shader_.isNull()) {
@@ -477,7 +490,6 @@ void ViewerDisplayWidget::OnPaint()
texture_to_draw = deinterlace_texture_; texture_to_draw = deinterlace_texture_;
} }
ColorTransformJob ctj;
ctj.SetColorProcessor(color_service()); ctj.SetColorProcessor(color_service());
ctj.SetInputTexture(texture_to_draw); ctj.SetInputTexture(texture_to_draw);
ctj.SetInputAlphaAssociation( ctj.SetInputAlphaAssociation(
@@ -489,13 +501,25 @@ void ViewerDisplayWidget::OnPaint()
ctj.SetCropMatrix(crop_matrix_); ctj.SetCropMatrix(crop_matrix_);
ctj.SetForceOpaque(true); ctj.SetForceOpaque(true);
renderer()->BlitColorManaged(ctj, device_params); have_ctj = true;
} }
} else { } else {
qDebug() << "[VIEWER] OnPaint no color_service, skipping texture draw"; qDebug() << "[VIEWER] OnPaint no color_service, skipping texture draw";
} }
} }
if (have_ctj) {
if (backend_neutral) {
DrawBackendNeutral(ctj, &bg_painter);
} else {
renderer()->BlitColorManaged(ctj, device_params);
}
}
if (bg_painter_active) {
bg_painter.end();
}
// Draw gizmos if we have any // Draw gizmos if we have any
if (gizmos_) { if (gizmos_) {
QPainter p(paint_device()); QPainter p(paint_device());
@@ -633,6 +657,8 @@ void ViewerDisplayWidget::OnDestroy()
texture_ = nullptr; texture_ = nullptr;
deinterlace_texture_ = nullptr; deinterlace_texture_ = nullptr;
backend_neutral_texture_ = nullptr;
backend_neutral_buffer_.clear();
if (load_frame_.isNull()) { if (load_frame_.isNull()) {
push_mode_ = kPushNull; push_mode_ = kPushNull;
} else { } else {
@@ -1364,6 +1390,55 @@ void ViewerDisplayWidget::DrawBlank(const VideoParams &device_params)
renderer()->Blit(blank_shader_, job, device_params, false); renderer()->Blit(blank_shader_, job, device_params, false);
} }
void ViewerDisplayWidget::DrawBackendNeutral(const ColorTransformJob &ctj,
QPainter *painter)
{
if (!painter || !painter->isActive()) {
return;
}
const int texture_width =
static_cast<int>(width() * devicePixelRatioF());
const int texture_height =
static_cast<int>(height() * devicePixelRatioF());
const VideoParams offscreen_params(
texture_width, texture_height, PixelFormat::U8,
VideoParams::kRGBAChannelCount);
if (!backend_neutral_texture_ ||
backend_neutral_texture_->params() != offscreen_params) {
backend_neutral_texture_ = renderer()->CreateTexture(offscreen_params);
backend_neutral_buffer_.resize(
texture_width * texture_height *
VideoParams::GetBytesPerPixel(PixelFormat::U8,
VideoParams::kRGBAChannelCount));
}
if (!backend_neutral_texture_ || backend_neutral_texture_->IsDummy()) {
return;
}
ColorTransformJob local_ctj = ctj;
local_ctj.SetClearDestinationEnabled(true);
renderer()->BlitColorManaged(local_ctj, backend_neutral_texture_.get());
backend_neutral_texture_->Download(backend_neutral_buffer_.data(), 0);
const int bytes_per_pixel = VideoParams::GetBytesPerPixel(
PixelFormat::U8, VideoParams::kRGBAChannelCount);
QImage img(reinterpret_cast<const uchar *>(
backend_neutral_buffer_.constData()),
texture_width, texture_height,
texture_width * bytes_per_pixel,
QImage::Format_RGBA8888_Premultiplied);
img.setDevicePixelRatio(devicePixelRatioF());
painter->drawImage(QPoint(0, 0), img);
}
void ViewerDisplayWidget::SetShowFPS(bool e) void ViewerDisplayWidget::SetShowFPS(bool e)
{ {
show_fps_ = e; show_fps_ = e;
+15
View File
@@ -327,6 +327,8 @@ private:
void DrawBlank(const VideoParams &device_params); void DrawBlank(const VideoParams &device_params);
void DrawBackendNeutral(const ColorTransformJob &ctj, QPainter *painter);
/** /**
* @brief Internal reference to the OpenGL texture to draw. Set in SetTexture() and used in paintGL(). * @brief Internal reference to the OpenGL texture to draw. Set in SetTexture() and used in paintGL().
*/ */
@@ -337,6 +339,19 @@ private:
*/ */
TexturePtr deinterlace_texture_; TexturePtr deinterlace_texture_;
/**
* @brief Offscreen texture for backend-neutral viewer rendering.
*
* The texture is rendered at device resolution and read back to a QImage so
* it can be painted with QPainter on the plain QWidget inner surface.
*/
TexturePtr backend_neutral_texture_;
/**
* @brief CPU readback buffer for backend_neutral_texture_.
*/
QByteArray backend_neutral_buffer_;
/** /**
* @brief Deinterlace shader * @brief Deinterlace shader
*/ */
+10 -2
View File
@@ -14,6 +14,7 @@ This document describes how to build Oak Video Editor from source on Windows, Li
- Expat - Expat
- PortAudio - PortAudio
- OpenGL headers - OpenGL headers
- Vulkan SDK (optional, required for the Vulkan render backend)
- XKB common (Linux) - XKB common (Linux)
--- ---
@@ -42,6 +43,8 @@ pacman -S --needed \
mingw-w64-ucrt-x86_64-fmt \ mingw-w64-ucrt-x86_64-fmt \
mingw-w64-ucrt-x86_64-expat \ mingw-w64-ucrt-x86_64-expat \
mingw-w64-ucrt-x86_64-portaudio \ mingw-w64-ucrt-x86_64-portaudio \
mingw-w64-ucrt-x86_64-vulkan-headers \
mingw-w64-ucrt-x86_64-vulkan-loader \
mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gcc
``` ```
@@ -83,7 +86,7 @@ sudo apt-get install -y \
cmake ninja-build pkg-config nasm \ cmake ninja-build pkg-config nasm \
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \ libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers libxkbcommon-dev
``` ```
Build FFmpeg 8.0+ from source: Build FFmpeg 8.0+ from source:
@@ -137,6 +140,8 @@ sudo dnf install -y \
expat-devel \ expat-devel \
portaudio-devel \ portaudio-devel \
mesa-libGL-devel \ mesa-libGL-devel \
vulkan-headers \
vulkan-loader-devel \
libxkbcommon-devel \ libxkbcommon-devel \
gcc-c++ \ gcc-c++ \
bzip2-devel bzip2-devel
@@ -171,6 +176,8 @@ sudo pacman -S --needed \
expat \ expat \
portaudio \ portaudio \
mesa \ mesa \
vulkan-headers \
vulkan-icd-loader \
libxkbcommon \ libxkbcommon \
gcc gcc
``` ```
@@ -200,7 +207,7 @@ Install dependencies:
```bash ```bash
brew update brew update
brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat molten-vk vulkan-headers
``` ```
Build OpenTimelineIO (optional, required for OTIO support): Build OpenTimelineIO (optional, required for OTIO support):
@@ -248,6 +255,7 @@ ctest --test-dir build --output-on-failure -C Release
| `BUILD_QT6` | `ON` | Build with Qt 6 instead of Qt 5 | | `BUILD_QT6` | `ON` | Build with Qt 6 instead of Qt 5 |
| `OTIO_LOCATION` | - | Path to OpenTimelineIO installation (optional) | | `OTIO_LOCATION` | - | Path to OpenTimelineIO installation (optional) |
| `OCIO_LOCATION` | - | Path to OpenColorIO installation | | `OCIO_LOCATION` | - | Path to OpenColorIO installation |
| `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` | `ON` | Build dynamic render backend libraries (`liboakgl.so` / `liboakvulkan.so`) |
--- ---
+10 -2
View File
@@ -14,6 +14,7 @@
- Expat - Expat
- PortAudio - PortAudio
- OpenGL 头文件 - OpenGL 头文件
- Vulkan SDK(可选,Vulkan 渲染后端需要)
- XKB commonLinux - XKB commonLinux
--- ---
@@ -42,6 +43,8 @@ pacman -S --needed \
mingw-w64-ucrt-x86_64-fmt \ mingw-w64-ucrt-x86_64-fmt \
mingw-w64-ucrt-x86_64-expat \ mingw-w64-ucrt-x86_64-expat \
mingw-w64-ucrt-x86_64-portaudio \ mingw-w64-ucrt-x86_64-portaudio \
mingw-w64-ucrt-x86_64-vulkan-headers \
mingw-w64-ucrt-x86_64-vulkan-loader \
mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-gcc
``` ```
@@ -83,7 +86,7 @@ sudo apt-get install -y \
cmake ninja-build pkg-config nasm \ cmake ninja-build pkg-config nasm \
qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \ qt6-base-dev qt6-base-dev-tools qt6-base-private-dev qt6-tools-dev qt6-tools-dev-tools \
libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \ libopencolorio-dev libopenimageio-dev libopenexr-dev libexpat1-dev \
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers libxkbcommon-dev
``` ```
从源码编译 FFmpeg 8.0+ 从源码编译 FFmpeg 8.0+
@@ -137,6 +140,8 @@ sudo dnf install -y \
expat-devel \ expat-devel \
portaudio-devel \ portaudio-devel \
mesa-libGL-devel \ mesa-libGL-devel \
vulkan-headers \
vulkan-loader-devel \
libxkbcommon-devel \ libxkbcommon-devel \
gcc-c++ gcc-c++
``` ```
@@ -170,6 +175,8 @@ sudo pacman -S --needed \
expat \ expat \
portaudio \ portaudio \
mesa \ mesa \
vulkan-headers \
vulkan-icd-loader \
libxkbcommon \ libxkbcommon \
gcc gcc
``` ```
@@ -199,7 +206,7 @@ ctest --test-dir build --output-on-failure -C Release
```bash ```bash
brew update brew update
brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat brew install cmake ninja pkg-config qt@6 ffmpeg openimageio opencolorio openexr portaudio expat molten-vk vulkan-headers
``` ```
构建 OpenTimelineIO(可选,如需 OTIO 支持): 构建 OpenTimelineIO(可选,如需 OTIO 支持):
@@ -247,6 +254,7 @@ ctest --test-dir build --output-on-failure -C Release
| `BUILD_QT6` | `ON` | 使用 Qt 6 而非 Qt 5 | | `BUILD_QT6` | `ON` | 使用 Qt 6 而非 Qt 5 |
| `OTIO_LOCATION` | - | OpenTimelineIO 安装路径(可选) | | `OTIO_LOCATION` | - | OpenTimelineIO 安装路径(可选) |
| `OCIO_LOCATION` | - | OpenColorIO 安装路径 | | `OCIO_LOCATION` | - | OpenColorIO 安装路径 |
| `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` | `ON` | 构建动态渲染后端库(`liboakgl.so` / `liboakvulkan.so` |
--- ---
+72 -37
View File
@@ -48,58 +48,93 @@
这一步是 ABI 稳定化,不是把后端内部实现改成 C。 这一步是 ABI 稳定化,不是把后端内部实现改成 C。
## 阶段 2.5:最小化 OpenGL 后端链接边界 ## 阶段 2.5:最小化 OpenGL/Vulkan 后端链接边界(已完成)
当前工程大量代码和静态依赖被编译进 `libolive-editor` object 库。直接把整个 object 库塞进 `liboakgl.so` 会带来两个问题: 此前 `oakgl`/`oakvulkan` 通过 `$<TARGET_OBJECTS:libolive-editor>` 把整个 editor 对象库链进动态库,导致后端库包含项目、节点、任务、cache、UI 等大量 editor 代码和全局状态。
- 非 PIC 静态依赖会阻塞共享库链接,例如 `KDDockWidgets` 本次已完成链接边界收敛:
- 主程序和后端库会复制全局状态,增加配置、cache、单例和 Qt meta-object 的一致性风险。
当前实验构建已经可以通过 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND=ON` 生成 `liboakgl.so`,做法是把相关 object/static 依赖切到 PIC 后链接进 OpenGL 后端库。这满足“动态库一侧 C++ 实现 + C ABI 导出”的第一步,但它仍不是最终边界:后端库暂时会带入较多 editor 代码和全局状态。 - 新增静态库 `libolive-rendercore`,仅包含渲染核心代码:
- 渲染器基类与数据类型:`Renderer``Texture``VideoParams``ShaderCode``AcceleratedJob``ShaderJob`
- 动态适配器:`DynamicRenderer``renderbackend_c.h`
- 必要的 value/config/工具:`node/value``node/param``node/valuedatabase``config/config``common/filefunctions``common/qtutils``common/avframeptr`
- `oakgl`/`oakvulkan` 现在只链接 `libolive-rendercore`,不再链接完整 `libolive-editor`
- `liboakgl.so` / `liboakvulkan.so` 体积从约 21 MB 降至约 600 KB。
- 为隔离依赖做的头文件清理:
- `renderer.h` 移除 `node/node.h``render/colorprocessor.h``render/job/colortransformjob.h``job/pluginjob.h`,改为前向声明。
- `videoparams.h` 移除 `ofxImageEffect.h`OFX 字符串 setter 实现下移到 `videoparams.cpp`
- `texture.h` 用新增的 `common/avframeptr.h` 替代 `common/ffmpegutils.h`,避免后端拉入大量 FFmpeg 工具代码。
- `renderer.cpp` 的颜色管理(`GetColorContext` / `BlitColorManaged`)和隔行(`InterlaceTexture`)实现分别拆到 `render/colormanagement.cpp``render/interlacetexture.cpp`,这两个文件仍由 editor/worker 链接,但不进入后端库。
- 修复了拆分过程中暴露的 `StyleManager::kDefaultStyle` 跨库符号问题:改为 header 内 `inline static` 定义,使 `config.cpp` 在后端库中自包含。
已新增 `DynamicRenderBackend.LoadsExperimentalOpenGLBackend` smoke test:默认构建跳过,实验构建会实际加载 `liboakgl` 并验证 create/destroy C ABI 路径。 剩余优化空间:
- 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。
已新增可选 `oak_renderer_is_available` C ABI:后端库可以在被加载和 create 后报告自身是否可用。OpenGL 后端返回可用;Vulkan 占位后端返回不可用,适配器随后卸载它并回退 OpenGL。这把“后端存在”和“后端可用于渲染”分开,避免后续最小化链接边界时把不可用实现误接入渲染路径。
已新增 `oak_renderer_get_info` C ABI:后端库可以报告 ABI 版本、后端类型、能力位和状态字符串。默认构建也会编译检查 OpenGL/Vulkan 两个 C wrapper,避免只在实验构建中发现 C ABI 破损。
因此动态后端成为默认路径前,仍需要把 OpenGL 后端库的链接边界收敛到最小集合:
- 后端库只拥有 OpenGL/Vulkan native 操作和必要的后端私有状态。
- 主程序侧保留项目、节点、任务、cache、OpenFX host 等 editor 状态。
- 如果某个后端函数需要主程序创建 `Texture` 或访问 cache,用 C callback table 从后端回调主程序,而不是把完整 editor 链进后端库。
- `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 可用于实验构建和加载验证;默认启用前必须完成最小链接边界拆分并通过 CI。
## 阶段 3Vulkan 后端 ## 阶段 3Vulkan 后端
- 新增 Vulkan 后端库。 - 新增 Vulkan 后端库 `liboakvulkan.so`
- 新增 `liboakvulkan.so` 实验占位库:内部使用 C++,对外导出与 OpenGL 后端一致的 C ABI;当前 `oak_renderer_is_available` 返回 `false`,请求 Vulkan 时由 `DynamicRenderer` 自动回退 OpenGL - 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线
- 已新增 `DynamicRenderBackend.FallsBackWhenExperimentalVulkanUnavailable` smoke test:默认构建跳过,实验构建验证 Vulkan 占位后端可加载且不会被误用为可渲染后端 - CMake 集成:根目录查找 `Vulkan``shaderc`(可选);`oakvulkan` 目标链接 `Vulkan::Vulkan``shaderc_shared`
- 实现 Vulkan instance/device/swapchain 或 offscreen image 管理。 - 实现 Vulkan instance/device/queue/command pool 管理。
- 实现 texture 创建、上传、下载、shader 编译/缓存、blit、clear、flush - 实现 offscreen image/texture 管理(`CreateNativeTexture` / `DestroyNativeTexture`),支持 2D/3D、多种 pixel formatU8/U16/F16/F32 × 1/2/3/4 channel
- 提供 OpenGL 辅助格式转换函数的 Vulkan 替代版本 - 实现 staging buffer 上传/下载(`UploadToTexture` / `DownloadFromTexture`
- 确认 CPU readback、proxy preview、scope、thumbnail/cache 输出一致 - 实现 `ClearDestination``vkCmdClearColorImage`
- 实现 `Flush``vkDeviceWaitIdle`)。
- 实现 GLSL → SPIR-V 运行时编译(通过 `shaderc`),支持自动 uniform binding。
- 实现基础 graphics pipeline 用于 `Blit`(全屏 quad、顶点缓冲、固定 render pass、combined image sampler descriptor set)。
- 提供 `GetPixelFromTexture`(基于 `DownloadFromTexture` 的简化实现)。
- `oak_renderer_is_available` 现在会在首次检查时尝试 `Init()`,成功后报告 Vulkan 可用。
- 测试更新:
- `LoadsExperimentalVulkanBackendWhenAvailable`:验证 Vulkan 后端可加载、初始化、报告能力位。
- `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。
- `GetPixelFromTexture`:已优化为仅下载 1×1 像素区域。
- RenderPass 格式缓存:已删除固定 `R32G32B32A32_SFLOAT` render pass,改为按 `VkFormat` 缓存;`CreatePipelineForShader` 按 (shader, render_pass_format) 缓存 pipeline。
- **已知限制 / 待完善**
- 链接边界已最小化,`liboakvulkan.so` 现在只依赖 `libolive-rendercore`
- 尚未在 proxy、thumbnail/cache 等完整渲染路径上验证 Vulkan 输出一致性。
## 阶段 4Viewer 双后端 ## 阶段 4Viewer 双后端(默认路径已切换)
- 当前 viewer display 基于 OpenGL widget 和 GL texture id。 - 当前 viewer display 基于 OpenGL widget 和 GL texture id。
- 已在 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND=ON` 实验构建下 Viewer 的 managed display 使用 `DynamicRenderer` 创建 renderer,并把现有 `QOpenGLContext` 传入动态后端;默认构建仍直接使用 `OpenGLRenderer` - 默认构建下 Viewer 的 managed display 现在使用 `DynamicRenderer` 创建 renderer,并把现有 `QOpenGLContext` 传入动态后端;若动态后端加载失败则回退到 `OpenGLRenderer`
- 新增 backend-neutral viewer path - `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认改为 `ON`,保留 `OFF` 作为应急开关
- 新增 backend-neutral viewer path 骨架:
- `ManagedDisplayWidget` 支持非 OpenGL inner widget(普通 `QWidget`),通过 `Renderer::IsOpenGL()` 判断。
- `RenderManager` 不再在 `requested_backend_ == kVulkan` 时强制 fallback。
- `ViewerDisplayWidget` 已移除 `glIsTexture()` 的直接 OpenGL 依赖,改为通用的跨 renderer texture 拷贝。
- `ScopeBase` 在 backend-neutral 时安全跳过(TODO:完整 scope display 路径)。
- OpenGL 使用现有 `QOpenGLWidget/QOpenGLWindow` - OpenGL 使用现有 `QOpenGLWidget/QOpenGLWindow`
- Vulkan 使用 `QVulkanWindow` 或 Qt RHI/QRhi 显示路径。 - Vulkan viewer 完整 readback display 路径(offscreen texture → download → QImage → QPainter)已实现:
- 新增 `ManagedDisplayWidgetBackendNeutral`,在普通 `QWidget``paintEvent` 中转发到 `ManagedDisplayWidget::OnPaint`
- `ViewerDisplayWidget::OnPaint` 在 backend-neutral 模式下改用 `QPainter` 填充背景,将颜色管理后的画面渲染到 U8 RGBA offscreen texture,再 `Download` 到 CPU buffer,最后用 `QImage::Format_RGBA8888_Premultiplied` + `setDevicePixelRatio` 绘制到 inner widget。
- OpenGL 路径保持原有 `BlitColorManaged` 直接到 widget 不变。
- Viewer 只消费后端 texture handle 或 readback frame,不直接假设 GL texture id。 - Viewer 只消费后端 texture handle 或 readback frame,不直接假设 GL texture id。
## 阶段 5OpenFX 处理边界 ## 阶段 5OpenFX 处理边界(已完成)
- OpenFX 插件 OpenGL 渲染路径保留 OpenGL 依赖,不强行改写。 - OpenFX 插件 OpenGL 渲染路径保留 OpenGL 依赖,不强行改写。
- 格式转换、readback、upload 等辅助函数移入后端库。 - `PluginRenderer` 不再继承 `OpenGLRenderer`,改为持有通用的 `Renderer *`
- Vulkan 后端提供等价辅助函数 - OpenGL 渲染路径仅在 `renderer_->IsOpenGL()` 为 true 时启用,并正确调用 `OlivePluginInstance::setOpenGLEnabled(use_opengl)`
- 如果某个 OFX 插件必须走 OpenGL,则在 Vulkan 项目里明确使用 OpenGL 兼容路径或回退 - OpenGL 渲染器(Vulkan、DynamicRenderer 加载的任意后端)自动回退到 CPU readback/upload 路径,不再因缺少 OpenGL context 而直接跳过插件渲染
- 将 OFX 输出纹理绑定/解绑抽象为 `Renderer::AttachOutputTexture` / `DetachOutputTexture`
- `OpenGLRenderer` 实现为 `AttachTextureAsDestination` / `DetachTextureAsDestination`
- C ABI 新增 `oak_renderer_attach_output_texture` / `oak_renderer_detach_output_texture`
- `DynamicRenderer` 通过 C ABI 转发,使动态 OpenGL 后端也能支持 OFX OpenGL 渲染。
- `VulkanRenderer` 默认 no-opVulkan 项目中的 OFX 插件回退到 CPU 路径。
- 格式转换(`ConvertFrameIfNeeded``ConvertTextureForParams`)、readback`ReadbackTextureToFrame`)、upload 等辅助函数保持后端无关,通过 `Renderer` 接口调用,无需移入后端库。
- `RenderProcessor::ProcessPluginJob` 不再要求 `render_ctx_` 实现 `OpenGLContextProvider`,任何 `Renderer` 都能驱动插件渲染。
- 更新相关 gtest`PluginRenderer` 构造函数现在需要传入 renderer 指针,测试传入 `nullptr` 验证纯 CPU 路径。
## 完成标准 ## 完成标准
- 主程序不直接 new `OpenGLRenderer` 作为默认渲染路径,而是通过动态后端适配器创建 - [x] 主程序默认不再直接 new `OpenGLRenderer`,而是通过 `DynamicRenderer` 动态加载 OpenGL/Vulkan 后端;加载失败时保留回退到 `OpenGLRenderer` 的安全路径
- OpenGL 后端库可单独构建、加载、初始化、销毁 - [x] `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认 `ON``liboakgl.so` / `liboakvulkan.so` 默认构建并安装
- 用户能在配置中选择 OpenGL/Vulkan - [x] OpenGL 后端库可单独构建、加载、初始化、销毁
- Vulkan 未实现完整 renderer 前,请求 Vulkan 不崩溃,并有明确回退 - [x] 用户能在配置中选择 OpenGL/Vulkan
- 手工测试计划覆盖 OpenGL/Vulkan 选择、重启持久化、回退、viewer、proxy、scope、导出 - [x] Vulkan 不可用时自动回退到 OpenGL,不崩溃
- [x] 链接边界已最小化:`oakgl` / `oakvulkan` 现在只链接独立的 `libolive-rendercore`,不再拉入完整 editor 代码;库体积从约 21 MB 降至约 600 KB。
- [x] Vulkan viewer 完整 readback display 路径已实现(offscreen texture → download → QImage → QPainter)。
- [x] OpenFX 插件渲染边界已处理:`PluginRenderer` 后端无关化,非 OpenGL 渲染器自动回退 CPU 路径,动态 OpenGL 后端通过 C ABI 支持 OFX OpenGL 输出绑定。
- [ ] 手工测试计划覆盖 viewer、proxy、scope、导出等完整路径。
+27 -2
View File
@@ -22,6 +22,28 @@ TEST(DynamicRenderBackend, LoadsExperimentalOpenGLBackend)
#endif #endif
} }
TEST(DynamicRenderBackend, LoadsExperimentalVulkanBackendWhenAvailable)
{
#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";
}
EXPECT_EQ(renderer.backend_name(), QStringLiteral("vulkan"));
EXPECT_EQ(renderer.OpenGLContext(), nullptr);
EXPECT_STREQ(info.name, "vulkan");
EXPECT_TRUE(info.capabilities & OAK_RENDER_BACKEND_CAP_TEXTURES);
EXPECT_TRUE(info.capabilities & OAK_RENDER_BACKEND_CAP_SHADERS);
EXPECT_TRUE(info.capabilities & OAK_RENDER_BACKEND_CAP_BLIT);
EXPECT_TRUE(info.capabilities & OAK_RENDER_BACKEND_CAP_READBACK);
#endif
}
TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable) TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable)
{ {
#ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
@@ -29,10 +51,13 @@ TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable)
#else #else
olive::DynamicRenderer renderer(QStringLiteral("vulkan")); olive::DynamicRenderer renderer(QStringLiteral("vulkan"));
ASSERT_TRUE(renderer.Load()); ASSERT_TRUE(renderer.Load());
EXPECT_EQ(renderer.backend_name(), QStringLiteral("opengl"));
EXPECT_EQ(renderer.OpenGLContext(), nullptr);
OakRenderBackendInfo info = {}; OakRenderBackendInfo info = {};
ASSERT_TRUE(renderer.GetBackendInfo(&info)); ASSERT_TRUE(renderer.GetBackendInfo(&info));
if (info.kind == OAK_RENDER_BACKEND_VULKAN) {
GTEST_SKIP() << "Vulkan backend is available on this system; skip fallback test";
}
EXPECT_EQ(renderer.backend_name(), QStringLiteral("opengl"));
EXPECT_EQ(renderer.OpenGLContext(), nullptr);
EXPECT_EQ(info.kind, OAK_RENDER_BACKEND_OPENGL); EXPECT_EQ(info.kind, OAK_RENDER_BACKEND_OPENGL);
EXPECT_STREQ(info.name, "opengl"); EXPECT_STREQ(info.name, "opengl");
#endif #endif
+1 -1
View File
@@ -119,7 +119,7 @@ TEST(PluginIntegration, ChromaKeyerCreateAndRender)
olive::plugin::PluginJob job(instance, nullptr, row); olive::plugin::PluginJob job(instance, nullptr, row);
olive::TexturePtr output = std::make_shared<olive::Texture>(params); olive::TexturePtr output = std::make_shared<olive::Texture>(params);
olive::plugin::PluginRenderer renderer; olive::plugin::PluginRenderer renderer(nullptr);
renderer.RenderPlugin(input, job, output, params, true, false); renderer.RenderPlugin(input, job, output, params, true, false);
EXPECT_TRUE(output->frame()); EXPECT_TRUE(output->frame());
+1 -1
View File
@@ -190,7 +190,7 @@ bool RenderPlugin(const std::string &plugin_id,
PluginJob job(instance, nullptr, inputs); PluginJob job(instance, nullptr, inputs);
TexturePtr output = std::make_shared<Texture>(params); TexturePtr output = std::make_shared<Texture>(params);
PluginRenderer renderer; PluginRenderer renderer(nullptr);
renderer.RenderPlugin(nullptr, job, output, params, true, false); renderer.RenderPlugin(nullptr, job, output, params, true, false);
bool has_frame = output->frame() != nullptr; bool has_frame = output->frame() != nullptr;