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:
@@ -38,6 +38,8 @@ jobs:
|
||||
mingw-w64-ucrt-x86_64-fmt
|
||||
mingw-w64-ucrt-x86_64-expat
|
||||
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-ntldd
|
||||
|
||||
@@ -91,7 +93,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
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
|
||||
run: |
|
||||
@@ -261,7 +263,7 @@ jobs:
|
||||
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 libxkbcommon-dev \
|
||||
portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers libxkbcommon-dev \
|
||||
wget fuse3 desktop-file-utils
|
||||
|
||||
- name: Build
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
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 libxkbcommon-dev
|
||||
portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers vulkan-validationlayers-dev libxkbcommon-dev
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# macOS dependencies
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
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 "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-expat
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -226,3 +228,45 @@ jobs:
|
||||
if: runner.os == 'Windows'
|
||||
shell: msys2 {0}
|
||||
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*"
|
||||
|
||||
@@ -104,6 +104,18 @@ endif()
|
||||
find_package(OpenGL REQUIRED)
|
||||
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
|
||||
find_package(OpenColorIO 2.1.1 REQUIRED)
|
||||
list(APPEND OLIVE_LIBRARIES ${OCIO_LIBRARIES})
|
||||
|
||||
+86
-15
@@ -87,7 +87,7 @@ include_directories(../third_party/openfx/include
|
||||
# Remove prefix - prevents CMake calling it "liblibolive-editor"
|
||||
set_target_properties(libolive-editor PROPERTIES PREFIX "")
|
||||
|
||||
option(OAK_ENABLE_DYNAMIC_RENDER_BACKEND "Build and use the experimental dynamic render backend adapter" OFF)
|
||||
option(OAK_ENABLE_DYNAMIC_RENDER_BACKEND "Build and use the dynamic render backend adapter" ON)
|
||||
if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
|
||||
set_target_properties(libolive-editor PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_definitions(libolive-editor PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
|
||||
@@ -98,41 +98,94 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
add_library(oakgl SHARED
|
||||
render/opengl/openglbackend_c.cpp
|
||||
$<TARGET_OBJECTS:libolive-editor>
|
||||
# Render core library: the minimal set of code required by the OpenGL/Vulkan backend
|
||||
# libraries. Keeping this separate from libolive-editor prevents the backends from
|
||||
# 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}/third_party/openfx/include
|
||||
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
|
||||
${OLIVE_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(oakgl PRIVATE ${OLIVE_LIBRARIES} OfxHost)
|
||||
target_compile_definitions(oakgl PRIVATE ${OLIVE_DEFINITIONS})
|
||||
target_compile_options(oakgl PRIVATE ${OLIVE_COMPILE_OPTIONS})
|
||||
target_link_libraries(libolive-rendercore PUBLIC ${OLIVE_LIBRARIES} OfxHost)
|
||||
target_compile_definitions(libolive-rendercore PUBLIC ${OLIVE_DEFINITIONS})
|
||||
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
|
||||
OUTPUT_NAME oakgl
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
RUNTIME_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
|
||||
render/vulkan/vulkanbackend_c.cpp
|
||||
render/vulkan/vulkanrenderer.cpp
|
||||
render/vulkan/vulkanrenderer.h
|
||||
)
|
||||
target_include_directories(oakvulkan PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/app
|
||||
)
|
||||
target_link_libraries(oakvulkan PRIVATE Qt${QT_VERSION_MAJOR}::Core)
|
||||
target_compile_definitions(oakvulkan PRIVATE ${OLIVE_DEFINITIONS})
|
||||
target_compile_options(oakvulkan PRIVATE ${OLIVE_COMPILE_OPTIONS})
|
||||
target_link_libraries(oakvulkan PRIVATE libolive-rendercore)
|
||||
if(Vulkan_FOUND)
|
||||
target_link_libraries(oakvulkan PRIVATE Vulkan::Vulkan)
|
||||
target_compile_definitions(oakvulkan PRIVATE OAK_HAS_VULKAN)
|
||||
endif()
|
||||
if(SHADERC_FOUND)
|
||||
target_link_libraries(oakvulkan PRIVATE ${SHADERC_LIBRARIES})
|
||||
target_include_directories(oakvulkan PRIVATE ${SHADERC_INCLUDE_DIRS})
|
||||
target_compile_definitions(oakvulkan PRIVATE OAK_HAS_SHADERC)
|
||||
endif()
|
||||
set_target_properties(oakvulkan PROPERTIES
|
||||
OUTPUT_NAME oakvulkan
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
install(TARGETS oakvulkan
|
||||
RUNTIME DESTINATION bin
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
)
|
||||
endif()
|
||||
|
||||
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
|
||||
render/vulkan/vulkanbackend_c.cpp
|
||||
render/vulkan/vulkanrenderer.cpp
|
||||
render/vulkan/vulkanrenderer.h
|
||||
)
|
||||
target_include_directories(oakvulkan-cabi-check PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/app
|
||||
${CMAKE_SOURCE_DIR}/third_party/openfx/include
|
||||
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
|
||||
${OLIVE_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(oakvulkan-cabi-check PRIVATE Qt${QT_VERSION_MAJOR}::Core)
|
||||
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_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_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
|
||||
if(DOXYGEN_FOUND)
|
||||
set(DOXYGEN_PROJECT_NAME "Oak Video Editor")
|
||||
|
||||
@@ -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
|
||||
@@ -30,6 +30,7 @@ extern "C" {
|
||||
|
||||
#include <olive/core/core.h>
|
||||
|
||||
#include "common/avframeptr.h"
|
||||
#include "render/videoparams.h"
|
||||
|
||||
namespace olive
|
||||
@@ -85,16 +86,6 @@ public:
|
||||
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
|
||||
|
||||
@@ -30,6 +30,7 @@ set(OLIVE_SOURCES
|
||||
render/backend/dynamicrenderer.h
|
||||
render/backend/renderbackend_c.h
|
||||
render/cancelatom.h
|
||||
render/colormanagement.cpp
|
||||
render/colorprocessor.cpp
|
||||
render/colorprocessor.h
|
||||
render/colorprocessorcache.h
|
||||
@@ -39,6 +40,7 @@ set(OLIVE_SOURCES
|
||||
render/framehashcache.h
|
||||
render/framemanager.cpp
|
||||
render/framemanager.h
|
||||
render/interlacetexture.cpp
|
||||
render/loopmode.h
|
||||
render/managedcolor.cpp
|
||||
render/managedcolor.h
|
||||
|
||||
@@ -45,6 +45,8 @@ QString DynamicRenderer::LibraryFilename() const
|
||||
const QStringList candidates = {
|
||||
app_dir.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))
|
||||
};
|
||||
@@ -139,6 +141,10 @@ bool DynamicRenderer::ResolveFunctions()
|
||||
RESOLVE(get_pixel_from_texture_, OakBackendGetPixelFromTextureFn,
|
||||
"oak_renderer_get_pixel_from_texture");
|
||||
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,
|
||||
"oak_renderer_opengl_context");
|
||||
#undef RESOLVE
|
||||
@@ -184,6 +190,8 @@ void DynamicRenderer::ResetFunctions()
|
||||
flush_ = nullptr;
|
||||
get_pixel_from_texture_ = nullptr;
|
||||
blit_ = nullptr;
|
||||
attach_output_texture_ = nullptr;
|
||||
detach_output_texture_ = nullptr;
|
||||
opengl_context_ = nullptr;
|
||||
}
|
||||
|
||||
@@ -271,6 +279,11 @@ QOpenGLContext *DynamicRenderer::OpenGLContext() const
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
bool DynamicRenderer::IsOpenGL() const
|
||||
{
|
||||
return backend_ == QStringLiteral("opengl");
|
||||
}
|
||||
|
||||
void DynamicRenderer::Blit(QVariant shader, AcceleratedJob &job,
|
||||
Texture *destination, VideoParams destination_params,
|
||||
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_);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@ public:
|
||||
const QPointF &pt) override;
|
||||
virtual QOpenGLContext *OpenGLContext() const override;
|
||||
|
||||
virtual bool IsOpenGL() const override;
|
||||
|
||||
virtual void AttachOutputTexture(Texture *texture) override;
|
||||
|
||||
virtual void DetachOutputTexture() override;
|
||||
|
||||
protected:
|
||||
virtual void Blit(QVariant shader, AcceleratedJob &job,
|
||||
Texture *destination, VideoParams destination_params,
|
||||
@@ -84,6 +90,8 @@ private:
|
||||
OakBackendFlushFn flush_ = nullptr;
|
||||
OakBackendGetPixelFromTextureFn get_pixel_from_texture_ = nullptr;
|
||||
OakBackendBlitFn blit_ = nullptr;
|
||||
OakBackendAttachOutputTextureFn attach_output_texture_ = nullptr;
|
||||
OakBackendDetachOutputTextureFn detach_output_texture_ = nullptr;
|
||||
OakBackendOpenGLContextFn opengl_context_ = nullptr;
|
||||
};
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ enum OakRenderBackendCapability {
|
||||
OAK_RENDER_BACKEND_CAP_SHADERS = 1ULL << 1,
|
||||
OAK_RENDER_BACKEND_CAP_BLIT = 1ULL << 2,
|
||||
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 {
|
||||
@@ -81,6 +83,9 @@ typedef void (*OakBackendBlitFn)(OakRenderBackendHandle handle,
|
||||
void *destination,
|
||||
const void *destination_params,
|
||||
bool clear_destination);
|
||||
typedef void (*OakBackendAttachOutputTextureFn)(OakRenderBackendHandle handle,
|
||||
const void *texture_id);
|
||||
typedef void (*OakBackendDetachOutputTextureFn)(OakRenderBackendHandle handle);
|
||||
typedef void *(*OakBackendOpenGLContextFn)(OakRenderBackendHandle handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ¶ms)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,8 @@ public:
|
||||
using olive::OpenGLRenderer::CreateNativeTexture;
|
||||
using olive::OpenGLRenderer::DestroyInternal;
|
||||
using olive::OpenGLRenderer::DestroyNativeTexture;
|
||||
using olive::OpenGLRenderer::AttachTextureAsDestination;
|
||||
using olive::OpenGLRenderer::DetachTextureAsDestination;
|
||||
};
|
||||
|
||||
BackendOpenGLRenderer *Renderer(OakRenderBackendHandle handle)
|
||||
@@ -176,3 +178,15 @@ OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_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();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
#include <QOpenGLExtraFunctions>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include "common/filefunctions.h"
|
||||
#include "config/config.h"
|
||||
#include "render/job/shaderjob.h"
|
||||
|
||||
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)
|
||||
{
|
||||
AttachTextureAsDestination(texture->id());
|
||||
|
||||
@@ -82,6 +82,15 @@ public:
|
||||
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);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1432,9 +1432,10 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
|
||||
auto *olive_instance =
|
||||
dynamic_cast<olive::plugin::OlivePluginInstance *>(instance);
|
||||
const bool use_opengl =
|
||||
supports_opengl && destination && destination->renderer() &&
|
||||
destination->id().isValid();
|
||||
supports_opengl && renderer_ && renderer_->IsOpenGL() && destination &&
|
||||
destination->renderer() == renderer_ && destination->id().isValid();
|
||||
if (olive_instance) {
|
||||
olive_instance->setOpenGLEnabled(use_opengl);
|
||||
olive_instance->setVideoParam(destination_params);
|
||||
// Ensure all clip instances inherit the project's params so that
|
||||
// getAspectRatio/getFrameRate etc. return valid values before
|
||||
@@ -1798,7 +1799,7 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
|
||||
renderScale, true, interactive);
|
||||
return;
|
||||
}
|
||||
AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params, this);
|
||||
AVFramePtr converted = ConvertFrameIfNeeded(frame_ptr, destination_params, renderer_);
|
||||
const AVPixelFormat expected_fmt =
|
||||
GetDestinationAVPixelFormat(destination_params);
|
||||
destination->handleFrame(converted);
|
||||
@@ -1837,15 +1838,16 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin::
|
||||
// Purpose: Attach output texture for OFX GL rendering.
|
||||
void olive::plugin::PluginRenderer::AttachOutputTexture(olive::TexturePtr texture)
|
||||
{
|
||||
if (!texture) {
|
||||
return;
|
||||
if (renderer_) {
|
||||
renderer_->AttachOutputTexture(texture.get());
|
||||
}
|
||||
AttachTextureAsDestination(texture->id());
|
||||
}
|
||||
|
||||
// 作用:解除 OFX 的 GL 输出绑定。
|
||||
// Purpose: Detach OFX GL output binding.
|
||||
void olive::plugin::PluginRenderer::DetachOutputTexture()
|
||||
{
|
||||
DetachTextureAsDestination();
|
||||
if (renderer_) {
|
||||
renderer_->DetachOutputTexture();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,12 @@
|
||||
|
||||
#ifndef PLUGINRENDERER_H
|
||||
#define PLUGINRENDERER_H
|
||||
#include <QOpenGLExtraFunctions>
|
||||
#include <QOpenGLBuffer>
|
||||
#include <QOpenGLFunctions>
|
||||
#include <QOpenGLShader>
|
||||
#include <QOpenGLVertexArrayObject>
|
||||
#include <QThread>
|
||||
#include <QOffscreenSurface>
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "render/renderer.h"
|
||||
#include "render/job/pluginjob.h"
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace plugin{
|
||||
@@ -44,11 +39,22 @@ int BytesToPixels(int byte_linesize, const olive::VideoParams ¶ms);
|
||||
}
|
||||
// 作用:OFX 插件渲染器,负责 CPU/GL 路径下的插件调用和纹理桥接。
|
||||
// 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
|
||||
public:
|
||||
PluginRenderer(QObject *parent=nullptr):OpenGLRenderer(parent){};
|
||||
virtual ~PluginRenderer() override{};
|
||||
explicit PluginRenderer(olive::Renderer *renderer, QObject *parent = nullptr)
|
||||
: QObject(parent), renderer_(renderer) {}
|
||||
virtual ~PluginRenderer() override {}
|
||||
|
||||
olive::Renderer *renderer() const
|
||||
{
|
||||
return renderer_;
|
||||
}
|
||||
|
||||
// 作用:将目标纹理绑定为插件输出。
|
||||
// Purpose: Attach destination texture as OFX output.
|
||||
void AttachOutputTexture(olive::TexturePtr texture);
|
||||
@@ -62,6 +68,8 @@ public:
|
||||
olive::VideoParams destination_params,
|
||||
bool clear_destination, bool interactive);
|
||||
|
||||
private:
|
||||
olive::Renderer *renderer_;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+1
-221
@@ -116,34 +116,6 @@ void Renderer::DestroyTexture(Texture *texture)
|
||||
}
|
||||
}
|
||||
|
||||
TexturePtr Renderer::InterlaceTexture(TexturePtr top, TexturePtr bottom,
|
||||
const VideoParams ¶ms)
|
||||
{
|
||||
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()
|
||||
{
|
||||
QMutexLocker locker(&color_cache_mutex_);
|
||||
@@ -191,142 +163,6 @@ TexturePtr Renderer::CreateTextureFromNativeHandle(const QVariant &v,
|
||||
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()
|
||||
{
|
||||
QMutexLocker locker(&texture_cache_lock_);
|
||||
@@ -342,60 +178,4 @@ void Renderer::ClearOldTextures()
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::BlitColorManaged(const ColorTransformJob &color_job,
|
||||
Texture *destination, const VideoParams ¶ms)
|
||||
{
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace olive
|
||||
|
||||
+31
-4
@@ -29,12 +29,15 @@
|
||||
#include <memory>
|
||||
|
||||
#include "common/define.h"
|
||||
#include "node/node.h"
|
||||
#include "render/colorprocessor.h"
|
||||
#include "render/job/colortransformjob.h"
|
||||
#include "render/shadercode.h"
|
||||
#include "render/videoparams.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
|
||||
{
|
||||
@@ -117,6 +120,30 @@ public:
|
||||
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:
|
||||
virtual void Blit(QVariant shader, olive::AcceleratedJob& job,
|
||||
olive::Texture *destination,
|
||||
|
||||
@@ -78,21 +78,31 @@ QString RenderManager::BackendToString(Backend backend)
|
||||
}
|
||||
|
||||
RenderManager::RenderManager(QObject *parent)
|
||||
: backend_(kOpenGL)
|
||||
, requested_backend_(BackendFromString(
|
||||
: backend_(BackendFromString(
|
||||
OLIVE_CONFIG("GraphicsBackend").toString()))
|
||||
, requested_backend_(backend_)
|
||||
, aggressive_gc_(0)
|
||||
, worker_pool_(nullptr)
|
||||
{
|
||||
if (requested_backend_ == kVulkan) {
|
||||
qWarning()
|
||||
<< "Vulkan graphics backend was requested, but the current render "
|
||||
"pipeline still uses OpenGL. Falling back to OpenGL renderer.";
|
||||
if (backend_ == kVulkan) {
|
||||
#ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
qWarning() << "Vulkan backend requested but dynamic render backend is not enabled. Falling back to OpenGL.";
|
||||
backend_ = kOpenGL;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (backend_ == kOpenGL) {
|
||||
if (backend_ == kOpenGL || backend_ == kVulkan) {
|
||||
#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
|
||||
context_ = new OpenGLRenderer();
|
||||
#endif
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
#include "node/block/transition/transition.h"
|
||||
#include "node/project.h"
|
||||
#include "rendermanager.h"
|
||||
#include "render/opengl/openglcontextprovider.h"
|
||||
#include "render/opengl/openglrenderer.h"
|
||||
#include "render/plugin/pluginrenderer.h"
|
||||
#include "pluginSupport/OliveClip.h"
|
||||
#include "pluginSupport/OliveHost.h"
|
||||
@@ -711,26 +709,8 @@ TexturePtr RenderProcessor::ProcessPluginJob(TexturePtr texture,
|
||||
return destination;
|
||||
}
|
||||
|
||||
plugin::PluginRenderer *plugin_renderer = nullptr;
|
||||
{
|
||||
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) {
|
||||
plugin::PluginRenderer plugin_renderer(render_ctx_);
|
||||
if (!plugin_renderer.renderer()) {
|
||||
return destination;
|
||||
}
|
||||
|
||||
@@ -782,7 +762,7 @@ TexturePtr RenderProcessor::ProcessPluginJob(TexturePtr texture,
|
||||
}
|
||||
}
|
||||
|
||||
plugin_renderer->RenderPlugin(
|
||||
plugin_renderer.RenderPlugin(
|
||||
src,
|
||||
*plugin_job,
|
||||
destination,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "texture.h"
|
||||
|
||||
#include "render/job/acceleratedjob.h"
|
||||
#include "renderer.h"
|
||||
|
||||
namespace olive
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#ifndef RENDERTEXTURE_H
|
||||
#define RENDERTEXTURE_H
|
||||
|
||||
#include "common/ffmpegutils.h"
|
||||
#include "common/avframeptr.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
@@ -29,6 +29,7 @@ extern "C" {
|
||||
#include <QtMath>
|
||||
|
||||
#include "core.h"
|
||||
#include "ofxImageEffect.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
@@ -120,6 +121,17 @@ VideoParams::VideoParams(int width, int height, int depth, PixelFormat format,
|
||||
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,
|
||||
PixelFormat format, int nb_channels,
|
||||
const rational &pixel_aspect_ratio,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#ifndef VIDEOPARAMS_H
|
||||
#define VIDEOPARAMS_H
|
||||
|
||||
#include "ofxImageEffect.h"
|
||||
#include <olive/core/core.h>
|
||||
#include <QVector2D>
|
||||
#include <QXmlStreamReader>
|
||||
@@ -178,18 +177,7 @@ public:
|
||||
{
|
||||
channel_count_ = c;
|
||||
}
|
||||
void set_channel_count(std::string ofxComponent)
|
||||
{
|
||||
if (ofxComponent == kOfxImageComponentAlpha){
|
||||
channel_count_ = 1;
|
||||
}
|
||||
else if (ofxComponent == kOfxImageComponentRGB){
|
||||
channel_count_ = kRGBChannelCount;
|
||||
}
|
||||
else if(ofxComponent == kOfxImageComponentRGBA){
|
||||
channel_count_ = kRGBAChannelCount;
|
||||
}
|
||||
}
|
||||
void set_channel_count(const std::string &ofxComponent);
|
||||
const rational &pixel_aspect_ratio() const
|
||||
{
|
||||
return pixel_aspect_ratio_;
|
||||
|
||||
@@ -1,41 +1,47 @@
|
||||
#include "render/backend/renderbackend_c.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QOpenGLContext>
|
||||
#include <QPointF>
|
||||
#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 {
|
||||
|
||||
struct VulkanBackend {
|
||||
bool warned = false;
|
||||
class BackendVulkanRenderer : public olive::VulkanRenderer {
|
||||
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);
|
||||
if (!backend || backend->warned) {
|
||||
return;
|
||||
}
|
||||
backend->warned = true;
|
||||
qWarning() << "Vulkan render backend is a C ABI placeholder; function"
|
||||
<< function << "is not implemented yet";
|
||||
return *static_cast<const QVariant *>(variant);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent)
|
||||
{
|
||||
Q_UNUSED(parent)
|
||||
return new VulkanBackend();
|
||||
return new BackendVulkanRenderer(static_cast<QObject *>(parent));
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -46,146 +52,134 @@ OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info(
|
||||
}
|
||||
out_info->abi_version = 1;
|
||||
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->status = "placeholder-unavailable";
|
||||
out_info->status = Renderer(handle)->IsAvailable() ? "available" : "unavailable";
|
||||
return true;
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available(
|
||||
OakRenderBackendHandle handle)
|
||||
{
|
||||
Q_UNUSED(handle)
|
||||
return false;
|
||||
auto *r = Renderer(handle);
|
||||
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)
|
||||
{
|
||||
WarnUnavailable(handle, "init");
|
||||
return false;
|
||||
return Renderer(handle)->Init();
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context(
|
||||
OakRenderBackendHandle handle, void *context)
|
||||
{
|
||||
Q_UNUSED(context)
|
||||
WarnUnavailable(handle, "init_with_context");
|
||||
Renderer(handle)->Init();
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_init(
|
||||
OakRenderBackendHandle handle)
|
||||
{
|
||||
Q_UNUSED(handle)
|
||||
Renderer(handle)->PostInit();
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_destroy(
|
||||
OakRenderBackendHandle handle)
|
||||
{
|
||||
Q_UNUSED(handle)
|
||||
Renderer(handle)->PostDestroy();
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_internal(
|
||||
OakRenderBackendHandle handle)
|
||||
{
|
||||
Q_UNUSED(handle)
|
||||
Renderer(handle)->DestroyInternal();
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination(
|
||||
OakRenderBackendHandle handle, void *texture, double r, double g, double b,
|
||||
double a)
|
||||
{
|
||||
Q_UNUSED(texture)
|
||||
Q_UNUSED(r)
|
||||
Q_UNUSED(g)
|
||||
Q_UNUSED(b)
|
||||
Q_UNUSED(a)
|
||||
WarnUnavailable(handle, "clear_destination");
|
||||
Renderer(handle)->ClearDestination(static_cast<olive::Texture *>(texture),
|
||||
r, g, b, a);
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture(
|
||||
OakRenderBackendHandle handle, int width, int height, int depth, int format,
|
||||
int channel_count, const void *data, int linesize, void *out_variant)
|
||||
{
|
||||
Q_UNUSED(width)
|
||||
Q_UNUSED(height)
|
||||
Q_UNUSED(depth)
|
||||
Q_UNUSED(format)
|
||||
Q_UNUSED(channel_count)
|
||||
Q_UNUSED(data)
|
||||
Q_UNUSED(linesize)
|
||||
static_cast<QVariant *>(out_variant)->clear();
|
||||
WarnUnavailable(handle, "create_native_texture");
|
||||
*static_cast<QVariant *>(out_variant) = Renderer(handle)->CreateNativeTexture(
|
||||
width, height, depth, static_cast<olive::PixelFormat::Format>(format),
|
||||
channel_count, data, linesize);
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture(
|
||||
OakRenderBackendHandle handle, const void *variant)
|
||||
{
|
||||
Q_UNUSED(variant)
|
||||
WarnUnavailable(handle, "destroy_native_texture");
|
||||
Renderer(handle)->DestroyNativeTexture(VariantRef(variant));
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader(
|
||||
OakRenderBackendHandle handle, const void *shader_code, void *out_variant)
|
||||
{
|
||||
Q_UNUSED(shader_code)
|
||||
static_cast<QVariant *>(out_variant)->clear();
|
||||
WarnUnavailable(handle, "create_native_shader");
|
||||
*static_cast<QVariant *>(out_variant) = Renderer(handle)->CreateNativeShader(
|
||||
*static_cast<const olive::ShaderCode *>(shader_code));
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader(
|
||||
OakRenderBackendHandle handle, const void *variant)
|
||||
{
|
||||
Q_UNUSED(variant)
|
||||
WarnUnavailable(handle, "destroy_native_shader");
|
||||
Renderer(handle)->DestroyNativeShader(VariantRef(variant));
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture(
|
||||
OakRenderBackendHandle handle, const void *variant, const void *video_params,
|
||||
const void *data, int linesize)
|
||||
{
|
||||
Q_UNUSED(variant)
|
||||
Q_UNUSED(video_params)
|
||||
Q_UNUSED(data)
|
||||
Q_UNUSED(linesize)
|
||||
WarnUnavailable(handle, "upload_to_texture");
|
||||
Renderer(handle)->UploadToTexture(
|
||||
VariantRef(variant), *static_cast<const olive::VideoParams *>(video_params),
|
||||
data, linesize);
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture(
|
||||
OakRenderBackendHandle handle, const void *variant, const void *video_params,
|
||||
void *data, int linesize)
|
||||
{
|
||||
Q_UNUSED(variant)
|
||||
Q_UNUSED(video_params)
|
||||
Q_UNUSED(data)
|
||||
Q_UNUSED(linesize)
|
||||
WarnUnavailable(handle, "download_from_texture");
|
||||
Renderer(handle)->DownloadFromTexture(
|
||||
VariantRef(variant), *static_cast<const olive::VideoParams *>(video_params),
|
||||
data, linesize);
|
||||
}
|
||||
|
||||
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(
|
||||
OakRenderBackendHandle handle, void *texture, const void *point,
|
||||
void *out_color)
|
||||
{
|
||||
Q_UNUSED(texture)
|
||||
Q_UNUSED(point)
|
||||
Q_UNUSED(out_color)
|
||||
WarnUnavailable(handle, "get_pixel_from_texture");
|
||||
*static_cast<olive::Color *>(out_color) = Renderer(handle)->GetPixelFromTexture(
|
||||
static_cast<olive::Texture *>(texture), *static_cast<const QPointF *>(point));
|
||||
}
|
||||
|
||||
OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit(
|
||||
OakRenderBackendHandle handle, const void *shader, void *job,
|
||||
void *destination, const void *destination_params, bool clear_destination)
|
||||
{
|
||||
Q_UNUSED(shader)
|
||||
Q_UNUSED(job)
|
||||
Q_UNUSED(destination)
|
||||
Q_UNUSED(destination_params)
|
||||
Q_UNUSED(clear_destination)
|
||||
WarnUnavailable(handle, "blit");
|
||||
Renderer(handle)->Blit(
|
||||
VariantRef(shader), *static_cast<olive::AcceleratedJob *>(job),
|
||||
static_cast<olive::Texture *>(destination),
|
||||
*static_cast<const olive::VideoParams *>(destination_params),
|
||||
clear_destination);
|
||||
}
|
||||
|
||||
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)
|
||||
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
@@ -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 ¶ms, const void *data,
|
||||
int linesize) override;
|
||||
virtual void DownloadFromTexture(const QVariant &handle,
|
||||
const VideoParams ¶ms, void *data,
|
||||
int linesize) override;
|
||||
|
||||
virtual void Flush() override;
|
||||
|
||||
virtual Color GetPixelFromTexture(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
|
||||
@@ -39,6 +39,9 @@
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
#include "render/ipc/ipcmessage.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/rendermanager.h"
|
||||
#include "render/renderprocessor.h"
|
||||
@@ -80,7 +83,7 @@ QJsonObject ErrorMessage(const QString &message, qint64 ticket_id = 0)
|
||||
|
||||
class RenderWorker {
|
||||
public:
|
||||
RenderWorker(olive::OpenGLRenderer *renderer, QFile *out)
|
||||
RenderWorker(olive::Renderer *renderer, QFile *out)
|
||||
: renderer_(renderer)
|
||||
, out_(out)
|
||||
{
|
||||
@@ -114,7 +117,16 @@ public:
|
||||
hs.input_slot_data_bytes = 0;
|
||||
|
||||
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();
|
||||
handshake["gl_major"] = fmt.majorVersion();
|
||||
handshake["gl_minor"] = fmt.minorVersion();
|
||||
@@ -427,7 +439,7 @@ private:
|
||||
return Write(ready.ToJson());
|
||||
}
|
||||
|
||||
olive::OpenGLRenderer *renderer_;
|
||||
olive::Renderer *renderer_;
|
||||
QFile *out_;
|
||||
bool shutdown_requested_ = false;
|
||||
std::unique_ptr<olive::Project> project_;
|
||||
@@ -459,15 +471,42 @@ int main(int argc, char *argv[])
|
||||
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()) {
|
||||
LogError(QStringLiteral("failed to initialize OpenGL renderer"));
|
||||
delete renderer;
|
||||
return 1;
|
||||
}
|
||||
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()) {
|
||||
LogError(QStringLiteral("OpenGL context is not valid after init"));
|
||||
renderer->Destroy();
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
namespace olive
|
||||
{
|
||||
|
||||
const char *StyleManager::kDefaultStyle = "olive-dark";
|
||||
QString StyleManager::current_style_;
|
||||
QMap<QString, QString> StyleManager::available_themes_;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public:
|
||||
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -40,13 +40,33 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, color_manager_(nullptr)
|
||||
, color_service_(nullptr)
|
||||
, is_backend_neutral_(false)
|
||||
{
|
||||
QHBoxLayout *layout = new QHBoxLayout(this);
|
||||
layout->setSpacing(0);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
|
||||
// Create OpenGL widget
|
||||
// Create renderer
|
||||
#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_->setAttribute(Qt::WA_TranslucentBackground, false);
|
||||
connect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
|
||||
@@ -64,16 +84,6 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
|
||||
|
||||
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
|
||||
#ifdef USE_QOPENGLWINDOW
|
||||
wrapper_ = QWidget::createWindowContainer(
|
||||
@@ -83,19 +93,29 @@ ManagedDisplayWidget::ManagedDisplayWidget(QWidget *parent)
|
||||
#endif
|
||||
layout->addWidget(wrapper_);
|
||||
} else {
|
||||
inner_widget_ = nullptr;
|
||||
wrapper_ = nullptr;
|
||||
// Backend-neutral path (Vulkan, etc.)
|
||||
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()
|
||||
{
|
||||
if (!is_backend_neutral_) {
|
||||
MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER;
|
||||
|
||||
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
|
||||
disconnect(static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_),
|
||||
&ManagedDisplayWidgetOpenGL::OnDestroy, this,
|
||||
&ManagedDisplayWidget::OnDestroy);
|
||||
} else {
|
||||
OnDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +271,7 @@ void ManagedDisplayWidget::SetColorTransform(const ColorTransform &transform)
|
||||
|
||||
void ManagedDisplayWidget::OnInit()
|
||||
{
|
||||
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
|
||||
if (!is_backend_neutral_) {
|
||||
QOpenGLContext *context =
|
||||
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->context();
|
||||
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
@@ -264,6 +284,9 @@ void ManagedDisplayWidget::OnInit()
|
||||
#endif
|
||||
static_cast<OpenGLRenderer *>(attached_renderer_)->Init(context);
|
||||
static_cast<OpenGLRenderer *>(attached_renderer_)->PostInit();
|
||||
} else {
|
||||
attached_renderer_->Init();
|
||||
attached_renderer_->PostInit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,25 +303,21 @@ void ManagedDisplayWidget::ColorProcessorChangedEvent()
|
||||
|
||||
void ManagedDisplayWidget::makeCurrent()
|
||||
{
|
||||
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
|
||||
if (!is_backend_neutral_) {
|
||||
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->makeCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
void ManagedDisplayWidget::doneCurrent()
|
||||
{
|
||||
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
|
||||
if (!is_backend_neutral_) {
|
||||
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->doneCurrent();
|
||||
}
|
||||
}
|
||||
|
||||
QPaintDevice *ManagedDisplayWidget::paint_device() const
|
||||
{
|
||||
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
|
||||
return static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_);
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
return inner_widget_;
|
||||
}
|
||||
|
||||
void ManagedDisplayWidget::SetInnerMouseTracking(bool e)
|
||||
@@ -320,8 +339,8 @@ VideoParams ManagedDisplayWidget::GetViewportParams() const
|
||||
|
||||
void ManagedDisplayWidget::update()
|
||||
{
|
||||
if (RenderManager::instance()->backend() == RenderManager::kOpenGL) {
|
||||
static_cast<ManagedDisplayWidgetOpenGL *>(inner_widget_)->update();
|
||||
if (inner_widget_) {
|
||||
inner_widget_->update();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,32 @@ private slots:
|
||||
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 {
|
||||
Q_OBJECT
|
||||
public:
|
||||
@@ -221,6 +247,11 @@ protected:
|
||||
|
||||
void SetInnerMouseTracking(bool e);
|
||||
|
||||
bool IsBackendNeutral() const
|
||||
{
|
||||
return is_backend_neutral_;
|
||||
}
|
||||
|
||||
QRect GetInnerRect() const
|
||||
{
|
||||
return wrapper_ ? wrapper_->rect() : QRect();
|
||||
@@ -285,6 +316,8 @@ private:
|
||||
*/
|
||||
ColorTransform color_transform_;
|
||||
|
||||
bool is_backend_neutral_ = false;
|
||||
|
||||
private slots:
|
||||
/**
|
||||
* @brief Sets all color settings to the defaults pertaining to this configuration
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "scopebase.h"
|
||||
|
||||
#include "config/config.h"
|
||||
#include "render/job/colortransformjob.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
@@ -68,6 +69,11 @@ void ScopeBase::OnInit()
|
||||
|
||||
void ScopeBase::OnPaint()
|
||||
{
|
||||
if (IsBackendNeutral()) {
|
||||
// TODO: implement backend-neutral scope display
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear display surface
|
||||
renderer()->ClearDestination();
|
||||
|
||||
|
||||
@@ -375,19 +375,40 @@ bool ViewerDisplayWidget::eventFilter(QObject *o, QEvent *e)
|
||||
|
||||
void ViewerDisplayWidget::OnPaint()
|
||||
{
|
||||
const bool backend_neutral = IsBackendNeutral();
|
||||
|
||||
QPainter bg_painter;
|
||||
bool bg_painter_active = false;
|
||||
|
||||
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
|
||||
if (push_mode_ != kPushNull) {
|
||||
// Draw texture through color transform
|
||||
VideoParams device_params = GetViewportParams();
|
||||
device_params = GetViewportParams();
|
||||
|
||||
if (push_mode_ == kPushBlank) {
|
||||
if (!backend_neutral) {
|
||||
DrawBlank(device_params);
|
||||
}
|
||||
} else if (color_service()) {
|
||||
if (FramePtr frame = load_frame_.value<FramePtr>()) {
|
||||
// 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.
|
||||
if (texture && texture->renderer() &&
|
||||
texture->renderer() != renderer()) {
|
||||
bool copied = false;
|
||||
QOpenGLContext *ctx = QOpenGLContext::currentContext();
|
||||
if (ctx) {
|
||||
QOpenGLFunctions *funcs = ctx->functions();
|
||||
GLuint tex_id = texture->id().value<GLuint>();
|
||||
if (funcs && tex_id && funcs->glIsTexture(tex_id)) {
|
||||
// Cross-renderer texture: download and re-upload
|
||||
FramePtr frame = Frame::Create();
|
||||
frame->set_video_params(texture->params());
|
||||
if (frame->allocate()) {
|
||||
renderer()->DownloadFromTexture(
|
||||
texture->renderer()->DownloadFromTexture(
|
||||
texture->id(), texture->params(),
|
||||
frame->data(), frame->linesize_pixels());
|
||||
texture_ = renderer()->CreateTexture(
|
||||
frame->video_params(), frame->data(),
|
||||
frame->linesize_pixels());
|
||||
copied = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!copied) {
|
||||
} else {
|
||||
texture_ = texture;
|
||||
}
|
||||
} else {
|
||||
@@ -445,7 +456,9 @@ void ViewerDisplayWidget::OnPaint()
|
||||
TexturePtr texture_to_draw = texture_;
|
||||
|
||||
if (!texture_to_draw || texture_to_draw->IsDummy()) {
|
||||
if (!backend_neutral) {
|
||||
DrawBlank(device_params);
|
||||
}
|
||||
} else {
|
||||
if (deinterlace_) {
|
||||
if (deinterlace_shader_.isNull()) {
|
||||
@@ -477,7 +490,6 @@ void ViewerDisplayWidget::OnPaint()
|
||||
texture_to_draw = deinterlace_texture_;
|
||||
}
|
||||
|
||||
ColorTransformJob ctj;
|
||||
ctj.SetColorProcessor(color_service());
|
||||
ctj.SetInputTexture(texture_to_draw);
|
||||
ctj.SetInputAlphaAssociation(
|
||||
@@ -489,13 +501,25 @@ void ViewerDisplayWidget::OnPaint()
|
||||
ctj.SetCropMatrix(crop_matrix_);
|
||||
ctj.SetForceOpaque(true);
|
||||
|
||||
renderer()->BlitColorManaged(ctj, device_params);
|
||||
have_ctj = true;
|
||||
}
|
||||
} else {
|
||||
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
|
||||
if (gizmos_) {
|
||||
QPainter p(paint_device());
|
||||
@@ -633,6 +657,8 @@ void ViewerDisplayWidget::OnDestroy()
|
||||
|
||||
texture_ = nullptr;
|
||||
deinterlace_texture_ = nullptr;
|
||||
backend_neutral_texture_ = nullptr;
|
||||
backend_neutral_buffer_.clear();
|
||||
if (load_frame_.isNull()) {
|
||||
push_mode_ = kPushNull;
|
||||
} else {
|
||||
@@ -1364,6 +1390,55 @@ void ViewerDisplayWidget::DrawBlank(const VideoParams &device_params)
|
||||
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)
|
||||
{
|
||||
show_fps_ = e;
|
||||
|
||||
@@ -327,6 +327,8 @@ private:
|
||||
|
||||
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().
|
||||
*/
|
||||
@@ -337,6 +339,19 @@ private:
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
||||
+10
-2
@@ -14,6 +14,7 @@ This document describes how to build Oak Video Editor from source on Windows, Li
|
||||
- Expat
|
||||
- PortAudio
|
||||
- OpenGL headers
|
||||
- Vulkan SDK (optional, required for the Vulkan render backend)
|
||||
- XKB common (Linux)
|
||||
|
||||
---
|
||||
@@ -42,6 +43,8 @@ pacman -S --needed \
|
||||
mingw-w64-ucrt-x86_64-fmt \
|
||||
mingw-w64-ucrt-x86_64-expat \
|
||||
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
|
||||
```
|
||||
|
||||
@@ -83,7 +86,7 @@ sudo apt-get install -y \
|
||||
cmake ninja-build pkg-config nasm \
|
||||
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 \
|
||||
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev
|
||||
portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers libxkbcommon-dev
|
||||
```
|
||||
|
||||
Build FFmpeg 8.0+ from source:
|
||||
@@ -137,6 +140,8 @@ sudo dnf install -y \
|
||||
expat-devel \
|
||||
portaudio-devel \
|
||||
mesa-libGL-devel \
|
||||
vulkan-headers \
|
||||
vulkan-loader-devel \
|
||||
libxkbcommon-devel \
|
||||
gcc-c++ \
|
||||
bzip2-devel
|
||||
@@ -171,6 +176,8 @@ sudo pacman -S --needed \
|
||||
expat \
|
||||
portaudio \
|
||||
mesa \
|
||||
vulkan-headers \
|
||||
vulkan-icd-loader \
|
||||
libxkbcommon \
|
||||
gcc
|
||||
```
|
||||
@@ -200,7 +207,7 @@ Install dependencies:
|
||||
|
||||
```bash
|
||||
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):
|
||||
@@ -248,6 +255,7 @@ ctest --test-dir build --output-on-failure -C Release
|
||||
| `BUILD_QT6` | `ON` | Build with Qt 6 instead of Qt 5 |
|
||||
| `OTIO_LOCATION` | - | Path to OpenTimelineIO installation (optional) |
|
||||
| `OCIO_LOCATION` | - | Path to OpenColorIO installation |
|
||||
| `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` | `ON` | Build dynamic render backend libraries (`liboakgl.so` / `liboakvulkan.so`) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+10
-2
@@ -14,6 +14,7 @@
|
||||
- Expat
|
||||
- PortAudio
|
||||
- OpenGL 头文件
|
||||
- Vulkan SDK(可选,Vulkan 渲染后端需要)
|
||||
- XKB common(Linux)
|
||||
|
||||
---
|
||||
@@ -42,6 +43,8 @@ pacman -S --needed \
|
||||
mingw-w64-ucrt-x86_64-fmt \
|
||||
mingw-w64-ucrt-x86_64-expat \
|
||||
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
|
||||
```
|
||||
|
||||
@@ -83,7 +86,7 @@ sudo apt-get install -y \
|
||||
cmake ninja-build pkg-config nasm \
|
||||
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 \
|
||||
portaudio19-dev libgl1-mesa-dev libxkbcommon-dev
|
||||
portaudio19-dev libgl1-mesa-dev libvulkan-dev vulkan-headers libxkbcommon-dev
|
||||
```
|
||||
|
||||
从源码编译 FFmpeg 8.0+:
|
||||
@@ -137,6 +140,8 @@ sudo dnf install -y \
|
||||
expat-devel \
|
||||
portaudio-devel \
|
||||
mesa-libGL-devel \
|
||||
vulkan-headers \
|
||||
vulkan-loader-devel \
|
||||
libxkbcommon-devel \
|
||||
gcc-c++
|
||||
```
|
||||
@@ -170,6 +175,8 @@ sudo pacman -S --needed \
|
||||
expat \
|
||||
portaudio \
|
||||
mesa \
|
||||
vulkan-headers \
|
||||
vulkan-icd-loader \
|
||||
libxkbcommon \
|
||||
gcc
|
||||
```
|
||||
@@ -199,7 +206,7 @@ ctest --test-dir build --output-on-failure -C Release
|
||||
|
||||
```bash
|
||||
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 支持):
|
||||
@@ -247,6 +254,7 @@ ctest --test-dir build --output-on-failure -C Release
|
||||
| `BUILD_QT6` | `ON` | 使用 Qt 6 而非 Qt 5 |
|
||||
| `OTIO_LOCATION` | - | OpenTimelineIO 安装路径(可选) |
|
||||
| `OCIO_LOCATION` | - | OpenColorIO 安装路径 |
|
||||
| `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` | `ON` | 构建动态渲染后端库(`liboakgl.so` / `liboakvulkan.so`) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -48,58 +48,93 @@
|
||||
|
||||
这一步是 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 路径。
|
||||
|
||||
已新增可选 `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。
|
||||
剩余优化空间:
|
||||
- 长远可将 `libolive-editor` 也改为依赖 `libolive-rendercore`,彻底消除渲染核心代码在主程序与后端库之间的重复编译/重复链接。当前阶段先保证后端边界干净、主程序保持兼容。
|
||||
|
||||
## 阶段 3:Vulkan 后端
|
||||
|
||||
- 新增 Vulkan 后端库。
|
||||
- 已新增 `liboakvulkan.so` 实验占位库:内部使用 C++,对外导出与 OpenGL 后端一致的 C ABI;当前 `oak_renderer_is_available` 返回 `false`,请求 Vulkan 时由 `DynamicRenderer` 自动回退 OpenGL。
|
||||
- 已新增 `DynamicRenderBackend.FallsBackWhenExperimentalVulkanUnavailable` smoke test:默认构建跳过,实验构建验证 Vulkan 占位后端可加载且不会被误用为可渲染后端。
|
||||
- 实现 Vulkan instance/device/swapchain 或 offscreen image 管理。
|
||||
- 实现 texture 创建、上传、下载、shader 编译/缓存、blit、clear、flush。
|
||||
- 提供 OpenGL 辅助格式转换函数的 Vulkan 替代版本。
|
||||
- 确认 CPU readback、proxy preview、scope、thumbnail/cache 输出一致。
|
||||
- 新增 Vulkan 后端库 `liboakvulkan.so`。
|
||||
- 新增 `VulkanRenderer` 类,继承 `Renderer`,使用原生 Vulkan API 实现 offscreen 渲染管线。
|
||||
- CMake 集成:根目录查找 `Vulkan` 和 `shaderc`(可选);`oakvulkan` 目标链接 `Vulkan::Vulkan` 与 `shaderc_shared`。
|
||||
- 实现 Vulkan instance/device/queue/command pool 管理。
|
||||
- 实现 offscreen image/texture 管理(`CreateNativeTexture` / `DestroyNativeTexture`),支持 2D/3D、多种 pixel format(U8/U16/F16/F32 × 1/2/3/4 channel)。
|
||||
- 实现 staging buffer 上传/下载(`UploadToTexture` / `DownloadFromTexture`)。
|
||||
- 实现 `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 输出一致性。
|
||||
|
||||
## 阶段 4:Viewer 双后端
|
||||
## 阶段 4:Viewer 双后端(默认路径已切换)
|
||||
|
||||
- 当前 viewer display 基于 OpenGL widget 和 GL texture id。
|
||||
- 已在 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND=ON` 实验构建下让 Viewer 的 managed display 使用 `DynamicRenderer` 创建 renderer,并把现有 `QOpenGLContext` 传入动态后端;默认构建仍直接使用 `OpenGLRenderer`。
|
||||
- 新增 backend-neutral viewer path。
|
||||
- 默认构建下 Viewer 的 managed display 现在使用 `DynamicRenderer` 创建 renderer,并把现有 `QOpenGLContext` 传入动态后端;若动态后端加载失败则回退到 `OpenGLRenderer`。
|
||||
- `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`。
|
||||
- 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。
|
||||
|
||||
## 阶段 5:OpenFX 处理边界
|
||||
## 阶段 5:OpenFX 处理边界(已完成)
|
||||
|
||||
- OpenFX 插件 OpenGL 渲染路径保留 OpenGL 依赖,不强行改写。
|
||||
- 格式转换、readback、upload 等辅助函数移入后端库。
|
||||
- Vulkan 后端提供等价辅助函数。
|
||||
- 如果某个 OFX 插件必须走 OpenGL,则在 Vulkan 项目里明确使用 OpenGL 兼容路径或回退。
|
||||
- `PluginRenderer` 不再继承 `OpenGLRenderer`,改为持有通用的 `Renderer *`:
|
||||
- OpenGL 渲染路径仅在 `renderer_->IsOpenGL()` 为 true 时启用,并正确调用 `OlivePluginInstance::setOpenGLEnabled(use_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-op,Vulkan 项目中的 OFX 插件回退到 CPU 路径。
|
||||
- 格式转换(`ConvertFrameIfNeeded`、`ConvertTextureForParams`)、readback(`ReadbackTextureToFrame`)、upload 等辅助函数保持后端无关,通过 `Renderer` 接口调用,无需移入后端库。
|
||||
- `RenderProcessor::ProcessPluginJob` 不再要求 `render_ctx_` 实现 `OpenGLContextProvider`,任何 `Renderer` 都能驱动插件渲染。
|
||||
- 更新相关 gtest:`PluginRenderer` 构造函数现在需要传入 renderer 指针,测试传入 `nullptr` 验证纯 CPU 路径。
|
||||
|
||||
## 完成标准
|
||||
|
||||
- 主程序不直接 new `OpenGLRenderer` 作为默认渲染路径,而是通过动态后端适配器创建。
|
||||
- OpenGL 后端库可单独构建、加载、初始化、销毁。
|
||||
- 用户能在配置中选择 OpenGL/Vulkan。
|
||||
- Vulkan 未实现完整 renderer 前,请求 Vulkan 不崩溃,并有明确回退。
|
||||
- 手工测试计划覆盖 OpenGL/Vulkan 选择、重启持久化、回退、viewer、proxy、scope、导出。
|
||||
- [x] 主程序默认不再直接 new `OpenGLRenderer`,而是通过 `DynamicRenderer` 动态加载 OpenGL/Vulkan 后端;加载失败时保留回退到 `OpenGLRenderer` 的安全路径。
|
||||
- [x] `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 默认 `ON`,`liboakgl.so` / `liboakvulkan.so` 默认构建并安装。
|
||||
- [x] OpenGL 后端库可单独构建、加载、初始化、销毁。
|
||||
- [x] 用户能在配置中选择 OpenGL/Vulkan。
|
||||
- [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、导出等完整路径。
|
||||
|
||||
@@ -22,6 +22,28 @@ TEST(DynamicRenderBackend, LoadsExperimentalOpenGLBackend)
|
||||
#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)
|
||||
{
|
||||
#ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
|
||||
@@ -29,10 +51,13 @@ TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable)
|
||||
#else
|
||||
olive::DynamicRenderer renderer(QStringLiteral("vulkan"));
|
||||
ASSERT_TRUE(renderer.Load());
|
||||
EXPECT_EQ(renderer.backend_name(), QStringLiteral("opengl"));
|
||||
EXPECT_EQ(renderer.OpenGLContext(), nullptr);
|
||||
OakRenderBackendInfo 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_STREQ(info.name, "opengl");
|
||||
#endif
|
||||
|
||||
@@ -119,7 +119,7 @@ TEST(PluginIntegration, ChromaKeyerCreateAndRender)
|
||||
olive::plugin::PluginJob job(instance, nullptr, row);
|
||||
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);
|
||||
|
||||
EXPECT_TRUE(output->frame());
|
||||
|
||||
@@ -190,7 +190,7 @@ bool RenderPlugin(const std::string &plugin_id,
|
||||
PluginJob job(instance, nullptr, inputs);
|
||||
TexturePtr output = std::make_shared<Texture>(params);
|
||||
|
||||
PluginRenderer renderer;
|
||||
PluginRenderer renderer(nullptr);
|
||||
renderer.RenderPlugin(nullptr, job, output, params, true, false);
|
||||
|
||||
bool has_frame = output->frame() != nullptr;
|
||||
|
||||
Reference in New Issue
Block a user