diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a3fa42b79..03d9f9c85 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9dc8254c..50e9e25d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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*" diff --git a/CMakeLists.txt b/CMakeLists.txt index 20f5be8b9..e1d9b3b41 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 54904c3e7..e0a87e4bf 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -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 - $ + # 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") diff --git a/app/common/avframeptr.h b/app/common/avframeptr.h new file mode 100644 index 000000000..957f9635d --- /dev/null +++ b/app/common/avframeptr.h @@ -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 . + +***/ + +#ifndef AVFRAMEPTR_H +#define AVFRAMEPTR_H + +extern "C" { +#include +} + +#include + +namespace olive +{ + +using AVFramePtr = std::shared_ptr; + +inline AVFramePtr CreateAVFramePtr(AVFrame *f) +{ + return std::shared_ptr(f, [](AVFrame *g) { av_frame_free(&g); }); +} + +inline AVFramePtr CreateAVFramePtr() +{ + return CreateAVFramePtr(av_frame_alloc()); +} + +} + +#endif // AVFRAMEPTR_H diff --git a/app/common/ffmpegutils.h b/app/common/ffmpegutils.h index d2546591d..0f38dc1ee 100644 --- a/app/common/ffmpegutils.h +++ b/app/common/ffmpegutils.h @@ -30,6 +30,7 @@ extern "C" { #include +#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; -inline AVFramePtr CreateAVFramePtr(AVFrame *f) -{ - return std::shared_ptr(f, [](AVFrame *g) { av_frame_free(&g); }); -} -inline AVFramePtr CreateAVFramePtr() -{ - return CreateAVFramePtr(av_frame_alloc()); -} - } #endif // FFMPEGABSTRACTION_H diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index b410ce75e..c3bfbcfb5 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -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 diff --git a/app/render/backend/dynamicrenderer.cpp b/app/render/backend/dynamicrenderer.cpp index 70603bbf9..a2c7b8e6d 100644 --- a/app/render/backend/dynamicrenderer.cpp +++ b/app/render/backend/dynamicrenderer.cpp @@ -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_); + } +} + } diff --git a/app/render/backend/dynamicrenderer.h b/app/render/backend/dynamicrenderer.h index fba0bbd51..95b92bf2d 100644 --- a/app/render/backend/dynamicrenderer.h +++ b/app/render/backend/dynamicrenderer.h @@ -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; }; diff --git a/app/render/backend/renderbackend_c.h b/app/render/backend/renderbackend_c.h index 47ac79ecc..3405bd78e 100644 --- a/app/render/backend/renderbackend_c.h +++ b/app/render/backend/renderbackend_c.h @@ -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 diff --git a/app/render/colormanagement.cpp b/app/render/colormanagement.cpp new file mode 100644 index 000000000..006a68b5f --- /dev/null +++ b/app/render/colormanagement.cpp @@ -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 . + +***/ + +#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()); + } +} + +} diff --git a/app/render/interlacetexture.cpp b/app/render/interlacetexture.cpp new file mode 100644 index 000000000..08de4a662 --- /dev/null +++ b/app/render/interlacetexture.cpp @@ -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 . + +***/ + +#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; +} + +} diff --git a/app/render/opengl/openglbackend_c.cpp b/app/render/opengl/openglbackend_c.cpp index 80ed2423a..178644ee4 100644 --- a/app/render/opengl/openglbackend_c.cpp +++ b/app/render/opengl/openglbackend_c.cpp @@ -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(); +} diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 2117e1076..2059e6c77 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -27,7 +27,9 @@ #include #include +#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()); diff --git a/app/render/opengl/openglrenderer.h b/app/render/opengl/openglrenderer.h index 6b6aa8d31..327a58ce8 100644 --- a/app/render/opengl/openglrenderer.h +++ b/app/render/opengl/openglrenderer.h @@ -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: diff --git a/app/render/plugin/pluginrenderer.cpp b/app/render/plugin/pluginrenderer.cpp index 311666ba7..550ae461d 100644 --- a/app/render/plugin/pluginrenderer.cpp +++ b/app/render/plugin/pluginrenderer.cpp @@ -1432,9 +1432,10 @@ void olive::plugin::PluginRenderer::RenderPlugin(TexturePtr src, olive::plugin:: auto *olive_instance = dynamic_cast(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(); + } } diff --git a/app/render/plugin/pluginrenderer.h b/app/render/plugin/pluginrenderer.h index 689848613..b0b62855d 100644 --- a/app/render/plugin/pluginrenderer.h +++ b/app/render/plugin/pluginrenderer.h @@ -23,17 +23,12 @@ #ifndef PLUGINRENDERER_H #define PLUGINRENDERER_H -#include -#include -#include -#include -#include -#include -#include + +#include #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_; }; } } diff --git a/app/render/renderer.cpp b/app/render/renderer.cpp index 891bdc4fa..839535767 100644 --- a/app/render/renderer.cpp +++ b/app/render/renderer.cpp @@ -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(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 diff --git a/app/render/renderer.h b/app/render/renderer.h index 85463c24c..3484342e2 100644 --- a/app/render/renderer.h +++ b/app/render/renderer.h @@ -29,12 +29,15 @@ #include #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, diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index 0195b72df..89ea29f6c 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -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 diff --git a/app/render/renderprocessor.cpp b/app/render/renderprocessor.cpp index 42b1f4435..9cd7a39f4 100644 --- a/app/render/renderprocessor.cpp +++ b/app/render/renderprocessor.cpp @@ -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> - cached_plugin_renderers; - if (!cached_plugin_renderers.hasLocalData()) { - auto *gl = dynamic_cast(render_ctx_); - if (gl && gl->OpenGLContext()) { - auto cached_plugin_renderer = - std::make_shared(); - cached_plugin_renderer->Init(gl->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, diff --git a/app/render/texture.cpp b/app/render/texture.cpp index cdd6a3b01..6e687dd46 100644 --- a/app/render/texture.cpp +++ b/app/render/texture.cpp @@ -21,6 +21,7 @@ #include "texture.h" +#include "render/job/acceleratedjob.h" #include "renderer.h" namespace olive diff --git a/app/render/texture.h b/app/render/texture.h index 5b1c55a67..01592bff7 100644 --- a/app/render/texture.h +++ b/app/render/texture.h @@ -22,7 +22,7 @@ #ifndef RENDERTEXTURE_H #define RENDERTEXTURE_H -#include "common/ffmpegutils.h" +#include "common/avframeptr.h" #include #include diff --git a/app/render/videoparams.cpp b/app/render/videoparams.cpp index 26c1178c5..f5252d407 100644 --- a/app/render/videoparams.cpp +++ b/app/render/videoparams.cpp @@ -29,6 +29,7 @@ extern "C" { #include #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, diff --git a/app/render/videoparams.h b/app/render/videoparams.h index cac603c18..f7138b100 100644 --- a/app/render/videoparams.h +++ b/app/render/videoparams.h @@ -22,7 +22,6 @@ #ifndef VIDEOPARAMS_H #define VIDEOPARAMS_H -#include "ofxImageEffect.h" #include #include #include @@ -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_; diff --git a/app/render/vulkan/vulkanbackend_c.cpp b/app/render/vulkan/vulkanbackend_c.cpp index b4f38a8f8..daa699ba7 100644 --- a/app/render/vulkan/vulkanbackend_c.cpp +++ b/app/render/vulkan/vulkanbackend_c.cpp @@ -1,41 +1,47 @@ #include "render/backend/renderbackend_c.h" #include +#include +#include #include +#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(handle); + return static_cast(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(variant); } } // namespace OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent) { - Q_UNUSED(parent) - return new VulkanBackend(); + return new BackendVulkanRenderer(static_cast(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(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(out_variant)->clear(); - WarnUnavailable(handle, "create_native_texture"); + *static_cast(out_variant) = Renderer(handle)->CreateNativeTexture( + width, height, depth, static_cast(format), + channel_count, data, linesize); } OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture( OakRenderBackendHandle handle, const void *variant) { - 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(out_variant)->clear(); - WarnUnavailable(handle, "create_native_shader"); + *static_cast(out_variant) = Renderer(handle)->CreateNativeShader( + *static_cast(shader_code)); } OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader( OakRenderBackendHandle handle, const void *variant) { - 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(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(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(out_color) = Renderer(handle)->GetPixelFromTexture( + static_cast(texture), *static_cast(point)); } OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( OakRenderBackendHandle handle, const void *shader, void *job, void *destination, const void *destination_params, bool clear_destination) { - 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(job), + static_cast(destination), + *static_cast(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. +} diff --git a/app/render/vulkan/vulkanrenderer.cpp b/app/render/vulkan/vulkanrenderer.cpp new file mode 100644 index 000000000..58b66de8f --- /dev/null +++ b/app/render/vulkan/vulkanrenderer.cpp @@ -0,0 +1,1777 @@ +#include "vulkanrenderer.h" + +#include +#include +#include + +#include "node/value.h" +#include "render/job/shaderjob.h" + +#ifdef OAK_HAS_SHADERC +#include +#endif + +namespace olive +{ + +struct VulkanRenderer::VulkanTexture { + quint64 id = 0; + VkImage image = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + int width = 0; + int height = 0; + int depth = 0; + PixelFormat format = PixelFormat::INVALID; + int channel_count = 0; + VkFormat vk_format = VK_FORMAT_UNDEFINED; + VkImageLayout current_layout = VK_IMAGE_LAYOUT_UNDEFINED; +}; + +struct VulkanRenderer::UniformInfo { + QString name; + QString type; + VkDeviceSize offset; + VkDeviceSize size; +}; + +struct VulkanRenderer::VulkanShader { + quint64 id = 0; + VkShaderModule vert_module = VK_NULL_HANDLE; + VkShaderModule frag_module = VK_NULL_HANDLE; + VkPipelineLayout pipeline_layout = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptor_layout = VK_NULL_HANDLE; + QHash pipeline_cache; + bool pipeline_created = false; + QVector uniforms; + VkDeviceSize ubo_size = 0; +}; + +static const float kBlitVertices[] = { + -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, + -1.0f, -1.0f, 0.0f, 0.0f, 0.0f, + -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, +}; + +VulkanRenderer::VulkanRenderer(QObject *parent) : Renderer(parent) +{ +} + +VulkanRenderer::~VulkanRenderer() +{ + Destroy(); + PostDestroy(); +} + +bool VulkanRenderer::Init() +{ + return CreateInstance() && CreateDevice() && CreateCommandPool() && + CreateDescriptorPool(); +} + +void VulkanRenderer::PostInit() +{ + CreateVertexBuffer(); +} + +void VulkanRenderer::PostDestroy() +{ +} + +void VulkanRenderer::DestroyInternal() +{ + if (device_ == VK_NULL_HANDLE) { + return; + } + vkDeviceWaitIdle(device_); + + QMutexLocker lock(&mutex_); + for (auto it = textures_.begin(); it != textures_.end(); ++it) { + VulkanTexture *tex = it.value(); + if (tex->view != VK_NULL_HANDLE) { + vkDestroyImageView(device_, tex->view, nullptr); + } + if (tex->image != VK_NULL_HANDLE) { + vkDestroyImage(device_, tex->image, nullptr); + } + if (tex->memory != VK_NULL_HANDLE) { + vkFreeMemory(device_, tex->memory, nullptr); + } + delete tex; + } + textures_.clear(); + + for (auto it = shaders_.begin(); it != shaders_.end(); ++it) { + VulkanShader *sh = it.value(); + for (auto pit = sh->pipeline_cache.begin(); pit != sh->pipeline_cache.end(); ++pit) { + if (pit.value() != VK_NULL_HANDLE) { + vkDestroyPipeline(device_, pit.value(), nullptr); + } + } + sh->pipeline_cache.clear(); + if (sh->pipeline_layout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device_, sh->pipeline_layout, nullptr); + } + if (sh->descriptor_layout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(device_, sh->descriptor_layout, nullptr); + } + if (sh->vert_module != VK_NULL_HANDLE) { + vkDestroyShaderModule(device_, sh->vert_module, nullptr); + } + if (sh->frag_module != VK_NULL_HANDLE) { + vkDestroyShaderModule(device_, sh->frag_module, nullptr); + } + delete sh; + } + shaders_.clear(); + + if (vertex_buffer_ != VK_NULL_HANDLE) { + vkDestroyBuffer(device_, vertex_buffer_, nullptr); + vkFreeMemory(device_, vertex_buffer_memory_, nullptr); + vertex_buffer_ = VK_NULL_HANDLE; + vertex_buffer_memory_ = VK_NULL_HANDLE; + } + + for (auto it = render_pass_cache_.begin(); it != render_pass_cache_.end(); ++it) { + if (it.value() != VK_NULL_HANDLE) { + vkDestroyRenderPass(device_, it.value(), nullptr); + } + } + render_pass_cache_.clear(); + + if (descriptor_pool_ != VK_NULL_HANDLE) { + vkDestroyDescriptorPool(device_, descriptor_pool_, nullptr); + descriptor_pool_ = VK_NULL_HANDLE; + } + + if (command_pool_ != VK_NULL_HANDLE) { + vkDestroyCommandPool(device_, command_pool_, nullptr); + command_pool_ = VK_NULL_HANDLE; + } + + if (device_ != VK_NULL_HANDLE) { + vkDestroyDevice(device_, nullptr); + device_ = VK_NULL_HANDLE; + } + + if (instance_ != VK_NULL_HANDLE) { + vkDestroyInstance(instance_, nullptr); + instance_ = VK_NULL_HANDLE; + } +} + +bool VulkanRenderer::CreateInstance() +{ + VkApplicationInfo app_info = {}; + app_info.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + app_info.pApplicationName = "Oak Video Editor"; + app_info.applicationVersion = VK_MAKE_VERSION(0, 3, 0); + app_info.pEngineName = "Oak"; + app_info.engineVersion = VK_MAKE_VERSION(0, 3, 0); + app_info.apiVersion = VK_API_VERSION_1_2; + + VkInstanceCreateInfo create_info = {}; + create_info.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + create_info.pApplicationInfo = &app_info; + + VkResult result = vkCreateInstance(&create_info, nullptr, &instance_); + if (result != VK_SUCCESS) { + qWarning() << "Failed to create Vulkan instance:" << result; + return false; + } + qDebug() << "Vulkan instance created successfully"; + return true; +} + +bool VulkanRenderer::CreateDevice() +{ + uint32_t device_count = 0; + vkEnumeratePhysicalDevices(instance_, &device_count, nullptr); + if (device_count == 0) { + qWarning() << "No Vulkan-capable physical devices found"; + return false; + } + + QVector devices(device_count); + vkEnumeratePhysicalDevices(instance_, &device_count, devices.data()); + physical_device_ = devices.first(); + + vkGetPhysicalDeviceMemoryProperties(physical_device_, &mem_properties_); + vkGetPhysicalDeviceProperties(physical_device_, &device_properties_); + + uint32_t queue_family_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(physical_device_, &queue_family_count, + nullptr); + QVector queue_families(queue_family_count); + vkGetPhysicalDeviceQueueFamilyProperties(physical_device_, &queue_family_count, + queue_families.data()); + + for (uint32_t i = 0; i < queue_family_count; i++) { + if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphics_queue_family_ = i; + break; + } + } + if (graphics_queue_family_ == UINT32_MAX) { + qWarning() << "No graphics queue family found"; + return false; + } + + float queue_priority = 1.0f; + VkDeviceQueueCreateInfo queue_create_info = {}; + queue_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_create_info.queueFamilyIndex = graphics_queue_family_; + queue_create_info.queueCount = 1; + queue_create_info.pQueuePriorities = &queue_priority; + + VkPhysicalDeviceFeatures device_features = {}; + + const char *device_extensions[] = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; + VkDeviceCreateInfo device_create_info = {}; + device_create_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + device_create_info.pQueueCreateInfos = &queue_create_info; + device_create_info.queueCreateInfoCount = 1; + device_create_info.pEnabledFeatures = &device_features; + device_create_info.enabledExtensionCount = 1; + device_create_info.ppEnabledExtensionNames = device_extensions; + + VkResult result = vkCreateDevice(physical_device_, &device_create_info, nullptr, + &device_); + if (result != VK_SUCCESS) { + qWarning() << "Failed to create Vulkan logical device:" << result; + return false; + } + + qDebug() << "Vulkan device created successfully on" + << QString::fromUtf8(device_properties_.deviceName); + vkGetDeviceQueue(device_, graphics_queue_family_, 0, &graphics_queue_); + return true; +} + +bool VulkanRenderer::CreateCommandPool() +{ + VkCommandPoolCreateInfo pool_info = {}; + pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; + pool_info.queueFamilyIndex = graphics_queue_family_; + pool_info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + + VkResult result = vkCreateCommandPool(device_, &pool_info, nullptr, &command_pool_); + if (result != VK_SUCCESS) { + qWarning() << "Failed to create Vulkan command pool:" << result; + return false; + } + return true; +} + +bool VulkanRenderer::CreateDescriptorPool() +{ + VkDescriptorPoolSize pool_sizes[2] = {}; + pool_sizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + pool_sizes[0].descriptorCount = kMaxDescriptorSets; + pool_sizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + pool_sizes[1].descriptorCount = kMaxDescriptorSets * 8; + + VkDescriptorPoolCreateInfo pool_info = {}; + pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; + pool_info.poolSizeCount = 2; + pool_info.pPoolSizes = pool_sizes; + pool_info.maxSets = kMaxDescriptorSets; + pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT; + + VkResult result = vkCreateDescriptorPool(device_, &pool_info, nullptr, + &descriptor_pool_); + if (result != VK_SUCCESS) { + qWarning() << "Failed to create Vulkan descriptor pool:" << result; + return false; + } + return true; +} + +VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format) +{ + auto it = render_pass_cache_.find(format); + if (it != render_pass_cache_.end()) { + return it.value(); + } + + VkAttachmentDescription color_attachment = {}; + color_attachment.format = format; + color_attachment.samples = VK_SAMPLE_COUNT_1_BIT; + color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + color_attachment.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + color_attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkAttachmentReference color_ref = {}; + color_ref.attachment = 0; + color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription subpass = {}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_ref; + + VkRenderPassCreateInfo render_pass_info = {}; + render_pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; + render_pass_info.attachmentCount = 1; + render_pass_info.pAttachments = &color_attachment; + render_pass_info.subpassCount = 1; + render_pass_info.pSubpasses = &subpass; + + VkRenderPass render_pass = VK_NULL_HANDLE; + VkResult result = vkCreateRenderPass(device_, &render_pass_info, nullptr, + &render_pass); + if (result != VK_SUCCESS) { + qWarning() << "Failed to create Vulkan render pass:" << result; + return VK_NULL_HANDLE; + } + + render_pass_cache_.insert(format, render_pass); + return render_pass; +} + +bool VulkanRenderer::CreateVertexBuffer() +{ + VkDeviceSize buffer_size = sizeof(kBlitVertices); + + VkBufferCreateInfo buffer_info = {}; + buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buffer_info.size = buffer_size; + buffer_info.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + VkBuffer staging_buffer; + VkResult result = vkCreateBuffer(device_, &buffer_info, nullptr, &staging_buffer); + if (result != VK_SUCCESS) { + return false; + } + + VkMemoryRequirements mem_req; + vkGetBufferMemoryRequirements(device_, staging_buffer, &mem_req); + + VkMemoryAllocateInfo alloc_info = {}; + alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.allocationSize = mem_req.size; + alloc_info.memoryTypeIndex = FindMemoryType( + mem_req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + + VkDeviceMemory staging_memory; + result = vkAllocateMemory(device_, &alloc_info, nullptr, &staging_memory); + if (result != VK_SUCCESS) { + vkDestroyBuffer(device_, staging_buffer, nullptr); + return false; + } + + vkBindBufferMemory(device_, staging_buffer, staging_memory, 0); + + void *data; + vkMapMemory(device_, staging_memory, 0, buffer_size, 0, &data); + memcpy(data, kBlitVertices, (size_t)buffer_size); + vkUnmapMemory(device_, staging_memory); + + buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + result = vkCreateBuffer(device_, &buffer_info, nullptr, &vertex_buffer_); + if (result != VK_SUCCESS) { + vkFreeMemory(device_, staging_memory, nullptr); + vkDestroyBuffer(device_, staging_buffer, nullptr); + return false; + } + + vkGetBufferMemoryRequirements(device_, vertex_buffer_, &mem_req); + + alloc_info.allocationSize = mem_req.size; + alloc_info.memoryTypeIndex = + FindMemoryType(mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + result = vkAllocateMemory(device_, &alloc_info, nullptr, &vertex_buffer_memory_); + if (result != VK_SUCCESS) { + vkDestroyBuffer(device_, vertex_buffer_, nullptr); + vkFreeMemory(device_, staging_memory, nullptr); + vkDestroyBuffer(device_, staging_buffer, nullptr); + return false; + } + + vkBindBufferMemory(device_, vertex_buffer_, vertex_buffer_memory_, 0); + + // Copy from staging to device local + VkCommandBuffer cmd = BeginOneTimeCommands(); + VkBufferCopy copy_region = {}; + copy_region.size = buffer_size; + vkCmdCopyBuffer(cmd, staging_buffer, vertex_buffer_, 1, ©_region); + EndOneTimeCommands(cmd); + + vkFreeMemory(device_, staging_memory, nullptr); + vkDestroyBuffer(device_, staging_buffer, nullptr); + + return true; +} + +bool VulkanRenderer::CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer, + VkDeviceMemory *out_memory) +{ + VkBufferCreateInfo buffer_info = {}; + buffer_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buffer_info.size = size; + buffer_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + VkResult result = vkCreateBuffer(device_, &buffer_info, nullptr, out_buffer); + if (result != VK_SUCCESS) { + return false; + } + + VkMemoryRequirements mem_req; + vkGetBufferMemoryRequirements(device_, *out_buffer, &mem_req); + + VkMemoryAllocateInfo alloc_info = {}; + alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.allocationSize = mem_req.size; + alloc_info.memoryTypeIndex = FindMemoryType( + mem_req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + + result = vkAllocateMemory(device_, &alloc_info, nullptr, out_memory); + if (result != VK_SUCCESS) { + vkDestroyBuffer(device_, *out_buffer, nullptr); + return false; + } + + vkBindBufferMemory(device_, *out_buffer, *out_memory, 0); + return true; +} + +void VulkanRenderer::DestroyStagingBuffer(VkBuffer buffer, VkDeviceMemory memory) +{ + if (buffer != VK_NULL_HANDLE) { + vkDestroyBuffer(device_, buffer, nullptr); + } + if (memory != VK_NULL_HANDLE) { + vkFreeMemory(device_, memory, nullptr); + } +} + +VkCommandBuffer VulkanRenderer::BeginOneTimeCommands() +{ + VkCommandBufferAllocateInfo alloc_info = {}; + alloc_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + alloc_info.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + alloc_info.commandPool = command_pool_; + alloc_info.commandBufferCount = 1; + + VkCommandBuffer cmd; + vkAllocateCommandBuffers(device_, &alloc_info, &cmd); + + VkCommandBufferBeginInfo begin_info = {}; + begin_info.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + begin_info.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + vkBeginCommandBuffer(cmd, &begin_info); + return cmd; +} + +void VulkanRenderer::EndOneTimeCommands(VkCommandBuffer cmd) +{ + vkEndCommandBuffer(cmd); + + VkSubmitInfo submit_info = {}; + submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submit_info.commandBufferCount = 1; + submit_info.pCommandBuffers = &cmd; + + vkQueueSubmit(graphics_queue_, 1, &submit_info, VK_NULL_HANDLE); + vkQueueWaitIdle(graphics_queue_); + + vkFreeCommandBuffers(device_, command_pool_, 1, &cmd); +} + +void VulkanRenderer::TransitionImageLayout(VkCommandBuffer cmd, VkImage image, + VkImageLayout old_layout, + VkImageLayout new_layout) +{ + VkImageMemoryBarrier barrier = {}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.oldLayout = old_layout; + barrier.newLayout = new_layout; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseMipLevel = 0; + barrier.subresourceRange.levelCount = 1; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + + VkPipelineStageFlags source_stage; + VkPipelineStageFlags destination_stage; + + if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED && + new_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && + new_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + destination_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + } else if (old_layout == VK_IMAGE_LAYOUT_UNDEFINED && + new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + } else if (old_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL && + new_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + source_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + destination_stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + } else if (old_layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL && + new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + source_stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + } else if (old_layout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL && + new_layout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + source_stage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; + destination_stage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + } else { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = 0; + source_stage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destination_stage = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT; + } + + vkCmdPipelineBarrier(cmd, source_stage, destination_stage, 0, 0, nullptr, 0, + nullptr, 1, &barrier); +} + +void VulkanRenderer::CopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer, + VkImage image, uint32_t width, + uint32_t height, uint32_t depth) +{ + VkBufferImageCopy region = {}; + region.bufferOffset = 0; + region.bufferRowLength = 0; + region.bufferImageHeight = 0; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = { 0, 0, 0 }; + region.imageExtent = { width, height, depth }; + + vkCmdCopyBufferToImage(cmd, buffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, ®ion); +} + +void VulkanRenderer::CopyImageToBuffer(VkCommandBuffer cmd, VkImage image, + VkBuffer buffer, uint32_t width, + uint32_t height, + uint32_t offset_x, uint32_t offset_y) +{ + VkBufferImageCopy region = {}; + region.bufferOffset = 0; + region.bufferRowLength = 0; + region.bufferImageHeight = 0; + region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + region.imageSubresource.mipLevel = 0; + region.imageSubresource.baseArrayLayer = 0; + region.imageSubresource.layerCount = 1; + region.imageOffset = { static_cast(offset_x), static_cast(offset_y), 0 }; + region.imageExtent = { width, height, 1 }; + + vkCmdCopyImageToBuffer(cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, buffer, + 1, ®ion); +} + +VkFormat VulkanRenderer::PixelFormatToVkFormat(PixelFormat format, + int channel_count) const +{ + switch (format) { + case PixelFormat::U8: + switch (channel_count) { + case 1: return VK_FORMAT_R8_UNORM; + case 2: return VK_FORMAT_R8G8_UNORM; + case 3: return VK_FORMAT_R8G8B8_UNORM; + case 4: return VK_FORMAT_R8G8B8A8_UNORM; + } + break; + case PixelFormat::U16: + switch (channel_count) { + case 1: return VK_FORMAT_R16_UNORM; + case 2: return VK_FORMAT_R16G16_UNORM; + case 3: return VK_FORMAT_R16G16B16_UNORM; + case 4: return VK_FORMAT_R16G16B16A16_UNORM; + } + break; + case PixelFormat::F16: + switch (channel_count) { + case 1: return VK_FORMAT_R16_SFLOAT; + case 2: return VK_FORMAT_R16G16_SFLOAT; + case 3: return VK_FORMAT_R16G16B16_SFLOAT; + case 4: return VK_FORMAT_R16G16B16A16_SFLOAT; + } + break; + case PixelFormat::F32: + switch (channel_count) { + case 1: return VK_FORMAT_R32_SFLOAT; + case 2: return VK_FORMAT_R32G32_SFLOAT; + case 3: return VK_FORMAT_R32G32B32_SFLOAT; + case 4: return VK_FORMAT_R32G32B32A32_SFLOAT; + } + break; + case PixelFormat::INVALID: + case PixelFormat::COUNT: + break; + } + return VK_FORMAT_UNDEFINED; +} + +VkDeviceSize VulkanRenderer::AlignSize(VkDeviceSize size, + VkDeviceSize alignment) const +{ + return (size + alignment - 1) & ~(alignment - 1); +} + +uint32_t VulkanRenderer::FindMemoryType(uint32_t type_filter, + VkMemoryPropertyFlags properties) const +{ + for (uint32_t i = 0; i < mem_properties_.memoryTypeCount; i++) { + if ((type_filter & (1 << i)) && + (mem_properties_.memoryTypes[i].propertyFlags & properties) == + properties) { + return i; + } + } + return UINT32_MAX; +} + +QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth, + PixelFormat format, int channel_count, + const void *data, int linesize) +{ + QMutexLocker lock(&mutex_); + + VkFormat vk_format = PixelFormatToVkFormat(format, channel_count); + if (vk_format == VK_FORMAT_UNDEFINED) { + qWarning() << "Unsupported pixel format for Vulkan texture"; + return QVariant(); + } + + VulkanTexture *tex = new VulkanTexture(); + tex->id = next_texture_id_++; + tex->width = width; + tex->height = height; + tex->depth = depth; + tex->format = format; + tex->channel_count = channel_count; + tex->vk_format = vk_format; + + VkImageCreateInfo image_info = {}; + image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + image_info.imageType = depth > 1 ? VK_IMAGE_TYPE_3D : VK_IMAGE_TYPE_2D; + image_info.extent.width = static_cast(width); + image_info.extent.height = static_cast(height); + image_info.extent.depth = static_cast(depth); + image_info.mipLevels = 1; + image_info.arrayLayers = 1; + image_info.format = vk_format; + image_info.tiling = VK_IMAGE_TILING_OPTIMAL; + image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + image_info.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | + VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + image_info.samples = VK_SAMPLE_COUNT_1_BIT; + + VkResult result = vkCreateImage(device_, &image_info, nullptr, &tex->image); + if (result != VK_SUCCESS) { + delete tex; + return QVariant(); + } + + VkMemoryRequirements mem_req; + vkGetImageMemoryRequirements(device_, tex->image, &mem_req); + + VkMemoryAllocateInfo alloc_info = {}; + alloc_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + alloc_info.allocationSize = mem_req.size; + alloc_info.memoryTypeIndex = + FindMemoryType(mem_req.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + + result = vkAllocateMemory(device_, &alloc_info, nullptr, &tex->memory); + if (result != VK_SUCCESS) { + vkDestroyImage(device_, tex->image, nullptr); + delete tex; + return QVariant(); + } + + vkBindImageMemory(device_, tex->image, tex->memory, 0); + + VkImageViewCreateInfo view_info = {}; + view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + view_info.image = tex->image; + view_info.viewType = depth > 1 ? VK_IMAGE_VIEW_TYPE_3D : VK_IMAGE_VIEW_TYPE_2D; + view_info.format = vk_format; + view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + view_info.subresourceRange.baseMipLevel = 0; + view_info.subresourceRange.levelCount = 1; + view_info.subresourceRange.baseArrayLayer = 0; + view_info.subresourceRange.layerCount = 1; + + result = vkCreateImageView(device_, &view_info, nullptr, &tex->view); + if (result != VK_SUCCESS) { + vkFreeMemory(device_, tex->memory, nullptr); + vkDestroyImage(device_, tex->image, nullptr); + delete tex; + return QVariant(); + } + + // Upload initial data if provided + if (data) { + int bytes_per_pixel = VideoParams::GetBytesPerPixel(format, channel_count); + VkDeviceSize image_size = static_cast(width) * height * depth * + bytes_per_pixel; + if (linesize == 0) { + linesize = width * bytes_per_pixel; + } + + VkBuffer staging_buffer; + VkDeviceMemory staging_memory; + if (CreateStagingBuffer(image_size, &staging_buffer, &staging_memory)) { + void *mapped; + vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); + if (linesize == width * bytes_per_pixel) { + memcpy(mapped, data, static_cast(image_size)); + } else { + char *dst = static_cast(mapped); + const char *src = static_cast(data); + for (int row = 0; row < height * depth; row++) { + memcpy(dst + row * width * bytes_per_pixel, + src + row * linesize, + static_cast(width * bytes_per_pixel)); + } + } + vkUnmapMemory(device_, staging_memory); + + VkCommandBuffer cmd = BeginOneTimeCommands(); + TransitionImageLayout(cmd, tex->image, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + CopyBufferToImage(cmd, staging_buffer, tex->image, + static_cast(width), + static_cast(height), + static_cast(depth)); + TransitionImageLayout(cmd, tex->image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + EndOneTimeCommands(cmd); + + DestroyStagingBuffer(staging_buffer, staging_memory); + tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + } + } else { + VkCommandBuffer cmd = BeginOneTimeCommands(); + TransitionImageLayout(cmd, tex->image, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + EndOneTimeCommands(cmd); + tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + + textures_.insert(tex->id, tex); + return QVariant::fromValue(tex->id); +} + +void VulkanRenderer::DestroyNativeTexture(QVariant texture) +{ + QMutexLocker lock(&mutex_); + quint64 id = texture.value(); + VulkanTexture *tex = textures_.take(id); + if (!tex) { + return; + } + if (tex->view != VK_NULL_HANDLE) { + vkDestroyImageView(device_, tex->view, nullptr); + } + if (tex->image != VK_NULL_HANDLE) { + vkDestroyImage(device_, tex->image, nullptr); + } + if (tex->memory != VK_NULL_HANDLE) { + vkFreeMemory(device_, tex->memory, nullptr); + } + delete tex; +} + +void VulkanRenderer::UploadToTexture(const QVariant &handle, + const VideoParams ¶ms, const void *data, + int linesize) +{ + QMutexLocker lock(&mutex_); + quint64 id = handle.value(); + VulkanTexture *tex = textures_.value(id); + if (!tex || !data) { + return; + } + + int width = params.effective_width(); + int height = params.effective_height(); + int depth = params.effective_depth(); + int bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), + params.channel_count()); + VkDeviceSize image_size = + static_cast(width) * height * depth * bytes_per_pixel; + if (linesize == 0) { + linesize = width * bytes_per_pixel; + } + + VkBuffer staging_buffer; + VkDeviceMemory staging_memory; + if (!CreateStagingBuffer(image_size, &staging_buffer, &staging_memory)) { + return; + } + + void *mapped; + vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); + if (linesize == width * bytes_per_pixel) { + memcpy(mapped, data, static_cast(image_size)); + } else { + char *dst = static_cast(mapped); + const char *src = static_cast(data); + for (int row = 0; row < height * depth; row++) { + memcpy(dst + row * width * bytes_per_pixel, + src + row * linesize, + static_cast(width * bytes_per_pixel)); + } + } + vkUnmapMemory(device_, staging_memory); + + VkCommandBuffer cmd = BeginOneTimeCommands(); + if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { + TransitionImageLayout(cmd, tex->image, tex->current_layout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + } + CopyBufferToImage(cmd, staging_buffer, tex->image, + static_cast(width), + static_cast(height), + static_cast(depth)); + TransitionImageLayout(cmd, tex->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + EndOneTimeCommands(cmd); + + DestroyStagingBuffer(staging_buffer, staging_memory); + tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; +} + +void VulkanRenderer::DownloadFromTexture(const QVariant &handle, + const VideoParams ¶ms, void *data, + int linesize) +{ + QMutexLocker lock(&mutex_); + quint64 id = handle.value(); + VulkanTexture *tex = textures_.value(id); + if (!tex || !data) { + return; + } + + int width = params.effective_width(); + int height = params.effective_height(); + int bytes_per_pixel = VideoParams::GetBytesPerPixel(params.format(), + params.channel_count()); + if (linesize == 0) { + linesize = width * bytes_per_pixel; + } + VkDeviceSize image_size = + static_cast(width) * height * bytes_per_pixel; + + VkBuffer staging_buffer; + VkDeviceMemory staging_memory; + if (!CreateStagingBuffer(image_size, &staging_buffer, &staging_memory)) { + return; + } + + VkCommandBuffer cmd = BeginOneTimeCommands(); + if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) { + TransitionImageLayout(cmd, tex->image, tex->current_layout, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + } + CopyImageToBuffer(cmd, tex->image, staging_buffer, + static_cast(width), + static_cast(height)); + EndOneTimeCommands(cmd); + + void *mapped; + vkMapMemory(device_, staging_memory, 0, image_size, 0, &mapped); + if (linesize == width * bytes_per_pixel) { + memcpy(data, mapped, static_cast(image_size)); + } else { + char *dst = static_cast(data); + const char *src = static_cast(mapped); + for (int row = 0; row < height; row++) { + memcpy(dst + row * linesize, + src + row * width * bytes_per_pixel, + static_cast(width * bytes_per_pixel)); + } + } + vkUnmapMemory(device_, staging_memory); + + DestroyStagingBuffer(staging_buffer, staging_memory); +} + +void VulkanRenderer::Flush() +{ + if (device_ != VK_NULL_HANDLE) { + vkDeviceWaitIdle(device_); + } +} + +void VulkanRenderer::ClearDestination(olive::Texture *texture, double r, double g, + double b, double a) +{ + QMutexLocker lock(&mutex_); + + VkCommandBuffer cmd = BeginOneTimeCommands(); + if (texture) { + quint64 id = texture->id().value(); + VulkanTexture *tex = textures_.value(id); + if (!tex) { + EndOneTimeCommands(cmd); + return; + } + if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { + TransitionImageLayout(cmd, tex->image, tex->current_layout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + } + VkClearColorValue clear_color = {}; + clear_color.float32[0] = static_cast(r); + clear_color.float32[1] = static_cast(g); + clear_color.float32[2] = static_cast(b); + clear_color.float32[3] = static_cast(a); + VkImageSubresourceRange range = {}; + range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + range.baseMipLevel = 0; + range.levelCount = 1; + range.baseArrayLayer = 0; + range.layerCount = 1; + vkCmdClearColorImage(cmd, tex->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + &clear_color, 1, &range); + TransitionImageLayout(cmd, tex->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + EndOneTimeCommands(cmd); +} + +Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture, + const QPointF &pt) +{ + if (!texture) { + return Color(); + } + int bytes_per_pixel = VideoParams::GetBytesPerPixel(texture->format(), + texture->channel_count()); + QByteArray data(bytes_per_pixel, Qt::Uninitialized); + + quint64 id = texture->id().value(); + QMutexLocker lock(&mutex_); + VulkanTexture *tex = textures_.value(id); + if (!tex) { + return Color(); + } + + uint32_t px = static_cast(qBound(0.0, pt.x(), double(tex->width - 1))); + uint32_t py = static_cast(qBound(0.0, pt.y(), double(tex->height - 1))); + + VkBuffer staging_buffer; + VkDeviceMemory staging_memory; + if (!CreateStagingBuffer(bytes_per_pixel, &staging_buffer, &staging_memory)) { + return Color(); + } + + VkCommandBuffer cmd = BeginOneTimeCommands(); + if (tex->current_layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) { + TransitionImageLayout(cmd, tex->image, tex->current_layout, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + } + CopyImageToBuffer(cmd, tex->image, staging_buffer, 1, 1, px, py); + EndOneTimeCommands(cmd); + + void *mapped; + vkMapMemory(device_, staging_memory, 0, bytes_per_pixel, 0, &mapped); + memcpy(data.data(), mapped, static_cast(bytes_per_pixel)); + vkUnmapMemory(device_, staging_memory); + + DestroyStagingBuffer(staging_buffer, staging_memory); + + return Color::fromData(data.data(), texture->format(), + texture->channel_count()); +} + +// ------------------------------------------------------------------ +// Shader compilation (GLSL -> SPIR-V via shaderc) +// ------------------------------------------------------------------ + +QString VulkanRenderer::ConvertGlslToVulkan(const QString &glsl, + VkShaderStageFlagBits stage) +{ + QString result = glsl; + + // Replace version 150 with 450 core + if (result.contains(QStringLiteral("#version 150"))) { + result.replace(QStringLiteral("#version 150"), + QStringLiteral("#version 450 core")); + } + + // Ensure fragment shader output has layout + if (stage == VK_SHADER_STAGE_FRAGMENT_BIT) { + result.replace(QStringLiteral("out vec4 frag_color;"), + QStringLiteral("layout(location = 0) out vec4 frag_color;")); + } + + // Add layout to vertex attributes + if (stage == VK_SHADER_STAGE_VERTEX_BIT) { + result.replace(QStringLiteral("in vec4 a_position;"), + QStringLiteral("layout(location = 0) in vec4 a_position;")); + result.replace(QStringLiteral("in vec2 a_texcoord;"), + QStringLiteral("layout(location = 1) in vec2 a_texcoord;")); + } + + return result; +} + +VkDeviceSize VulkanRenderer::GetStd140Size(const QString &type) const +{ + if (type == QStringLiteral("float")) return 4; + if (type == QStringLiteral("vec2")) return 8; + if (type == QStringLiteral("vec3")) return 12; + if (type == QStringLiteral("vec4")) return 16; + if (type == QStringLiteral("mat4")) return 64; + if (type == QStringLiteral("int") || type == QStringLiteral("bool")) return 4; + return 4; +} + +VkDeviceSize VulkanRenderer::GetStd140Alignment(const QString &type) const +{ + if (type == QStringLiteral("float")) return 4; + if (type == QStringLiteral("vec2")) return 8; + if (type == QStringLiteral("vec3")) return 16; + if (type == QStringLiteral("vec4")) return 16; + if (type == QStringLiteral("mat4")) return 16; + if (type == QStringLiteral("int") || type == QStringLiteral("bool")) return 4; + return 4; +} + +QString VulkanRenderer::ConvertGlslUniformsToUbo(const QString &glsl, + QVector *out_uniforms) +{ + QString result = glsl; + QVector uniforms; + + // Regex to match uniform declarations like: uniform vec4 color; + // Exclude sampler types + QRegularExpression re(QStringLiteral(R"(^\s*uniform\s+(\w+)\s+(\w+)\s*;)"), + QRegularExpression::MultilineOption); + QRegularExpression sampler_re(QStringLiteral(R"(sampler\d*D|samplerCube|sampler2DArray)")); + + int offset = 0; + QRegularExpressionMatchIterator it = re.globalMatch(result); + QVector matches; + while (it.hasNext()) { + matches.append(it.next()); + } + + // Process in reverse order so removal doesn't shift indices + for (int i = matches.size() - 1; i >= 0; --i) { + const QRegularExpressionMatch &m = matches[i]; + QString type = m.captured(1); + QString name = m.captured(2); + + if (sampler_re.match(type).hasMatch()) { + continue; // Let shaderc auto-bind samplers + } + + UniformInfo info; + info.name = name; + info.type = type; + VkDeviceSize align = GetStd140Alignment(type); + offset = static_cast(AlignSize(offset, align)); + info.offset = offset; + info.size = GetStd140Size(type); + offset += static_cast(info.size); + uniforms.prepend(info); + + // Remove the original declaration + result.remove(m.capturedStart(), m.capturedLength()); + } + + if (!uniforms.isEmpty()) { + // Build UBO block + QString ubo = QStringLiteral("\nlayout(set = 0, binding = 0) uniform UniformBuffer {\n"); + for (const UniformInfo &info : uniforms) { + ubo += QStringLiteral(" %1 %2;\n").arg(info.type, info.name); + } + ubo += QStringLiteral("} ubo;\n\n"); + + // Insert UBO after #version line or at the beginning + int version_end = result.indexOf(QStringLiteral("\n")); + if (version_end >= 0 && result.startsWith(QStringLiteral("#version"))) { + result.insert(version_end + 1, ubo); + } else { + result.prepend(ubo); + } + + // Replace bare uniform names with ubo.name in the rest of the shader + // We do this carefully to avoid replacing the names inside the UBO block itself + for (const UniformInfo &info : uniforms) { + QString old_name = QStringLiteral("\\b%1\\b").arg(info.name); + QString new_name = QStringLiteral("ubo.%1").arg(info.name); + result.replace(QRegularExpression(old_name), new_name); + } + // Fix double ubo.ubo. that may have been created + result.replace(QStringLiteral("ubo.ubo."), QStringLiteral("ubo.")); + } + + if (out_uniforms) { + *out_uniforms = uniforms; + } + return result; +} + +bool VulkanRenderer::CompileGlslToSpv(const QString &glsl, + VkShaderStageFlagBits stage, + QByteArray *out_spv) +{ +#ifdef OAK_HAS_SHADERC + shaderc_compiler_t compiler = shaderc_compiler_initialize(); + if (!compiler) { + qWarning() << "Failed to initialize shaderc compiler"; + return false; + } + + shaderc_compile_options_t options = shaderc_compile_options_initialize(); + shaderc_compile_options_set_auto_bind_uniforms(options, true); + shaderc_compile_options_set_optimization_level( + options, shaderc_optimization_level_performance); + + shaderc_shader_kind kind; + switch (stage) { + case VK_SHADER_STAGE_VERTEX_BIT: + kind = shaderc_glsl_vertex_shader; + break; + case VK_SHADER_STAGE_FRAGMENT_BIT: + kind = shaderc_glsl_fragment_shader; + break; + default: + shaderc_compile_options_release(options); + shaderc_compiler_release(compiler); + return false; + } + + QString converted = ConvertGlslToVulkan(glsl, stage); + QByteArray source_utf8 = converted.toUtf8(); + + shaderc_compilation_result_t compile_result = shaderc_compile_into_spv( + compiler, source_utf8.constData(), source_utf8.size(), kind, + "shader.glsl", "main", options); + + shaderc_compile_options_release(options); + + if (shaderc_result_get_compilation_status(compile_result) != + shaderc_compilation_status_success) { + qWarning() << "shaderc compilation failed:" + << shaderc_result_get_error_message(compile_result); + shaderc_result_release(compile_result); + shaderc_compiler_release(compiler); + return false; + } + + size_t spv_size = shaderc_result_get_length(compile_result); + const char *spv_data = shaderc_result_get_bytes(compile_result); + out_spv->resize(static_cast(spv_size)); + memcpy(out_spv->data(), spv_data, spv_size); + + shaderc_result_release(compile_result); + shaderc_compiler_release(compiler); + return true; +#else + Q_UNUSED(glsl) + Q_UNUSED(stage) + Q_UNUSED(out_spv) + qWarning() << "shaderc not available, cannot compile GLSL to SPIR-V"; + return false; +#endif +} + +QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code) +{ + QMutexLocker lock(&mutex_); + + QByteArray vert_spv; + QByteArray frag_spv; + + QString vert_code = code.vert_code(); + QString frag_code = code.frag_code(); + + // Use default shaders if empty + if (vert_code.isEmpty()) { + vert_code = FileFunctions::ReadFileAsString( + QStringLiteral(":/shaders/default.vert")); + } + if (frag_code.isEmpty()) { + frag_code = FileFunctions::ReadFileAsString( + QStringLiteral(":/shaders/default.frag")); + } + + // Convert fragment shader uniforms to UBO before compiling + QVector frag_uniforms; + QString converted_frag = ConvertGlslUniformsToUbo(frag_code, &frag_uniforms); + + if (!CompileGlslToSpv(vert_code, VK_SHADER_STAGE_VERTEX_BIT, &vert_spv)) { + return QVariant(); + } + if (!CompileGlslToSpv(converted_frag, VK_SHADER_STAGE_FRAGMENT_BIT, &frag_spv)) { + return QVariant(); + } + + VulkanShader *sh = new VulkanShader(); + sh->id = next_shader_id_++; + sh->uniforms = frag_uniforms; + sh->ubo_size = 0; + for (const UniformInfo &u : frag_uniforms) { + sh->ubo_size = qMax(sh->ubo_size, u.offset + u.size); + } + + VkShaderModuleCreateInfo vert_info = {}; + vert_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + vert_info.codeSize = static_cast(vert_spv.size()); + vert_info.pCode = reinterpret_cast(vert_spv.constData()); + + VkResult result = vkCreateShaderModule(device_, &vert_info, nullptr, + &sh->vert_module); + if (result != VK_SUCCESS) { + delete sh; + return QVariant(); + } + + VkShaderModuleCreateInfo frag_info = {}; + frag_info.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; + frag_info.codeSize = static_cast(frag_spv.size()); + frag_info.pCode = reinterpret_cast(frag_spv.constData()); + + result = vkCreateShaderModule(device_, &frag_info, nullptr, &sh->frag_module); + if (result != VK_SUCCESS) { + vkDestroyShaderModule(device_, sh->vert_module, nullptr); + delete sh; + return QVariant(); + } + + // Create descriptor set layout: binding 0 = UBO, binding 1+ = samplers + VkDescriptorSetLayoutBinding bindings[2] = {}; + bindings[0].binding = 0; + bindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + bindings[0].descriptorCount = 1; + bindings[0].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + bindings[1].binding = 1; + bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + bindings[1].descriptorCount = 16; // max textures + bindings[1].stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + + VkDescriptorSetLayoutCreateInfo ds_layout_info = {}; + ds_layout_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; + ds_layout_info.bindingCount = 2; + ds_layout_info.pBindings = bindings; + + result = vkCreateDescriptorSetLayout(device_, &ds_layout_info, nullptr, + &sh->descriptor_layout); + if (result != VK_SUCCESS) { + vkDestroyShaderModule(device_, sh->frag_module, nullptr); + vkDestroyShaderModule(device_, sh->vert_module, nullptr); + delete sh; + return QVariant(); + } + + VkPipelineLayoutCreateInfo layout_info = {}; + layout_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; + layout_info.setLayoutCount = 1; + layout_info.pSetLayouts = &sh->descriptor_layout; + + result = vkCreatePipelineLayout(device_, &layout_info, nullptr, + &sh->pipeline_layout); + if (result != VK_SUCCESS) { + vkDestroyDescriptorSetLayout(device_, sh->descriptor_layout, nullptr); + vkDestroyShaderModule(device_, sh->frag_module, nullptr); + vkDestroyShaderModule(device_, sh->vert_module, nullptr); + delete sh; + return QVariant(); + } + + shaders_.insert(sh->id, sh); + return QVariant::fromValue(sh->id); +} + +void VulkanRenderer::DestroyNativeShader(QVariant shader) +{ + QMutexLocker lock(&mutex_); + quint64 id = shader.value(); + VulkanShader *sh = shaders_.take(id); + if (!sh) { + return; + } + for (auto it = sh->pipeline_cache.begin(); it != sh->pipeline_cache.end(); ++it) { + if (it.value() != VK_NULL_HANDLE) { + vkDestroyPipeline(device_, it.value(), nullptr); + } + } + sh->pipeline_cache.clear(); + if (sh->pipeline_layout != VK_NULL_HANDLE) { + vkDestroyPipelineLayout(device_, sh->pipeline_layout, nullptr); + } + if (sh->descriptor_layout != VK_NULL_HANDLE) { + vkDestroyDescriptorSetLayout(device_, sh->descriptor_layout, nullptr); + } + if (sh->vert_module != VK_NULL_HANDLE) { + vkDestroyShaderModule(device_, sh->vert_module, nullptr); + } + if (sh->frag_module != VK_NULL_HANDLE) { + vkDestroyShaderModule(device_, sh->frag_module, nullptr); + } + delete sh; +} + +bool VulkanRenderer::CreatePipelineForShader(VulkanShader *shader, + const VideoParams &dest_params, + + VkFormat render_pass_format) +{ + if (shader->pipeline_cache.contains(render_pass_format)) { + return true; + } + + VkPipelineShaderStageCreateInfo vert_stage = {}; + vert_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + vert_stage.stage = VK_SHADER_STAGE_VERTEX_BIT; + vert_stage.module = shader->vert_module; + vert_stage.pName = "main"; + + VkPipelineShaderStageCreateInfo frag_stage = {}; + frag_stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + frag_stage.stage = VK_SHADER_STAGE_FRAGMENT_BIT; + frag_stage.module = shader->frag_module; + frag_stage.pName = "main"; + + VkPipelineShaderStageCreateInfo stages[] = { vert_stage, frag_stage }; + + VkPipelineVertexInputStateCreateInfo vertex_input = {}; + vertex_input.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; + + VkVertexInputBindingDescription binding_desc = {}; + binding_desc.binding = 0; + binding_desc.stride = 5 * sizeof(float); + binding_desc.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + VkVertexInputAttributeDescription attrs[2] = {}; + attrs[0].binding = 0; + attrs[0].location = 0; + attrs[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attrs[0].offset = 0; + attrs[1].binding = 0; + attrs[1].location = 1; + attrs[1].format = VK_FORMAT_R32G32_SFLOAT; + attrs[1].offset = 3 * sizeof(float); + + vertex_input.vertexBindingDescriptionCount = 1; + vertex_input.pVertexBindingDescriptions = &binding_desc; + vertex_input.vertexAttributeDescriptionCount = 2; + vertex_input.pVertexAttributeDescriptions = attrs; + + VkPipelineInputAssemblyStateCreateInfo input_assembly = {}; + input_assembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; + input_assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + + VkViewport viewport = {}; + viewport.width = static_cast(dest_params.effective_width()); + viewport.height = static_cast(dest_params.effective_height()); + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + + VkRect2D scissor = {}; + scissor.extent.width = static_cast(dest_params.effective_width()); + scissor.extent.height = static_cast(dest_params.effective_height()); + + VkPipelineViewportStateCreateInfo viewport_state = {}; + viewport_state.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; + viewport_state.viewportCount = 1; + viewport_state.pViewports = &viewport; + viewport_state.scissorCount = 1; + viewport_state.pScissors = &scissor; + + VkPipelineRasterizationStateCreateInfo rasterizer = {}; + rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; + rasterizer.polygonMode = VK_POLYGON_MODE_FILL; + rasterizer.cullMode = VK_CULL_MODE_NONE; + rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterizer.lineWidth = 1.0f; + + VkPipelineMultisampleStateCreateInfo multisampling = {}; + multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; + multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + VkPipelineColorBlendAttachmentState color_blend = {}; + color_blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + color_blend.blendEnable = VK_FALSE; + + VkPipelineColorBlendStateCreateInfo color_blending = {}; + color_blending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; + color_blending.attachmentCount = 1; + color_blending.pAttachments = &color_blend; + + VkGraphicsPipelineCreateInfo pipeline_info = {}; + pipeline_info.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; + pipeline_info.stageCount = 2; + pipeline_info.pStages = stages; + pipeline_info.pVertexInputState = &vertex_input; + pipeline_info.pInputAssemblyState = &input_assembly; + pipeline_info.pViewportState = &viewport_state; + pipeline_info.pRasterizationState = &rasterizer; + pipeline_info.pMultisampleState = &multisampling; + pipeline_info.pColorBlendState = &color_blending; + pipeline_info.layout = shader->pipeline_layout; + pipeline_info.renderPass = GetOrCreateRenderPass(render_pass_format); + pipeline_info.subpass = 0; + + VkPipeline new_pipeline = VK_NULL_HANDLE; + VkResult result = vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, + &pipeline_info, nullptr, + &new_pipeline); + if (result != VK_SUCCESS) { + qWarning() << "Failed to create Vulkan graphics pipeline:" << result; + return false; + } + + shader->pipeline_cache.insert(render_pass_format, new_pipeline); + return true; +} + +void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, + olive::Texture *destination, + VideoParams destination_params, + bool clear_destination) +{ + QMutexLocker lock(&mutex_); + + ShaderJob *s_job = dynamic_cast(&a_job); + if (!s_job) { + return; + } + ShaderJob job(*s_job); + + quint64 shader_id = shader_variant.value(); + VulkanShader *shader = shaders_.value(shader_id); + if (!shader) { + return; + } + + VulkanTexture *dest_tex = nullptr; + if (destination) { + quint64 dest_id = destination->id().value(); + dest_tex = textures_.value(dest_id); + if (!dest_tex) { + return; + } + } + + VkFormat render_pass_format = dest_tex ? dest_tex->vk_format : VK_FORMAT_R32G32B32A32_SFLOAT; + VkRenderPass render_pass = GetOrCreateRenderPass(render_pass_format); + if (render_pass == VK_NULL_HANDLE) { + return; + } + + if (!CreatePipelineForShader(shader, destination_params, render_pass_format)) { + return; + } + + VkPipeline pipeline = shader->pipeline_cache.value(render_pass_format); + + // Collect textures to bind and build UBO data + struct TextureBinding { + QString name; + VulkanTexture *tex; + Texture::Interpolation interp; + }; + QVector bindings; + + QByteArray ubo_data; + if (shader->ubo_size > 0) { + ubo_data.resize(static_cast(shader->ubo_size)); + ubo_data.fill(0); + } + + for (auto it = job.GetValues().constBegin(); it != job.GetValues().constEnd(); + ++it) { + const NodeValue &value = it.value(); + if (value.type() == NodeValue::kTexture) { + TexturePtr texture = value.toTexture(); + VulkanTexture *vtex = nullptr; + if (texture) { + quint64 tid = texture->id().value(); + vtex = textures_.value(tid); + } + bindings.append({ it.key(), vtex, + job.GetInterpolation(it.key()) }); + } else if (!shader->uniforms.isEmpty() && shader->ubo_size > 0) { + // Find matching uniform + for (const UniformInfo &u : shader->uniforms) { + if (u.name != it.key()) + continue; + char *dst = ubo_data.data() + static_cast(u.offset); + switch (value.type()) { + case NodeValue::kFloat: + *reinterpret_cast(dst) = static_cast(value.toDouble()); + break; + case NodeValue::kInt: + *reinterpret_cast(dst) = static_cast(value.toInt()); + break; + case NodeValue::kBoolean: + *reinterpret_cast(dst) = value.toBool() ? 1 : 0; + break; + case NodeValue::kVec2: { + QVector2D v = value.toVec2(); + memcpy(dst, &v, sizeof(float) * 2); + break; + } + case NodeValue::kVec3: { + QVector3D v = value.toVec3(); + memcpy(dst, &v, sizeof(float) * 3); + break; + } + case NodeValue::kVec4: { + QVector4D v = value.toVec4(); + memcpy(dst, &v, sizeof(float) * 4); + break; + } + case NodeValue::kMatrix: { + QMatrix4x4 m = value.toMatrix(); + memcpy(dst, m.constData(), sizeof(float) * 16); + break; + } + case NodeValue::kColor: { + Color c = value.toColor(); + float col[4] = { static_cast(c.red()), + static_cast(c.green()), + static_cast(c.blue()), + static_cast(c.alpha()) }; + memcpy(dst, col, sizeof(float) * 4); + break; + } + case NodeValue::kCombo: + *reinterpret_cast(dst) = value.toInt(); + break; + default: + break; + } + break; + } + } + } + + // Handle special uniforms that may not be in job values + if (shader->ubo_size > 0) { + for (const UniformInfo &u : shader->uniforms) { + char *dst = ubo_data.data() + static_cast(u.offset); + if (u.name == QStringLiteral("ove_mvpmat")) { + QMatrix4x4 m = job.Get(QStringLiteral("ove_mvpmat")).toMatrix(); + memcpy(dst, m.constData(), sizeof(float) * 16); + } else if (u.name == QStringLiteral("ove_cropmatrix")) { + QMatrix4x4 m = job.Get(QStringLiteral("ove_cropmatrix")).toMatrix(); + memcpy(dst, m.constData(), sizeof(float) * 16); + } else if (u.name == QStringLiteral("ove_maintex_alpha")) { + *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_maintex_alpha")).toInt(); + } else if (u.name == QStringLiteral("ove_force_opaque")) { + *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_force_opaque")).toBool() ? 1 : 0; + } else if (u.name == QStringLiteral("ove_iteration")) { + *reinterpret_cast(dst) = job.Get(QStringLiteral("ove_iteration")).toInt(); + } + } + } + + // Create framebuffer and render pass for this blit + VkFramebuffer framebuffer = VK_NULL_HANDLE; + if (dest_tex) { + VkFramebufferCreateInfo fb_info = {}; + fb_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + fb_info.renderPass = render_pass; + fb_info.attachmentCount = 1; + fb_info.pAttachments = &dest_tex->view; + fb_info.width = static_cast(destination_params.effective_width()); + fb_info.height = static_cast(destination_params.effective_height()); + fb_info.layers = 1; + vkCreateFramebuffer(device_, &fb_info, nullptr, &framebuffer); + } + + // Create UBO buffer if needed + VkBuffer ubo_buffer = VK_NULL_HANDLE; + VkDeviceMemory ubo_memory = VK_NULL_HANDLE; + if (shader->ubo_size > 0 && !ubo_data.isEmpty()) { + if (CreateStagingBuffer(shader->ubo_size, &ubo_buffer, &ubo_memory)) { + void *mapped; + vkMapMemory(device_, ubo_memory, 0, shader->ubo_size, 0, &mapped); + memcpy(mapped, ubo_data.constData(), static_cast(shader->ubo_size)); + vkUnmapMemory(device_, ubo_memory); + } + } + + VkCommandBuffer cmd = BeginOneTimeCommands(); + + if (dest_tex && dest_tex->current_layout != VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { + TransitionImageLayout(cmd, dest_tex->image, dest_tex->current_layout, + VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); + dest_tex->current_layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + } + + VkRenderPassBeginInfo rp_begin = {}; + rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; + rp_begin.renderPass = render_pass; + rp_begin.framebuffer = framebuffer; + rp_begin.renderArea.extent.width = + static_cast(destination_params.effective_width()); + rp_begin.renderArea.extent.height = + static_cast(destination_params.effective_height()); + + if (clear_destination) { + VkClearValue clear_val = {}; + clear_val.color = { { 0.0f, 0.0f, 0.0f, 0.0f } }; + rp_begin.clearValueCount = 1; + rp_begin.pClearValues = &clear_val; + } + + vkCmdBeginRenderPass(cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE); + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + + // Set viewport + VkViewport viewport = {}; + viewport.width = static_cast(destination_params.effective_width()); + viewport.height = static_cast(destination_params.effective_height()); + viewport.minDepth = 0.0f; + viewport.maxDepth = 1.0f; + vkCmdSetViewport(cmd, 0, 1, &viewport); + + // Set scissor + VkRect2D scissor = {}; + scissor.extent.width = + static_cast(destination_params.effective_width()); + scissor.extent.height = + static_cast(destination_params.effective_height()); + vkCmdSetScissor(cmd, 0, 1, &scissor); + + // Bind vertex buffer + VkDeviceSize offsets[] = { 0 }; + vkCmdBindVertexBuffers(cmd, 0, 1, &vertex_buffer_, offsets); + + // Create and update descriptor set + VkDescriptorSet descriptor_set = VK_NULL_HANDLE; + if (shader->ubo_size > 0 || !bindings.isEmpty()) { + VkDescriptorSetAllocateInfo ds_alloc = {}; + ds_alloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; + ds_alloc.descriptorPool = descriptor_pool_; + ds_alloc.descriptorSetCount = 1; + ds_alloc.pSetLayouts = &shader->descriptor_layout; + + VkResult result = vkAllocateDescriptorSets(device_, &ds_alloc, &descriptor_set); + if (result == VK_SUCCESS) { + QVector writes; + + // UBO binding + if (ubo_buffer != VK_NULL_HANDLE) { + VkDescriptorBufferInfo buffer_info = {}; + buffer_info.buffer = ubo_buffer; + buffer_info.offset = 0; + buffer_info.range = shader->ubo_size; + + VkWriteDescriptorSet write = {}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = descriptor_set; + write.dstBinding = 0; + write.dstArrayElement = 0; + write.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + write.descriptorCount = 1; + write.pBufferInfo = &buffer_info; + writes.append(write); + } + + // Texture samplers binding + if (!bindings.isEmpty()) { + VkSamplerCreateInfo sampler_info = {}; + sampler_info.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; + sampler_info.magFilter = VK_FILTER_LINEAR; + sampler_info.minFilter = VK_FILTER_LINEAR; + sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + VkSampler sampler; + vkCreateSampler(device_, &sampler_info, nullptr, &sampler); + + QVector image_infos; + for (int i = 0; i < bindings.size() && i < 16; i++) { + const TextureBinding &tb = bindings.at(i); + VkDescriptorImageInfo img_info = {}; + img_info.sampler = sampler; + img_info.imageView = tb.tex ? tb.tex->view : VK_NULL_HANDLE; + img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + image_infos.append(img_info); + } + + if (!image_infos.isEmpty()) { + VkWriteDescriptorSet write = {}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = descriptor_set; + write.dstBinding = 1; + write.dstArrayElement = 0; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.descriptorCount = static_cast(image_infos.size()); + write.pImageInfo = image_infos.constData(); + writes.append(write); + } + + vkDestroySampler(device_, sampler, nullptr); + } + + if (!writes.isEmpty()) { + vkUpdateDescriptorSets(device_, writes.size(), writes.constData(), 0, + nullptr); + } + + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, + shader->pipeline_layout, 0, 1, &descriptor_set, 0, + nullptr); + } + } + + // Draw + vkCmdDraw(cmd, 6, 1, 0, 0); + + vkCmdEndRenderPass(cmd); + + EndOneTimeCommands(cmd); + + if (framebuffer != VK_NULL_HANDLE) { + vkDestroyFramebuffer(device_, framebuffer, nullptr); + } + if (ubo_buffer != VK_NULL_HANDLE) { + DestroyStagingBuffer(ubo_buffer, ubo_memory); + } +} + +} // namespace olive diff --git a/app/render/vulkan/vulkanrenderer.h b/app/render/vulkan/vulkanrenderer.h new file mode 100644 index 000000000..c228220c4 --- /dev/null +++ b/app/render/vulkan/vulkanrenderer.h @@ -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 . + +***************************************************************************/ + +#ifndef VULKANRENDERER_H +#define VULKANRENDERER_H + +#include + +#include +#include +#include + +#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 *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 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 textures_; + + // Shader handle counter + quint64 next_shader_id_ = 1; + QHash shaders_; + + static const int kMaxDescriptorSets = 1024; +}; + +} + +#endif // VULKANRENDERER_H diff --git a/app/render/worker/workermain.cpp b/app/render/worker/workermain.cpp index da0816f45..68d173ef5 100644 --- a/app/render/worker/workermain.cpp +++ b/app/render/worker/workermain.cpp @@ -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(renderer_)) { + ctx = dynamic_renderer->OpenGLContext(); + } else +#endif + { + ctx = static_cast(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 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(renderer)) { + ctx = loaded_renderer->OpenGLContext(); + } else +#endif + { + ctx = static_cast(renderer)->context(); + } if (!ctx || !ctx->isValid()) { LogError(QStringLiteral("OpenGL context is not valid after init")); renderer->Destroy(); diff --git a/app/ui/style/style.cpp b/app/ui/style/style.cpp index ea6b56da4..f57764fb8 100644 --- a/app/ui/style/style.cpp +++ b/app/ui/style/style.cpp @@ -35,7 +35,6 @@ namespace olive { -const char *StyleManager::kDefaultStyle = "olive-dark"; QString StyleManager::current_style_; QMap StyleManager::available_themes_; diff --git a/app/ui/style/style.h b/app/ui/style/style.h index 4ff645040..ebd776c79 100644 --- a/app/ui/style/style.h +++ b/app/ui/style/style.h @@ -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 &available_themes() { diff --git a/app/widget/manageddisplay/manageddisplay.cpp b/app/widget/manageddisplay/manageddisplay.cpp index 1e37b1684..df58e6d88 100644 --- a/app/widget/manageddisplay/manageddisplay.cpp +++ b/app/widget/manageddisplay/manageddisplay.cpp @@ -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(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() { - MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER; + if (!is_backend_neutral_) { + MANAGEDDISPLAYWIDGET_DEFAULT_DESTRUCTOR_INNER; - if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { disconnect(static_cast(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(inner_widget_)->context(); #ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND @@ -264,6 +284,9 @@ void ManagedDisplayWidget::OnInit() #endif static_cast(attached_renderer_)->Init(context); static_cast(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(inner_widget_)->makeCurrent(); } } void ManagedDisplayWidget::doneCurrent() { - if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { + if (!is_backend_neutral_) { static_cast(inner_widget_)->doneCurrent(); } } QPaintDevice *ManagedDisplayWidget::paint_device() const { - if (RenderManager::instance()->backend() == RenderManager::kOpenGL) { - return static_cast(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(inner_widget_)->update(); + if (inner_widget_) { + inner_widget_->update(); } } diff --git a/app/widget/manageddisplay/manageddisplay.h b/app/widget/manageddisplay/manageddisplay.h index 817cbf703..1c299e9fa 100644 --- a/app/widget/manageddisplay/manageddisplay.h +++ b/app/widget/manageddisplay/manageddisplay.h @@ -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 diff --git a/app/widget/scope/scopebase/scopebase.cpp b/app/widget/scope/scopebase/scopebase.cpp index 83c17457f..42e910bfa 100644 --- a/app/widget/scope/scopebase/scopebase.cpp +++ b/app/widget/scope/scopebase/scopebase.cpp @@ -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(); diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index 73b0116a9..7037cf1fe 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -375,19 +375,40 @@ bool ViewerDisplayWidget::eventFilter(QObject *o, QEvent *e) void ViewerDisplayWidget::OnPaint() { - // 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()); + 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) { - DrawBlank(device_params); + if (!backend_neutral) { + DrawBlank(device_params); + } } else if (color_service()) { if (FramePtr frame = load_frame_.value()) { // 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(); - if (funcs && tex_id && funcs->glIsTexture(tex_id)) { - FramePtr frame = Frame::Create(); - frame->set_video_params(texture->params()); - if (frame->allocate()) { - 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) { + // Cross-renderer texture: download and re-upload + FramePtr frame = Frame::Create(); + frame->set_video_params(texture->params()); + if (frame->allocate()) { + texture->renderer()->DownloadFromTexture( + texture->id(), texture->params(), + frame->data(), frame->linesize_pixels()); + texture_ = renderer()->CreateTexture( + frame->video_params(), frame->data(), + frame->linesize_pixels()); + } else { texture_ = texture; } } else { @@ -445,7 +456,9 @@ void ViewerDisplayWidget::OnPaint() TexturePtr texture_to_draw = texture_; if (!texture_to_draw || texture_to_draw->IsDummy()) { - DrawBlank(device_params); + 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(width() * devicePixelRatioF()); + const int texture_height = + static_cast(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( + 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; diff --git a/app/widget/viewer/viewerdisplay.h b/app/widget/viewer/viewerdisplay.h index 268a95e65..d312580b5 100644 --- a/app/widget/viewer/viewerdisplay.h +++ b/app/widget/viewer/viewerdisplay.h @@ -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 */ diff --git a/docs/build.md b/docs/build.md index 6e53b5eed..713309c34 100644 --- a/docs/build.md +++ b/docs/build.md @@ -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`) | --- diff --git a/docs/zh/build.md b/docs/zh/build.md index a2b4e0baf..dc051efbe 100644 --- a/docs/zh/build.md +++ b/docs/zh/build.md @@ -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`) | --- diff --git a/docs/zh/render-backend-dynamic-plan.md b/docs/zh/render-backend-dynamic-plan.md index 8df1069f6..fc89ca4e5 100644 --- a/docs/zh/render-backend-dynamic-plan.md +++ b/docs/zh/render-backend-dynamic-plan.md @@ -48,58 +48,93 @@ 这一步是 ABI 稳定化,不是把后端内部实现改成 C。 -## 阶段 2.5:最小化 OpenGL 后端链接边界 +## 阶段 2.5:最小化 OpenGL/Vulkan 后端链接边界(已完成) -当前工程大量代码和静态依赖被编译进 `libolive-editor` object 库。直接把整个 object 库塞进 `liboakgl.so` 会带来两个问题: +此前 `oakgl`/`oakvulkan` 通过 `$` 把整个 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、导出等完整路径。 diff --git a/tests/gtest/dynamic_render_backend_test.cpp b/tests/gtest/dynamic_render_backend_test.cpp index 1895bdb8f..5bd431e91 100644 --- a/tests/gtest/dynamic_render_backend_test.cpp +++ b/tests/gtest/dynamic_render_backend_test.cpp @@ -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 diff --git a/tests/gtest/plugin_ofx_integration_test.cpp b/tests/gtest/plugin_ofx_integration_test.cpp index 9951225f1..6a17bbb12 100644 --- a/tests/gtest/plugin_ofx_integration_test.cpp +++ b/tests/gtest/plugin_ofx_integration_test.cpp @@ -119,7 +119,7 @@ TEST(PluginIntegration, ChromaKeyerCreateAndRender) olive::plugin::PluginJob job(instance, nullptr, row); olive::TexturePtr output = std::make_shared(params); - olive::plugin::PluginRenderer renderer; + olive::plugin::PluginRenderer renderer(nullptr); renderer.RenderPlugin(input, job, output, params, true, false); EXPECT_TRUE(output->frame()); diff --git a/tests/gtest/plugin_ofx_misc_test.cpp b/tests/gtest/plugin_ofx_misc_test.cpp index 89741e4fa..17d03b2e5 100644 --- a/tests/gtest/plugin_ofx_misc_test.cpp +++ b/tests/gtest/plugin_ofx_misc_test.cpp @@ -190,7 +190,7 @@ bool RenderPlugin(const std::string &plugin_id, PluginJob job(instance, nullptr, inputs); TexturePtr output = std::make_shared(params); - PluginRenderer renderer; + PluginRenderer renderer(nullptr); renderer.RenderPlugin(nullptr, job, output, params, true, false); bool has_frame = output->frame() != nullptr;