diff --git a/app/render/backend/dynamicrenderer.cpp b/app/render/backend/dynamicrenderer.cpp index a2c7b8e6d..a81eb3fd5 100644 --- a/app/render/backend/dynamicrenderer.cpp +++ b/app/render/backend/dynamicrenderer.cpp @@ -9,12 +9,16 @@ namespace olive { +// Stores the requested backend name; the actual backend may later become +// OpenGL if loading or availability checks require a Vulkan fallback. DynamicRenderer::DynamicRenderer(const QString &backend, QObject *parent) : Renderer(parent) , backend_(backend.toLower()) { } +// Tears down the backend in the reverse order used by Load(): release renderer +// resources, destroy the opaque backend object, then unload the shared library. DynamicRenderer::~DynamicRenderer() { Destroy(); @@ -28,6 +32,9 @@ DynamicRenderer::~DynamicRenderer() } } +// Builds the private backend library path for the current platform. +// The search is intentionally restricted to Oak-controlled directories so a +// system libGL/libvulkan loader is never mistaken for an Oak render backend. QString DynamicRenderer::LibraryFilename() const { const QString base = backend_ == QStringLiteral("vulkan") @@ -58,6 +65,9 @@ QString DynamicRenderer::LibraryFilename() const return candidates.first(); } +// Loads the selected backend, resolves its C ABI table, creates the opaque +// backend object, and optionally falls back from Vulkan to OpenGL when runtime +// availability checks fail. bool DynamicRenderer::Load() { if (handle_) { @@ -106,6 +116,8 @@ bool DynamicRenderer::Load() return handle_ != nullptr; } +// Resolves the mandatory C ABI entry points from the loaded shared library. +// Optional information probes are resolved after the required render interface. bool DynamicRenderer::ResolveFunctions() { ResetFunctions(); @@ -155,6 +167,8 @@ bool DynamicRenderer::ResolveFunctions() return true; } +// Discards a partially-created backend and restarts loading with the OpenGL +// backend. This keeps RenderManager's fallback path inside the adapter. bool DynamicRenderer::FallbackToOpenGL() { if (handle_ && destroy_) { @@ -169,6 +183,8 @@ bool DynamicRenderer::FallbackToOpenGL() return Load(); } +// Clears all cached C function pointers so a failed backend cannot leave stale +// call targets behind for a later fallback load. void DynamicRenderer::ResetFunctions() { create_ = nullptr; @@ -195,16 +211,20 @@ void DynamicRenderer::ResetFunctions() opengl_context_ = nullptr; } +// Returns backend metadata exposed by the dynamic library when available. bool DynamicRenderer::GetBackendInfo(OakRenderBackendInfo *out_info) const { return handle_ && get_info_ && out_info && get_info_(handle_, out_info); } +// Initializes the loaded backend using its own context/device creation path. bool DynamicRenderer::Init() { return Load() && init_(handle_); } +// Initializes an OpenGL backend against an existing widget context; non-OpenGL +// backends may ignore the context on the library side. bool DynamicRenderer::InitWithOpenGLContext(QOpenGLContext *context) { if (!Load()) { @@ -214,6 +234,8 @@ bool DynamicRenderer::InitWithOpenGLContext(QOpenGLContext *context) return true; } +// Forwards post-destroy cleanup to the backend while the library is still +// loaded and its symbols are still valid. void DynamicRenderer::PostDestroy() { if (handle_ && post_destroy_) { @@ -221,6 +243,8 @@ void DynamicRenderer::PostDestroy() } } +// Runs backend post-initialization after Init/InitWithOpenGLContext has +// established the device or GL context. void DynamicRenderer::PostInit() { if (handle_) { @@ -228,12 +252,15 @@ void DynamicRenderer::PostInit() } } +// Forwards render target clearing through the C ABI. void DynamicRenderer::ClearDestination(Texture *texture, double r, double g, double b, double a) { clear_destination_(handle_, texture, r, g, b, a); } +// Creates a backend-native shader and receives the result as an opaque QVariant +// because this first-generation ABI still shares C++/Qt types between modules. QVariant DynamicRenderer::CreateNativeShader(ShaderCode code) { QVariant out; @@ -241,11 +268,13 @@ QVariant DynamicRenderer::CreateNativeShader(ShaderCode code) return out; } +// Releases a backend-native shader handle. void DynamicRenderer::DestroyNativeShader(QVariant shader) { destroy_native_shader_(handle_, &shader); } +// Uploads CPU pixel data into a backend texture through the dynamic ABI. void DynamicRenderer::UploadToTexture(const QVariant &handle, const VideoParams ¶ms, const void *data, int linesize) @@ -253,6 +282,7 @@ void DynamicRenderer::UploadToTexture(const QVariant &handle, upload_to_texture_(handle_, &handle, ¶ms, data, linesize); } +// Downloads backend texture data into a caller-provided CPU buffer. void DynamicRenderer::DownloadFromTexture(const QVariant &handle, const VideoParams ¶ms, void *data, int linesize) @@ -260,11 +290,13 @@ void DynamicRenderer::DownloadFromTexture(const QVariant &handle, download_from_texture_(handle_, &handle, ¶ms, data, linesize); } +// Waits for backend work to become visible to subsequent CPU or GPU consumers. void DynamicRenderer::Flush() { flush_(handle_); } +// Reads a single pixel through the backend-provided readback hook. Color DynamicRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt) { Color out; @@ -272,6 +304,8 @@ Color DynamicRenderer::GetPixelFromTexture(Texture *texture, const QPointF &pt) return out; } +// Exposes the wrapped OpenGL context when the backend is OpenGL; Vulkan returns +// null so callers can avoid GL-only paths. QOpenGLContext *DynamicRenderer::OpenGLContext() const { return opengl_context_ && handle_ @@ -279,11 +313,13 @@ QOpenGLContext *DynamicRenderer::OpenGLContext() const : nullptr; } +// Reports the effective backend after any load-time fallback has completed. bool DynamicRenderer::IsOpenGL() const { return backend_ == QStringLiteral("opengl"); } +// Dispatches a shader blit to the loaded backend. void DynamicRenderer::Blit(QVariant shader, AcceleratedJob &job, Texture *destination, VideoParams destination_params, bool clear_destination) @@ -292,6 +328,7 @@ void DynamicRenderer::Blit(QVariant shader, AcceleratedJob &job, clear_destination); } +// Allocates a backend-native texture and wraps its opaque handle in QVariant. QVariant DynamicRenderer::CreateNativeTexture(int width, int height, int depth, PixelFormat format, int channel_count, const void *data, int linesize) @@ -302,11 +339,14 @@ QVariant DynamicRenderer::CreateNativeTexture(int width, int height, int depth, return out; } +// Releases a backend-native texture handle. void DynamicRenderer::DestroyNativeTexture(QVariant texture) { destroy_native_texture_(handle_, &texture); } +// Releases renderer-owned backend resources before the backend object itself is +// destroyed. void DynamicRenderer::DestroyInternal() { if (handle_) { @@ -314,6 +354,7 @@ void DynamicRenderer::DestroyInternal() } } +// Exposes OFX OpenGL output binding through the dynamic backend when supported. void DynamicRenderer::AttachOutputTexture(Texture *texture) { if (attach_output_texture_ && texture) { @@ -322,6 +363,7 @@ void DynamicRenderer::AttachOutputTexture(Texture *texture) } } +// Clears any OFX output texture binding owned by the backend. void DynamicRenderer::DetachOutputTexture() { if (detach_output_texture_) { diff --git a/app/render/backend/dynamicrenderer.h b/app/render/backend/dynamicrenderer.h index 772c44f2a..f25794924 100644 --- a/app/render/backend/dynamicrenderer.h +++ b/app/render/backend/dynamicrenderer.h @@ -11,62 +11,92 @@ namespace olive { +// C++ Renderer adapter that loads an Oak render backend shared library and +// forwards Renderer calls through the backend's C ABI. class DynamicRenderer : public Renderer, public OpenGLContextProvider { Q_OBJECT public: + // Stores the requested backend name; Load() may change it after fallback. explicit DynamicRenderer(const QString &backend, QObject *parent = nullptr); + // Destroys backend resources and unloads the dynamic library. virtual ~DynamicRenderer() override; using Renderer::Blit; + // Loads the backend library, resolves C ABI symbols, and creates the handle. bool Load(); + // Initializes an OpenGL backend with a caller-owned viewer context. bool InitWithOpenGLContext(QOpenGLContext *context); + // Retrieves backend metadata through the optional info entry point. bool GetBackendInfo(OakRenderBackendInfo *out_info) const; + // Returns the effective backend after any load-time fallback. QString backend_name() const { return backend_; } + // Initializes the backend using its default device/context path. virtual bool Init() override; + // Runs backend post-destroy cleanup. virtual void PostDestroy() override; + // Runs backend post-init setup. virtual void PostInit() override; + // Clears either a native texture destination or the backend output target. virtual void ClearDestination(Texture *texture = nullptr, double r = 0.0, double g = 0.0, double b = 0.0, double a = 0.0) override; + // Creates a native shader through the dynamic backend. virtual QVariant CreateNativeShader(ShaderCode code) override; + // Destroys a native shader through the dynamic backend. virtual void DestroyNativeShader(QVariant shader) override; + // Uploads CPU pixels to a backend texture. virtual void UploadToTexture(const QVariant &handle, const VideoParams ¶ms, const void *data, int linesize) override; + // Downloads backend texture pixels to CPU memory. virtual void DownloadFromTexture(const QVariant &handle, const VideoParams ¶ms, void *data, int linesize) override; + // Waits for backend work to complete. virtual void Flush() override; + // Reads one pixel from a backend texture. virtual Color GetPixelFromTexture(Texture *texture, const QPointF &pt) override; + // Returns the wrapped OpenGL context for OpenGL backends. virtual QOpenGLContext *OpenGLContext() const override; + // Reports whether the effective backend is OpenGL. virtual bool IsOpenGL() const override; + // Attaches a texture for OFX OpenGL output when supported. virtual void AttachOutputTexture(Texture *texture) override; + // Detaches any OFX output texture binding when supported. virtual void DetachOutputTexture() override; protected: + // Dispatches a shader blit through the dynamic backend. virtual void Blit(QVariant shader, AcceleratedJob &job, Texture *destination, VideoParams destination_params, bool clear_destination) override; + // Allocates a native texture through the dynamic backend. virtual QVariant CreateNativeTexture(int width, int height, int depth, PixelFormat format, int channel_count, const void *data = nullptr, int linesize = 0) override; + // Releases a native texture through the dynamic backend. virtual void DestroyNativeTexture(QVariant texture) override; + // Releases backend-owned renderer resources. virtual void DestroyInternal() override; private: + // Resolves required backend C ABI symbols. bool ResolveFunctions(); + // Replaces a failed Vulkan backend with OpenGL. bool FallbackToOpenGL(); + // Clears all cached function pointers. void ResetFunctions(); + // Resolves the private backend library path. QString LibraryFilename() const; QString backend_; diff --git a/app/render/backend/renderbackend_c.h b/app/render/backend/renderbackend_c.h index 3405bd78e..173f55d6c 100644 --- a/app/render/backend/renderbackend_c.h +++ b/app/render/backend/renderbackend_c.h @@ -14,14 +14,17 @@ extern "C" { #endif +/* Opaque pointer to the backend-owned C++ renderer object. */ typedef void *OakRenderBackendHandle; +/* Identifies the concrete backend behind a dynamically loaded library. */ enum OakRenderBackendKind { OAK_RENDER_BACKEND_UNKNOWN = 0, OAK_RENDER_BACKEND_OPENGL = 1, OAK_RENDER_BACKEND_VULKAN = 2 }; +/* Capability bits advertised by a backend through oak_renderer_get_info(). */ enum OakRenderBackendCapability { OAK_RENDER_BACKEND_CAP_TEXTURES = 1ULL << 0, OAK_RENDER_BACKEND_CAP_SHADERS = 1ULL << 1, @@ -32,6 +35,7 @@ enum OakRenderBackendCapability { OAK_RENDER_BACKEND_CAP_DEVICE = 1ULL << 6 }; +/* Static and runtime metadata returned by the backend. */ struct OakRenderBackendInfo { uint32_t abi_version; uint32_t kind; @@ -40,52 +44,74 @@ struct OakRenderBackendInfo { const char *status; }; +/* Creates a backend renderer object. */ typedef OakRenderBackendHandle (*OakBackendCreateFn)(void *parent); +/* Destroys a backend renderer object created by OakBackendCreateFn. */ typedef void (*OakBackendDestroyFn)(OakRenderBackendHandle handle); +/* Queries backend metadata and capability bits. */ typedef bool (*OakBackendGetInfoFn)(OakRenderBackendHandle handle, struct OakRenderBackendInfo *out_info); +/* Checks whether the backend can run on the current machine. */ typedef bool (*OakBackendIsAvailableFn)(OakRenderBackendHandle handle); +/* Initializes backend-owned device/context resources. */ typedef bool (*OakBackendInitFn)(OakRenderBackendHandle handle); +/* Initializes the backend against a caller-supplied GL context when applicable. */ typedef void (*OakBackendInitWithContextFn)(OakRenderBackendHandle handle, void *context); +/* Runs backend post-initialization after the device/context exists. */ typedef void (*OakBackendPostInitFn)(OakRenderBackendHandle handle); +/* Runs backend post-destroy cleanup before the library unloads. */ typedef void (*OakBackendPostDestroyFn)(OakRenderBackendHandle handle); +/* Destroys renderer-owned native resources. */ typedef void (*OakBackendDestroyInternalFn)(OakRenderBackendHandle handle); +/* Clears a texture destination or implicit output target. */ typedef void (*OakBackendClearDestinationFn)(OakRenderBackendHandle handle, void *texture, double r, double g, double b, double a); +/* Creates a native texture and writes a QVariant-compatible handle. */ typedef void (*OakBackendCreateNativeTextureFn)(OakRenderBackendHandle handle, int width, int height, int depth, int format, int channel_count, const void *data, int linesize, void *out_variant); +/* Destroys a native texture represented by a QVariant-compatible handle. */ typedef void (*OakBackendDestroyNativeTextureFn)(OakRenderBackendHandle handle, const void *variant); +/* Creates a native shader and writes a QVariant-compatible handle. */ typedef void (*OakBackendCreateNativeShaderFn)(OakRenderBackendHandle handle, const void *shader_code, void *out_variant); +/* Destroys a native shader represented by a QVariant-compatible handle. */ typedef void (*OakBackendDestroyNativeShaderFn)(OakRenderBackendHandle handle, const void *variant); +/* Uploads CPU pixel data to a native texture. */ typedef void (*OakBackendUploadToTextureFn)(OakRenderBackendHandle handle, const void *variant, const void *video_params, const void *data, int linesize); +/* Downloads native texture pixels into caller-owned CPU memory. */ typedef void (*OakBackendDownloadFromTextureFn)(OakRenderBackendHandle handle, const void *variant, const void *video_params, void *data, int linesize); +/* Waits for backend work that must be visible to later operations. */ typedef void (*OakBackendFlushFn)(OakRenderBackendHandle handle); +/* Reads one pixel from a texture. */ typedef void (*OakBackendGetPixelFromTextureFn)(OakRenderBackendHandle handle, void *texture, const void *point, void *out_color); +/* Executes a shader blit job. */ typedef void (*OakBackendBlitFn)(OakRenderBackendHandle handle, const void *shader, void *job, void *destination, const void *destination_params, bool clear_destination); +/* Attaches an output texture for OFX OpenGL rendering when supported. */ typedef void (*OakBackendAttachOutputTextureFn)(OakRenderBackendHandle handle, const void *texture_id); +/* Detaches an OFX output texture when supported. */ typedef void (*OakBackendDetachOutputTextureFn)(OakRenderBackendHandle handle); +/* Returns the backend OpenGL context, or null for non-OpenGL backends. */ typedef void *(*OakBackendOpenGLContextFn)(OakRenderBackendHandle handle); #ifdef __cplusplus diff --git a/app/render/opengl/openglbackend_c.cpp b/app/render/opengl/openglbackend_c.cpp index 178644ee4..c1896f739 100644 --- a/app/render/opengl/openglbackend_c.cpp +++ b/app/render/opengl/openglbackend_c.cpp @@ -23,11 +23,14 @@ public: using olive::OpenGLRenderer::DetachTextureAsDestination; }; +// Converts the opaque C ABI handle back to the C++ renderer used internally. BackendOpenGLRenderer *Renderer(OakRenderBackendHandle handle) { return static_cast(handle); } +// Interprets ABI QVariant payloads without copying; both modules are built +// against the same Qt/C++ ABI in this first-generation dynamic backend. const QVariant &VariantRef(const void *variant) { return *static_cast(variant); @@ -35,16 +38,19 @@ const QVariant &VariantRef(const void *variant) } // namespace +// Creates the backend object and returns it as an opaque C handle. OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent) { return new BackendOpenGLRenderer(static_cast(parent)); } +// Destroys the opaque backend object created by oak_renderer_create(). OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy(OakRenderBackendHandle handle) { delete Renderer(handle); } +// Reports static OpenGL backend capabilities to the adapter. OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info( OakRenderBackendHandle handle, OakRenderBackendInfo *out_info) { @@ -62,39 +68,47 @@ OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info( return true; } +// OpenGL availability is context-dependent, so object creation is the minimum +// availability signal for this backend. OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available( OakRenderBackendHandle handle) { return handle != nullptr; } +// Initializes an offscreen OpenGL context for non-viewer users. OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle) { return Renderer(handle)->Init(); } +// Initializes the backend against a caller-owned viewer OpenGL context. OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context( OakRenderBackendHandle handle, void *context) { Renderer(handle)->Init(static_cast(context)); } +// Runs renderer post-initialization once the GL context is available. OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_init(OakRenderBackendHandle handle) { Renderer(handle)->PostInit(); } +// Releases post-init OpenGL surface/context state. OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_destroy(OakRenderBackendHandle handle) { Renderer(handle)->PostDestroy(); } +// Releases renderer-owned GL resources before object destruction. OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_internal( OakRenderBackendHandle handle) { Renderer(handle)->DestroyInternal(); } +// Clears either the widget framebuffer or a texture destination. OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination( OakRenderBackendHandle handle, void *texture, double r, double g, double b, double a) @@ -103,6 +117,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination( r, g, b, a); } +// Creates an OpenGL texture and writes its QVariant handle to out_variant. 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) @@ -112,12 +127,14 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture( channel_count, data, linesize); } +// Destroys an OpenGL texture represented by a QVariant handle. OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture( OakRenderBackendHandle handle, const void *variant) { Renderer(handle)->DestroyNativeTexture(VariantRef(variant)); } +// Compiles an OpenGL shader program and returns its QVariant handle. OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader( OakRenderBackendHandle handle, const void *shader_code, void *out_variant) { @@ -125,12 +142,14 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader( *static_cast(shader_code)); } +// Destroys an OpenGL shader program represented by a QVariant handle. OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader( OakRenderBackendHandle handle, const void *variant) { Renderer(handle)->DestroyNativeShader(VariantRef(variant)); } +// Uploads CPU pixel data into an OpenGL texture. OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture( OakRenderBackendHandle handle, const void *variant, const void *video_params, const void *data, int linesize) @@ -140,6 +159,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture( data, linesize); } +// Reads an OpenGL texture back to CPU memory. OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture( OakRenderBackendHandle handle, const void *variant, const void *video_params, void *data, int linesize) @@ -149,11 +169,13 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture( data, linesize); } +// Flushes/waits for pending OpenGL work as required by the renderer. OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle) { Renderer(handle)->Flush(); } +// Reads one pixel from an OpenGL texture. OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture( OakRenderBackendHandle handle, void *texture, const void *point, void *out_color) @@ -162,6 +184,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture( static_cast(texture), *static_cast(point)); } +// Executes a shader blit through the wrapped C++ OpenGL renderer. OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( OakRenderBackendHandle handle, const void *shader, void *job, void *destination, const void *destination_params, bool clear_destination) @@ -173,18 +196,21 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( clear_destination); } +// Exposes the wrapped OpenGL context for GL-specific integrations. OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context( OakRenderBackendHandle handle) { return Renderer(handle)->context(); } +// Binds an output texture for OFX OpenGL rendering. OAK_RENDER_BACKEND_EXPORT void oak_renderer_attach_output_texture( OakRenderBackendHandle handle, const void *texture_id) { Renderer(handle)->AttachTextureAsDestination(VariantRef(texture_id)); } +// Detaches any OFX OpenGL output texture binding. OAK_RENDER_BACKEND_EXPORT void oak_renderer_detach_output_texture( OakRenderBackendHandle handle) { diff --git a/app/render/opengl/openglrenderer.cpp b/app/render/opengl/openglrenderer.cpp index 253920869..4d1968bc8 100644 --- a/app/render/opengl/openglrenderer.cpp +++ b/app/render/opengl/openglrenderer.cpp @@ -408,6 +408,8 @@ void OpenGLRenderer::Flush() } } +// Adapts the generic Renderer output attachment hook to OpenGL's framebuffer +// attachment path used by OFX OpenGL rendering. void OpenGLRenderer::AttachOutputTexture(olive::Texture *texture) { if (texture) { @@ -415,6 +417,7 @@ void OpenGLRenderer::AttachOutputTexture(olive::Texture *texture) } } +// Clears the framebuffer attachment installed by AttachOutputTexture(). void OpenGLRenderer::DetachOutputTexture() { DetachTextureAsDestination(); @@ -987,6 +990,9 @@ bool OpenGLRenderer::EnsureContextCurrent(const char *caller) return false; } + // QOpenGLContext may only be made current from its owning thread. Viewer + // paint code can receive textures produced by a render-thread OpenGL + // renderer, so guard here before makeCurrent() can crash inside Qt/GL. if (context_->thread() != QThread::currentThread()) { qWarning() << caller << "called from the wrong thread for this OpenGL context"; return false; diff --git a/app/render/vulkan/vulkanbackend_c.cpp b/app/render/vulkan/vulkanbackend_c.cpp index daa699ba7..0b80850e5 100644 --- a/app/render/vulkan/vulkanbackend_c.cpp +++ b/app/render/vulkan/vulkanbackend_c.cpp @@ -22,11 +22,14 @@ public: using olive::VulkanRenderer::DestroyNativeTexture; }; +// Converts the opaque C ABI handle back to the C++ Vulkan renderer. BackendVulkanRenderer *Renderer(OakRenderBackendHandle handle) { return static_cast(handle); } +// Interprets ABI QVariant payloads without copying; this ABI version assumes +// the host and backend are built with the same Qt/C++ ABI. const QVariant &VariantRef(const void *variant) { return *static_cast(variant); @@ -34,16 +37,19 @@ const QVariant &VariantRef(const void *variant) } // namespace +// Creates the Vulkan backend object and returns it as an opaque C handle. OAK_RENDER_BACKEND_EXPORT OakRenderBackendHandle oak_renderer_create(void *parent) { return new BackendVulkanRenderer(static_cast(parent)); } +// Destroys the opaque backend object created by oak_renderer_create(). OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy(OakRenderBackendHandle handle) { delete Renderer(handle); } +// Reports Vulkan backend capabilities and runtime availability status. OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info( OakRenderBackendHandle handle, OakRenderBackendInfo *out_info) { @@ -60,6 +66,8 @@ OAK_RENDER_BACKEND_EXPORT bool oak_renderer_get_info( return true; } +// Probes runtime availability by trying Init() once; this lets missing ICDs or +// unusable drivers fall back before normal rendering starts. OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available( OakRenderBackendHandle handle) { @@ -74,11 +82,13 @@ OAK_RENDER_BACKEND_EXPORT bool oak_renderer_is_available( return r->IsAvailable(); } +// Initializes the Vulkan device path. OAK_RENDER_BACKEND_EXPORT bool oak_renderer_init(OakRenderBackendHandle handle) { return Renderer(handle)->Init(); } +// Vulkan does not use a QOpenGLContext; the argument is accepted for ABI parity. OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context( OakRenderBackendHandle handle, void *context) { @@ -86,24 +96,28 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_init_with_context( Renderer(handle)->Init(); } +// Creates reusable Vulkan resources after device initialization. OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_init( OakRenderBackendHandle handle) { Renderer(handle)->PostInit(); } +// Reserved for API symmetry; Vulkan cleanup is handled by destroy_internal. OAK_RENDER_BACKEND_EXPORT void oak_renderer_post_destroy( OakRenderBackendHandle handle) { Renderer(handle)->PostDestroy(); } +// Releases all Vulkan resources owned by the renderer. OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_internal( OakRenderBackendHandle handle) { Renderer(handle)->DestroyInternal(); } +// Clears a Vulkan texture destination. OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination( OakRenderBackendHandle handle, void *texture, double r, double g, double b, double a) @@ -112,6 +126,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_clear_destination( r, g, b, a); } +// Creates a Vulkan texture and writes its QVariant handle to out_variant. 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) @@ -121,12 +136,14 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_texture( channel_count, data, linesize); } +// Destroys a Vulkan texture represented by a QVariant handle. OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_texture( OakRenderBackendHandle handle, const void *variant) { Renderer(handle)->DestroyNativeTexture(VariantRef(variant)); } +// Compiles a Vulkan shader and returns its QVariant handle. OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader( OakRenderBackendHandle handle, const void *shader_code, void *out_variant) { @@ -134,12 +151,14 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_create_native_shader( *static_cast(shader_code)); } +// Destroys a Vulkan shader represented by a QVariant handle. OAK_RENDER_BACKEND_EXPORT void oak_renderer_destroy_native_shader( OakRenderBackendHandle handle, const void *variant) { Renderer(handle)->DestroyNativeShader(VariantRef(variant)); } +// Uploads CPU pixel data into a Vulkan texture. OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture( OakRenderBackendHandle handle, const void *variant, const void *video_params, const void *data, int linesize) @@ -149,6 +168,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_upload_to_texture( data, linesize); } +// Downloads a Vulkan texture to CPU memory. OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture( OakRenderBackendHandle handle, const void *variant, const void *video_params, void *data, int linesize) @@ -158,11 +178,13 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_download_from_texture( data, linesize); } +// Waits for all queued Vulkan work to finish. OAK_RENDER_BACKEND_EXPORT void oak_renderer_flush(OakRenderBackendHandle handle) { Renderer(handle)->Flush(); } +// Reads one pixel from a Vulkan texture. OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture( OakRenderBackendHandle handle, void *texture, const void *point, void *out_color) @@ -171,6 +193,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_get_pixel_from_texture( static_cast(texture), *static_cast(point)); } +// Executes a shader blit through the wrapped Vulkan renderer. OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( OakRenderBackendHandle handle, const void *shader, void *job, void *destination, const void *destination_params, bool clear_destination) @@ -182,6 +205,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_blit( clear_destination); } +// Vulkan has no OpenGL context; return null so callers avoid GL-only paths. OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context( OakRenderBackendHandle handle) { @@ -189,6 +213,7 @@ OAK_RENDER_BACKEND_EXPORT void *oak_renderer_opengl_context( return nullptr; } +// OFX OpenGL output attachment is unsupported in Vulkan and intentionally no-op. OAK_RENDER_BACKEND_EXPORT void oak_renderer_attach_output_texture( OakRenderBackendHandle handle, const void *texture_id) { @@ -197,6 +222,7 @@ OAK_RENDER_BACKEND_EXPORT void oak_renderer_attach_output_texture( // Vulkan does not support OFX OpenGL render output attachment. } +// OFX OpenGL output detachment is unsupported in Vulkan and intentionally no-op. OAK_RENDER_BACKEND_EXPORT void oak_renderer_detach_output_texture( OakRenderBackendHandle handle) { diff --git a/app/render/vulkan/vulkanrenderer.cpp b/app/render/vulkan/vulkanrenderer.cpp index 9e7c6bcad..3964dd6dd 100644 --- a/app/render/vulkan/vulkanrenderer.cpp +++ b/app/render/vulkan/vulkanrenderer.cpp @@ -60,16 +60,21 @@ static const float kBlitVertices[] = { 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, }; +// Constructs the renderer; Vulkan resources are created lazily so unavailable +// Vulkan systems can still instantiate the object and report fallback state. VulkanRenderer::VulkanRenderer(QObject *parent) : Renderer(parent) { } +// Ensures Vulkan resources are destroyed before the QObject hierarchy goes away. VulkanRenderer::~VulkanRenderer() { Destroy(); PostDestroy(); } +// Initializes the Vulkan instance/device path once. Repeated calls are accepted +// because backend availability probes may call Init() before normal rendering. bool VulkanRenderer::Init() { if (instance_ != VK_NULL_HANDLE) { @@ -79,6 +84,7 @@ bool VulkanRenderer::Init() CreateDescriptorPool(); } +// Creates reusable draw resources after Init() has a valid logical device. void VulkanRenderer::PostInit() { if (vertex_buffer_ != VK_NULL_HANDLE) { @@ -89,10 +95,15 @@ void VulkanRenderer::PostInit() CreateNearestSampler(); } +// Reserved for Renderer API symmetry; Vulkan teardown is centralized in +// DestroyInternal() so object destruction and explicit Destroy() share a path. void VulkanRenderer::PostDestroy() { } +// Destroys all Vulkan objects in dependency order. The device is idled first so +// cached textures, pipelines, descriptor pools, and command pools are no longer +// referenced by in-flight work. void VulkanRenderer::DestroyInternal() { if (device_ != VK_NULL_HANDLE) { @@ -185,6 +196,7 @@ void VulkanRenderer::DestroyInternal() } } +// Creates the minimal Vulkan instance needed for offscreen rendering. bool VulkanRenderer::CreateInstance() { VkApplicationInfo app_info = {}; @@ -208,6 +220,8 @@ bool VulkanRenderer::CreateInstance() return true; } +// Selects the first physical device with a graphics queue and creates a logical +// device without swapchain extensions because viewer output is CPU readback. bool VulkanRenderer::CreateDevice() { VkResult result = vkEnumeratePhysicalDevices(instance_, &physical_device_count_, nullptr); @@ -284,6 +298,8 @@ bool VulkanRenderer::CreateDevice() return true; } +// Creates the command pool used for short-lived transfer and draw command +// buffers. bool VulkanRenderer::CreateCommandPool() { VkCommandPoolCreateInfo pool_info = {}; @@ -299,6 +315,8 @@ bool VulkanRenderer::CreateCommandPool() return true; } +// Creates a pool large enough for transient per-blit descriptor sets. Descriptor +// sets are freed after each pass, so this is capacity rather than lifetime count. bool VulkanRenderer::CreateDescriptorPool() { VkDescriptorPoolSize pool_sizes[2] = {}; @@ -323,6 +341,7 @@ bool VulkanRenderer::CreateDescriptorPool() return true; } +// Returns a cached render pass keyed by color format and load operation. VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format, bool clear) { const quint64 key = (static_cast(format) << 1) | (clear ? 1ULL : 0ULL); @@ -387,6 +406,7 @@ VkRenderPass VulkanRenderer::GetOrCreateRenderPass(VkFormat format, bool clear) } +// Uploads a fullscreen quad to device-local memory through a staging buffer. bool VulkanRenderer::CreateVertexBuffer() { VkDeviceSize buffer_size = sizeof(kBlitVertices); @@ -486,6 +506,7 @@ bool VulkanRenderer::CreateVertexBuffer() return true; } +// Creates the persistent linear sampler shared by all texture bindings. bool VulkanRenderer::CreateLinearSampler() { VkSamplerCreateInfo sampler_info = {}; @@ -512,6 +533,7 @@ bool VulkanRenderer::CreateLinearSampler() return true; } +// Creates the persistent nearest sampler shared by all texture bindings. bool VulkanRenderer::CreateNearestSampler() { VkSamplerCreateInfo sampler_info = {}; @@ -538,6 +560,7 @@ bool VulkanRenderer::CreateNearestSampler() return true; } +// Maps Oak interpolation settings to persistent Vulkan sampler objects. VkSampler VulkanRenderer::GetSampler(Texture::Interpolation interpolation) const { switch (interpolation) { @@ -550,6 +573,7 @@ VkSampler VulkanRenderer::GetSampler(Texture::Interpolation interpolation) const } } +// Allocates host-visible coherent memory for one upload/download transfer. bool VulkanRenderer::CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer, VkDeviceMemory *out_memory) { @@ -596,6 +620,7 @@ bool VulkanRenderer::CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer return true; } +// Releases a staging buffer and its memory allocation. void VulkanRenderer::DestroyStagingBuffer(VkBuffer buffer, VkDeviceMemory memory) { if (buffer != VK_NULL_HANDLE) { @@ -606,6 +631,7 @@ void VulkanRenderer::DestroyStagingBuffer(VkBuffer buffer, VkDeviceMemory memory } } +// Starts a primary command buffer intended for immediate submit-and-wait use. VkCommandBuffer VulkanRenderer::BeginOneTimeCommands() { VkCommandBufferAllocateInfo alloc_info = {}; @@ -625,6 +651,7 @@ VkCommandBuffer VulkanRenderer::BeginOneTimeCommands() return cmd; } +// Submits a one-time command buffer and waits synchronously for completion. void VulkanRenderer::EndOneTimeCommands(VkCommandBuffer cmd) { vkEndCommandBuffer(cmd); @@ -640,6 +667,8 @@ void VulkanRenderer::EndOneTimeCommands(VkCommandBuffer cmd) vkFreeCommandBuffers(device_, command_pool_, 1, &cmd); } +// Emits a conservative barrier for the image layout transitions used by this +// renderer: upload, shader read, color attachment, clear, and readback. void VulkanRenderer::TransitionImageLayout(VkCommandBuffer cmd, VkImage image, VkImageLayout old_layout, VkImageLayout new_layout) @@ -748,6 +777,7 @@ void VulkanRenderer::TransitionImageLayout(VkCommandBuffer cmd, VkImage image, } +// Records a buffer-to-image copy for tightly packed texture uploads. void VulkanRenderer::CopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer, VkImage image, uint32_t width, uint32_t height, uint32_t depth) @@ -767,6 +797,7 @@ void VulkanRenderer::CopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer, 1, ®ion); } +// Records an image-to-buffer copy for full texture downloads or one-pixel reads. void VulkanRenderer::CopyImageToBuffer(VkCommandBuffer cmd, VkImage image, VkBuffer buffer, uint32_t width, uint32_t height, @@ -787,6 +818,7 @@ void VulkanRenderer::CopyImageToBuffer(VkCommandBuffer cmd, VkImage image, 1, ®ion); } +// Converts Oak's pixel format/channel count pair to the closest Vulkan format. VkFormat VulkanRenderer::PixelFormatToVkFormat(PixelFormat format, int channel_count) const { @@ -830,6 +862,7 @@ VkFormat VulkanRenderer::PixelFormatToVkFormat(PixelFormat format, return VK_FORMAT_UNDEFINED; } +// Checks color-attachment support before selecting renderable image formats. bool VulkanRenderer::IsColorAttachmentSupported(VkFormat format) const { VkFormatProperties props; @@ -838,6 +871,8 @@ bool VulkanRenderer::IsColorAttachmentSupported(VkFormat format) const VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0; } +// Chooses a renderable Vulkan format and falls back from RGB to RGBA when a +// driver does not expose 3-channel color attachment support. VkFormat VulkanRenderer::PickRenderableFormat(PixelFormat format, int channel_count) const { @@ -858,6 +893,7 @@ VkFormat VulkanRenderer::PickRenderableFormat(PixelFormat format, return VK_FORMAT_UNDEFINED; } +// Returns the packed texel size for the VkFormat values generated above. int VulkanRenderer::GetVkFormatBytesPerPixel(VkFormat format) const { switch (format) { @@ -898,6 +934,7 @@ int VulkanRenderer::GetVkFormatBytesPerPixel(VkFormat format) const } } +// Returns the alpha fill value used when expanding formats without alpha. float VulkanRenderer::GetFormatMaxAlpha(PixelFormat format) const { if (format == PixelFormat::U8) { @@ -908,6 +945,8 @@ float VulkanRenderer::GetFormatMaxAlpha(PixelFormat format) const return 1.0f; } +// Copies tightly-packed pixels while changing channel count. This handles the +// common Vulkan fallback where requested RGB data is stored as RGBA on the GPU. void VulkanRenderer::CopyPixelsWithChannelConversion(const void *src, void *dst, int width, int height, int depth, int src_channels, int dst_channels, @@ -958,12 +997,14 @@ void VulkanRenderer::CopyPixelsWithChannelConversion(const void *src, void *dst, } } +// Rounds size up to the next multiple of alignment. VkDeviceSize VulkanRenderer::AlignSize(VkDeviceSize size, VkDeviceSize alignment) const { return (size + alignment - 1) & ~(alignment - 1); } +// Finds a compatible memory type satisfying Vulkan's bitmask and property flags. uint32_t VulkanRenderer::FindMemoryType(uint32_t type_filter, VkMemoryPropertyFlags properties) const { @@ -977,6 +1018,7 @@ uint32_t VulkanRenderer::FindMemoryType(uint32_t type_filter, return UINT32_MAX; } +// Creates a Vulkan image, memory allocation, and image view for an Oak texture. QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth, PixelFormat format, int channel_count, const void *data, int linesize) @@ -1164,6 +1206,7 @@ QVariant VulkanRenderer::CreateNativeTexture(int width, int height, int depth, return QVariant::fromValue(tex->id); } +// Destroys a texture handle and all Vulkan objects owned by that texture. void VulkanRenderer::DestroyNativeTexture(QVariant texture) { QMutexLocker lock(&mutex_); @@ -1187,6 +1230,8 @@ void VulkanRenderer::DestroyNativeTexture(QVariant texture) delete tex; } +// Uploads CPU pixels to an existing image. The staging layout is based on the +// selected GPU VkFormat, then CPU data is repacked when channel counts differ. void VulkanRenderer::UploadToTexture(const QVariant &handle, const VideoParams ¶ms, const void *data, int linesize) @@ -1273,6 +1318,8 @@ void VulkanRenderer::UploadToTexture(const QVariant &handle, tex->current_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; } +// Downloads an image to CPU memory. When the GPU format is wider than the +// requested CPU format, the staging data is compacted back to the caller layout. void VulkanRenderer::DownloadFromTexture(const QVariant &handle, const VideoParams ¶ms, void *data, int linesize) @@ -1355,6 +1402,7 @@ void VulkanRenderer::DownloadFromTexture(const QVariant &handle, tex->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; } +// Blocks until the device is idle so later CPU readback or teardown is safe. void VulkanRenderer::Flush() { if (device_ != VK_NULL_HANDLE) { @@ -1362,6 +1410,8 @@ void VulkanRenderer::Flush() } } +// Clears a texture with vkCmdClearColorImage; null destinations are ignored +// because this backend has no implicit swapchain framebuffer. void VulkanRenderer::ClearDestination(olive::Texture *texture, double r, double g, double b, double a) { @@ -1399,6 +1449,7 @@ void VulkanRenderer::ClearDestination(olive::Texture *texture, double r, double EndOneTimeCommands(cmd); } +// Reads one pixel by copying a 1x1 image region into a staging buffer. Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture, const QPointF &pt) { @@ -1465,6 +1516,7 @@ Color VulkanRenderer::GetPixelFromTexture(olive::Texture *texture, // Shader compilation (GLSL -> SPIR-V via shaderc) // ------------------------------------------------------------------ +// Returns true for GLSL sampler uniforms that must become explicit descriptors. static bool IsSamplerType(const QString &type) { static const QRegularExpression sampler_re( @@ -1472,6 +1524,8 @@ static bool IsSamplerType(const QString &type) return sampler_re.match(type).hasMatch(); } +// Ensures GLSL has a Vulkan-compatible version directive before shaderc compiles +// it as GLSL 450. QString VulkanRenderer::EnsureGlslVersion450(const QString &glsl) const { QString result = glsl.trimmed(); @@ -1483,6 +1537,9 @@ QString VulkanRenderer::EnsureGlslVersion450(const QString &glsl) const return QStringLiteral("#version 450 core\n") + result; } +// Converts legacy Oak/OpenGL GLSL into Vulkan GLSL. The conversion keeps shader +// semantics but replaces implicit attributes/varyings and texture sampling with +// explicit layouts that Vulkan requires. QString VulkanRenderer::ConvertGlslToVulkan(const QString &glsl, VkShaderStageFlagBits stage) { @@ -1513,6 +1570,7 @@ QString VulkanRenderer::ConvertGlslToVulkan(const QString &glsl, return result; } +// Returns the std140 storage size for scalar, vector, color, and matrix values. VkDeviceSize VulkanRenderer::GetStd140Size(const QString &type) const { if (type == QStringLiteral("float")) return 4; @@ -1524,6 +1582,7 @@ VkDeviceSize VulkanRenderer::GetStd140Size(const QString &type) const return 4; } +// Returns std140 base alignment so generated UBO offsets match GPU layout rules. VkDeviceSize VulkanRenderer::GetStd140Alignment(const QString &type) const { if (type == QStringLiteral("float")) return 4; @@ -1535,6 +1594,8 @@ VkDeviceSize VulkanRenderer::GetStd140Alignment(const QString &type) const return 4; } +// Scans GLSL uniform declarations and splits them into samplers and values. This +// is intentionally narrow and targets the shader style generated by Oak nodes. void VulkanRenderer::ExtractUniforms(const QString &glsl, QVector *out_uniforms, QVector *out_samplers) const @@ -1573,6 +1634,7 @@ void VulkanRenderer::ExtractUniforms(const QString &glsl, } } +// Computes std140 offsets in declaration order and records the total UBO size. void VulkanRenderer::ComputeUniformLayout(QVector *uniforms) const { VkDeviceSize offset = 0; @@ -1585,6 +1647,7 @@ void VulkanRenderer::ComputeUniformLayout(QVector *uniforms) const } } +// Generates the uniform block source inserted into rewritten shaders. QString VulkanRenderer::BuildUboBlock(const QVector &uniforms) const { if (uniforms.isEmpty()) { @@ -1599,6 +1662,8 @@ QString VulkanRenderer::BuildUboBlock(const QVector &uniforms) cons return ubo; } +// Rewrites GLSL so non-sampler uniforms live in set=0,binding=0 and sampler +// uniforms get deterministic explicit bindings after the UBO. QString VulkanRenderer::RewriteShaderWithUbo( const QString &glsl, const QVector &all_uniforms, @@ -1658,6 +1723,8 @@ QString VulkanRenderer::RewriteShaderWithUbo( } +// Compiles Vulkan GLSL into SPIR-V using shaderc. Without shaderc this backend +// can initialize but cannot create shaders. bool VulkanRenderer::CompileGlslToSpv(const QString &glsl, VkShaderStageFlagBits stage, QByteArray *out_spv) @@ -1724,6 +1791,7 @@ bool VulkanRenderer::CompileGlslToSpv(const QString &glsl, #endif } +// Converts, compiles, and stores a shader pair plus descriptor metadata. QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code) { QMutexLocker lock(&mutex_); @@ -1893,6 +1961,7 @@ QVariant VulkanRenderer::CreateNativeShader(olive::ShaderCode code) } +// Releases shader modules, descriptor layout, pipeline layout, and pipelines. void VulkanRenderer::DestroyNativeShader(QVariant shader) { QMutexLocker lock(&mutex_); @@ -1922,6 +1991,8 @@ void VulkanRenderer::DestroyNativeShader(QVariant shader) delete sh; } +// Creates a graphics pipeline for the destination render format. Viewport and +// scissor are dynamic so one pipeline can handle multiple target sizes. bool VulkanRenderer::CreatePipelineForShader(VulkanShader *shader, const VideoParams &dest_params, @@ -2045,6 +2116,8 @@ bool VulkanRenderer::CreatePipelineForShader(VulkanShader *shader, return true; } +// Executes one fullscreen draw pass. Texture descriptors and a transient UBO are +// allocated per pass so iterative shaders can update bindings cheaply. void VulkanRenderer::BlitPass(VulkanShader *shader, VulkanTexture *dest_tex, const QVector &bindings, const QByteArray &ubo_data, @@ -2257,6 +2330,8 @@ void VulkanRenderer::BlitPass(VulkanShader *shader, VulkanTexture *dest_tex, } } +// Runs a shader job. Multi-iteration jobs ping-pong between temporary textures +// and replace the configured iterative input with the previous pass output. void VulkanRenderer::Blit(QVariant shader_variant, olive::AcceleratedJob &a_job, olive::Texture *destination, VideoParams destination_params, diff --git a/app/render/vulkan/vulkanrenderer.h b/app/render/vulkan/vulkanrenderer.h index 2a6accfb6..b1e65f22d 100644 --- a/app/render/vulkan/vulkanrenderer.h +++ b/app/render/vulkan/vulkanrenderer.h @@ -35,29 +35,45 @@ namespace olive class VulkanRenderer : public Renderer { Q_OBJECT public: + // Creates a renderer object; Vulkan objects are created lazily in Init(). explicit VulkanRenderer(QObject *parent = nullptr); + // Releases Vulkan objects through the normal Renderer destruction path. virtual ~VulkanRenderer() override; + // Creates the Vulkan instance, logical device, command pool, and descriptor + // pool required for offscreen rendering. virtual bool Init() override; + // Creates reusable GPU resources that require a fully initialized device. virtual void PostInit() override; + // Reserved for symmetry with OpenGLRenderer; Vulkan cleanup is handled by + // DestroyInternal(). virtual void PostDestroy() override; + // Clears either a texture render target or the currently bound output target. virtual void ClearDestination(olive::Texture *texture = nullptr, double r = 0.0, double g = 0.0, double b = 0.0, double a = 0.0) override; + // Compiles GLSL to SPIR-V, creates shader modules, and prepares descriptor + // metadata for later blits. virtual QVariant CreateNativeShader(olive::ShaderCode code) override; + // Destroys shader modules, descriptor layout, pipeline layout, and cached + // pipelines associated with a shader handle. virtual void DestroyNativeShader(QVariant shader) override; + // Uploads CPU pixel data to a Vulkan image via a staging buffer. virtual void UploadToTexture(const QVariant &handle, const VideoParams ¶ms, const void *data, int linesize) override; + // Downloads a Vulkan image to CPU memory via a staging buffer. virtual void DownloadFromTexture(const QVariant &handle, const VideoParams ¶ms, void *data, int linesize) override; + // Waits for outstanding device work to complete. virtual void Flush() override; + // Reads a single texture pixel using a one-pixel transfer readback. virtual Color GetPixelFromTexture(olive::Texture *texture, const QPointF &pt) override; @@ -67,14 +83,19 @@ public: } protected: + // Runs one or more fullscreen shader passes into the destination texture. virtual void Blit(QVariant shader, olive::AcceleratedJob &job, olive::Texture *destination, VideoParams destination_params, bool clear_destination) override; + // Creates a Vulkan image/view/memory bundle and optionally uploads initial + // pixel data. virtual QVariant CreateNativeTexture(int width, int height, int depth, PixelFormat format, int channel_count, const void *data = nullptr, int linesize = 0) override; + // Releases a Vulkan texture bundle. virtual void DestroyNativeTexture(QVariant texture) override; + // Releases all Vulkan device resources owned by this renderer. virtual void DestroyInternal() override; private: @@ -82,62 +103,99 @@ private: struct VulkanShader; struct UniformInfo; + // Creates the Vulkan instance used for all offscreen work. bool CreateInstance(); + // Chooses a graphics-capable physical device and creates the logical device. bool CreateDevice(); + // Creates a command pool for short-lived command buffers. bool CreateCommandPool(); + // Creates the descriptor pool used for per-blit UBO/sampler sets. bool CreateDescriptorPool(); + // Uploads the fullscreen quad vertex buffer used by BlitPass(). bool CreateVertexBuffer(); + // Creates the persistent linear sampler. bool CreateLinearSampler(); + // Creates the persistent nearest-neighbor sampler. bool CreateNearestSampler(); + // Returns the persistent sampler matching the requested interpolation mode. VkSampler GetSampler(Texture::Interpolation interpolation) const; + // Allocates a host-visible staging buffer for upload/download transfers. bool CreateStagingBuffer(VkDeviceSize size, VkBuffer *out_buffer, VkDeviceMemory *out_memory); + // Destroys a staging buffer pair allocated by CreateStagingBuffer(). void DestroyStagingBuffer(VkBuffer buffer, VkDeviceMemory memory); + // Begins a one-shot command buffer and records it immediately. VkCommandBuffer BeginOneTimeCommands(); + // Submits and waits for a one-shot command buffer. void EndOneTimeCommands(VkCommandBuffer cmd); + // Emits an image memory barrier for the subset of layouts this renderer uses. void TransitionImageLayout(VkCommandBuffer cmd, VkImage image, VkImageLayout old_layout, VkImageLayout new_layout); + // Records a tightly packed buffer-to-image copy. void CopyBufferToImage(VkCommandBuffer cmd, VkBuffer buffer, VkImage image, uint32_t width, uint32_t height, uint32_t depth); + // Records an image-to-buffer copy, optionally reading one pixel offset. void CopyImageToBuffer(VkCommandBuffer cmd, VkImage image, VkBuffer buffer, uint32_t width, uint32_t height, uint32_t offset_x = 0, uint32_t offset_y = 0); + // Converts Oak pixel format/channel metadata to a preferred Vulkan format. VkFormat PixelFormatToVkFormat(PixelFormat format, int channel_count) const; + // Picks a color-attachment-capable format, falling back from RGB to RGBA + // where drivers do not support 3-channel render targets. VkFormat PickRenderableFormat(PixelFormat format, int channel_count) const; + // Checks whether a format can be used as a render target. bool IsColorAttachmentSupported(VkFormat format) const; + // Returns the packed byte size for supported VkFormat values. int GetVkFormatBytesPerPixel(VkFormat format) const; + // Returns the alpha fill value used when expanding RGB data to RGBA. float GetFormatMaxAlpha(PixelFormat format) const; + // Repackages tightly packed pixels when the requested CPU channel count + // differs from the selected GPU format channel count. void CopyPixelsWithChannelConversion(const void *src, void *dst, int width, int height, int depth, int src_channels, int dst_channels, PixelFormat format) const; + // Rounds a size up to the requested alignment. VkDeviceSize AlignSize(VkDeviceSize size, VkDeviceSize alignment) const; + // Finds a Vulkan memory type matching the requested properties. uint32_t FindMemoryType(uint32_t type_filter, VkMemoryPropertyFlags properties) const; + // Compiles GLSL source into SPIR-V using shaderc when available. bool CompileGlslToSpv(const QString &glsl, VkShaderStageFlagBits stage, QByteArray *out_spv); + // Rewrites an Oak GLSL shader into Vulkan-compatible GLSL. QString ConvertGlslToVulkan(const QString &glsl, VkShaderStageFlagBits stage); + // Ensures a shader declares a Vulkan-compatible GLSL version. QString EnsureGlslVersion450(const QString &glsl) const; + // Extracts uniforms and sampler names from GLSL declarations. void ExtractUniforms(const QString &glsl, QVector *out_uniforms, QVector *out_samplers) const; + // Computes std140 offsets and total UBO size for extracted uniforms. void ComputeUniformLayout(QVector *uniforms) const; + // Builds the generated uniform block used by rewritten shaders. QString BuildUboBlock(const QVector &uniforms) const; + // Rewrites standalone uniforms and samplers into explicit UBO/sampler + // bindings accepted by Vulkan GLSL. QString RewriteShaderWithUbo(const QString &glsl, const QVector &all_uniforms, const QHash &sampler_bindings) const; + // Returns std140 storage size for a supported GLSL type. VkDeviceSize GetStd140Size(const QString &type) const; + // Returns std140 alignment for a supported GLSL type. VkDeviceSize GetStd140Alignment(const QString &type) const; + // Creates or retrieves the graphics pipeline for a shader/render format pair. bool CreatePipelineForShader(VulkanShader *shader, const VideoParams &dest_params, VkFormat render_pass_format); + // Caches simple single-color-attachment render passes by format/clear mode. VkRenderPass GetOrCreateRenderPass(VkFormat format, bool clear); struct TextureBinding { @@ -146,6 +204,7 @@ private: Texture::Interpolation interp; }; + // Executes one fullscreen pass with the provided texture bindings and UBO. void BlitPass(VulkanShader *shader, VulkanTexture *dest_tex, const QVector &bindings, const QByteArray &ubo_data, diff --git a/app/widget/viewer/viewerdisplay.cpp b/app/widget/viewer/viewerdisplay.cpp index f7730601c..cd657796b 100644 --- a/app/widget/viewer/viewerdisplay.cpp +++ b/app/widget/viewer/viewerdisplay.cpp @@ -430,6 +430,10 @@ void ViewerDisplayWidget::OnPaint() if (texture && texture->renderer() && texture->renderer() != renderer()) { if (texture->renderer()->IsOpenGL() && renderer()->IsOpenGL()) { + // Shared OpenGL contexts can display the producer texture + // directly. Avoid readback here because the producer + // renderer may belong to a render thread whose context + // cannot be made current from the GUI paint callback. texture_ = texture; } else { // Cross-backend texture: download and re-upload @@ -1394,6 +1398,8 @@ void ViewerDisplayWidget::DrawBlank(const VideoParams &device_params) renderer()->Blit(blank_shader_, job, device_params, false); } +// Renders a backend-neutral frame by drawing into an offscreen backend texture, +// downloading it to CPU memory, then painting that image with QPainter. void ViewerDisplayWidget::DrawBackendNeutral(const ColorTransformJob &ctj, QPainter *painter) { @@ -1412,6 +1418,8 @@ void ViewerDisplayWidget::DrawBackendNeutral(const ColorTransformJob &ctj, if (!backend_neutral_texture_ || backend_neutral_texture_->params() != offscreen_params) { + // The offscreen texture is sized in device pixels so high-DPI widgets + // draw one downloaded pixel per device pixel after setDevicePixelRatio(). backend_neutral_texture_ = renderer()->CreateTexture(offscreen_params); backend_neutral_buffer_.resize( texture_width * texture_height * @@ -1426,6 +1434,8 @@ void ViewerDisplayWidget::DrawBackendNeutral(const ColorTransformJob &ctj, ColorTransformJob local_ctj = ctj; local_ctj.SetClearDestinationEnabled(true); + // Reuse the normal color-management shader path, but render into a texture + // instead of an OpenGL widget framebuffer. renderer()->BlitColorManaged(local_ctj, backend_neutral_texture_.get()); backend_neutral_texture_->Download(backend_neutral_buffer_.data(), 0); @@ -1440,6 +1450,8 @@ void ViewerDisplayWidget::DrawBackendNeutral(const ColorTransformJob &ctj, QImage::Format_RGBA8888_Premultiplied); img.setDevicePixelRatio(devicePixelRatioF()); + // QImage references backend_neutral_buffer_ directly; draw it before the + // buffer can be resized or reused by a later paint. painter->drawImage(QPoint(0, 0), img); } diff --git a/docs/zh/v04-color-audio-performance-manual-test-plan.md b/docs/zh/v04-color-audio-performance-manual-test-plan.md index b9499bc18..bd21c1907 100644 --- a/docs/zh/v04-color-audio-performance-manual-test-plan.md +++ b/docs/zh/v04-color-audio-performance-manual-test-plan.md @@ -18,10 +18,10 @@ |:--|:--|:--| | Linux + Mesa/AMD 或 Intel | 软件导出、代理、示波器、音频同步 | 当前开发主环境优先 | | Linux + NVIDIA | NVENC、代理、4K/8K 预览 | 需要 NVIDIA 驱动和 ffmpeg 编码器支持 | -| Linux + Vulkan 驱动 | 图形后端选择、Vulkan 请求、OpenGL 回退 | Vulkan 当前为实验入口,需确认不会破坏 OpenGL 渲染 | +| Linux + Vulkan 驱动 | Vulkan 后端加载、viewer readback、代理、导出、OpenGL 回退 | 需先用 `vulkaninfo --summary` 确认可创建 Vulkan instance/device | | macOS Apple Silicon | VideoToolbox、ColorSync/显示路径、代理 | 重点看硬件导出和 UI 响应 | | Windows + NVIDIA/Intel | NVENC/QSV 可用性、路径编码、文件管理器 reveal | 重点看中文路径和空格路径 | -| Windows + Vulkan Runtime | 图形后端选择、Vulkan 请求、驱动缺失回退 | 重点看设置持久化和启动稳定性 | +| Windows + Vulkan Runtime | Vulkan 后端加载、viewer readback、驱动缺失回退 | 重点看设置持久化、启动稳定性和显卡驱动兼容性 | ## 测试素材准备 @@ -359,9 +359,14 @@ 通过标准:内存没有持续不可控增长;播放停止后应用仍可操作和保存。 -## 10. 图形后端选择测试 +## 10. 图形后端与 Vulkan 手工测试 -当前版本允许用户在 Preferences 中选择 OpenGL 或 Vulkan。注意:Vulkan 入口目前是实验性图形 API 请求和后续 VulkanRenderer 的接入点;默认构建下时间线/viewer 渲染仍应安全回退到现有 OpenGL renderer。动态后端适配器通过 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 实验开关接入,测试重点是“用户可选择、设置可持久化、Vulkan 请求不破坏现有渲染、失败可回退”。 +当前版本允许用户在 Preferences 中选择 OpenGL 或 Vulkan。动态后端适配器通过 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 接入 `liboakgl` / `liboakvulkan`。Vulkan 后端已具备 offscreen texture、upload/download、shader Blit、viewer backend-neutral readback 的代码路径,但完整 UI 播放、代理、导出、Scope 和 OpenFX CPU 回退仍需要在真实显示环境与可用 Vulkan runtime 上手工验收。 + +Vulkan 测试必须先区分两类环境: + +- 可用 Vulkan 环境:`vulkaninfo --summary` 能成功列出 instance、physical device、driver 和 queue family。 +- 不可用 Vulkan 环境:缺少 runtime/ICD、驱动损坏,或 `vulkaninfo --summary` 报 `Found no drivers` / `ERROR_INCOMPATIBLE_DRIVER`。这类环境只测试回退,不应把 Vulkan 渲染用例记为通过。 ### 10.1 默认 OpenGL 后端 @@ -373,7 +378,17 @@ 通过标准:默认值为 OpenGL;viewer、Scope、调色和播放行为与原 OpenGL 路径一致。 -### 10.2 切换到 Vulkan 并重启 +### 10.2 Vulkan 运行时预检 + +1. 在待测机器上运行 `vulkaninfo --summary`。 +2. 记录 GPU 型号、Vulkan API 版本、driver 版本、ICD 文件路径。 +3. 确认应用构建产物中存在 Oak 私有 Vulkan 后端库:Linux 为 `liboakvulkan.so`,macOS 为 `liboakvulkan.dylib`,Windows 为 `oakvulkan.dll`。 +4. 运行动态后端 gtest:`olive-gtest --gtest_filter='DynamicRenderBackend.*'`。 +5. 检查 Vulkan 用例是实际执行还是 SKIP。 + +通过标准:可用 Vulkan 环境下 `vulkaninfo` 成功,`liboakvulkan` 存在,Vulkan gtest 至少执行 backend load、upload/download、Blit 相关用例;不可用 Vulkan 环境下测试必须明确记录 driver/runtime 错误,Vulkan gtest 可 SKIP,但 OpenGL fallback 用例必须通过。 + +### 10.3 切换到 Vulkan 并重启 1. 在 Preferences > Behavior > Rendering 中选择 `Vulkan (experimental)`。 2. 确认设置保存。 @@ -382,9 +397,81 @@ 5. 导入并播放 `4k_camera_a.mov`。 6. 检查日志,确认 `RenderManager` 报告的实际后端与回退结果一致。 -通过标准:Vulkan 选择可持久化;重启后应用不崩溃;当前 Vulkan 未完整验证时应明确回退 OpenGL 渲染,`RenderManager::backend()` 必须反映实际运行后端(回退后应为 OpenGL),播放仍可用。 +通过标准:Vulkan 选择可持久化;重启后应用不崩溃;可用 Vulkan 环境下日志显示动态 Vulkan 后端加载并初始化成功,`RenderManager::backend()` 报告 Vulkan;不可用 Vulkan 环境下应明确回退 OpenGL,`RenderManager::backend()` 必须反映实际运行后端,播放仍可用。 -### 10.3 Vulkan 驱动缺失或不可用 +### 10.4 Vulkan Viewer 基础播放 + +1. 在可用 Vulkan 环境中选择 Vulkan 并重启。 +2. 导入 `color_chart.mov` 和 `4k_camera_a.mov`。 +3. 将两个 clip 放入时间线,打开 Viewer。 +4. 播放 10 秒,期间执行暂停、继续播放、逐帧前进、拖动时间线、缩放 Viewer。 +5. 打开/关闭全屏或浮动 Viewer 窗口。 +6. 观察画面是否黑屏、闪烁、残留上一帧或颜色明显错误。 + +通过标准:Viewer 通过 Vulkan backend-neutral readback 路径正常显示,播放和 seek 不崩溃;画面比例、裁切、缩放和 device pixel ratio 正常;没有长期黑屏、上一帧残留或 UI 死锁。 + +### 10.5 Vulkan 调色/LUT 显示一致性 + +1. 选择 Vulkan 并重启。 +2. 将 `color_chart.mov` 放入时间线。 +3. 加载 `lut_valid.cube`,再做一次三向色轮明显调整。 +4. 在同一帧记录 Viewer 截图或视觉观察结果。 +5. 切回 OpenGL 重启,打开同一项目并定位同一帧。 +6. 对比 Vulkan 和 OpenGL 的画面颜色、亮度和透明度表现。 + +通过标准:Vulkan 与 OpenGL 预览颜色方向一致,LUT 和三向色轮均生效;不要求像素完全一致,但不能出现通道错乱、alpha 错误、明显 gamma 反转或 LUT 失效。 + +### 10.6 Vulkan 代理媒体与重素材播放 + +1. 选择 Vulkan 并重启。 +2. 对 `8k_or_heavy_camera.mov` 生成代理并启用代理。 +3. 播放代理路径 30 秒,期间拖动时间线和缩放 Viewer。 +4. 关闭代理,播放原片路径 10 秒。 +5. 保存、关闭并重开项目,确认代理状态仍正确。 + +通过标准:启用代理后 Viewer 可播放且不崩溃;禁用代理后回到原片路径;保存重开后代理状态一致;Vulkan 路径不应把导出源降级为代理。 + +### 10.7 Vulkan 软件导出 + +1. 选择 Vulkan 并重启。 +2. 创建 10 秒 sequence,包含 `color_chart.mov`、LUT、三向调色、一个代理 clip 和一段音频。 +3. 执行软件编码导出 H.264 或 ProRes。 +4. 用播放器检查导出文件。 +5. 使用 OpenGL 后端重复导出同一段作为对照。 + +通过标准:Vulkan 下导出成功,输出可播放,音画同步不超过 1 帧;颜色处理和 OpenGL 导出方向一致;启用代理时导出仍使用原片质量路径;失败时有明确错误,不生成损坏的完成文件。 + +### 10.8 Vulkan Scope 行为 + +1. 选择 Vulkan 并重启。 +2. 打开 Waveform、Vectorscope、Histogram。 +3. 播放 `color_chart.mov` 并调整 LUT/三向色轮。 +4. 观察 Scope 面板行为。 +5. 切回 OpenGL 后重复同一操作。 + +通过标准:当前 backend-neutral Scope 若仍是安全跳过,应明确记录为已知限制,且不能崩溃或卡死;OpenGL 下 Scope 必须正常更新。若 Vulkan Scope 已实现,则三类 Scope 必须随当前帧和调色变化更新。 + +### 10.9 Vulkan OpenFX CPU 回退 + +1. 选择 Vulkan 并重启。 +2. 在 clip 上添加一个已知可用的 OFX 插件,优先选择支持 CPU 渲染且效果明显的插件。 +3. 播放并导出 5 秒片段。 +4. 检查日志中 OpenGL OFX render 是否被禁用,插件是否走 CPU readback/upload 路径。 +5. 切回 OpenGL,确认支持 OpenGL render 的插件仍能走 OpenGL 输出绑定路径。 + +通过标准:Vulkan 下 OFX 插件不因缺少 OpenGL context 而被跳过或崩溃;CPU 回退输出可见且可导出;OpenGL 下原有 OFX OpenGL 路径不回退或失效。 + +### 10.10 Vulkan 后端长时间稳定性 + +1. 选择 Vulkan 并重启。 +2. 打开包含 4K/8K、LUT、代理、音频和至少 10 个 clip 的项目。 +3. 循环播放 20 分钟。 +4. 期间反复 seek、切换代理、打开/关闭 Viewer、打开/关闭导出窗口。 +5. 观察日志、显存/内存占用和 UI 响应。 + +通过标准:无崩溃、无持续不可控内存增长、无明显 Vulkan validation/driver error;停止播放后仍可保存项目和退出应用。 + +### 10.11 Vulkan 驱动缺失或不可用 1. 在没有 Vulkan Runtime 或驱动不可用的机器上选择 Vulkan。 2. 重启 Oak。 @@ -394,7 +481,7 @@ 通过标准:应用可以启动;日志应说明 Vulkan 请求不可完全满足或当前回退 OpenGL;`RenderManager::backend()` 必须与实际运行后端一致;用户能回到 Preferences 改回 OpenGL。 -### 10.4 从 Vulkan 切回 OpenGL +### 10.12 从 Vulkan 切回 OpenGL 1. 在 Vulkan 已选中状态下打开 Preferences。 2. 将 Graphics Backend 改为 OpenGL。 @@ -403,7 +490,7 @@ 通过标准:重启后显示 OpenGL;播放和导出正常;不会保留错误的 Vulkan 状态。 -### 10.5 代理、Scope 与调色组合 +### 10.13 代理、Scope 与调色组合回归 1. 选择 Vulkan 并重启。 2. 对 `8k_or_heavy_camera.mov` 生成并启用代理。 @@ -413,7 +500,7 @@ 通过标准:Vulkan 请求状态下代理、Scope、调色不崩溃;切回 OpenGL 后项目状态一致;两种选择下导出默认仍使用原片。 -### 10.6 动态 OpenGL 后端加载 +### 10.14 动态 OpenGL 后端加载 1. 使用开启 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 的实验构建。 2. 确认应用目录存在 Oak 私有 OpenGL 后端库,例如 `liboakgl.so`、`liboakgl.dylib` 或 `oakgl.dll`。 @@ -423,7 +510,7 @@ 通过标准:日志显示动态 OpenGL 后端加载成功;viewer、Scope、调色和播放行为与默认 OpenGL 路径一致;退出时执行 destroy/unload 无崩溃。 -### 10.7 动态后端缺失或损坏 +### 10.15 动态后端缺失或损坏 1. 使用开启 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 的实验构建。 2. 临时移走或重命名 Oak 私有 OpenGL 后端库。 @@ -432,14 +519,35 @@ 通过标准:应用不能静默崩溃;日志明确说明后端库加载失败;用户能够恢复库文件或切回默认构建继续打开项目。 -### 10.8 Vulkan 动态后端占位 +### 10.16 Vulkan 动态后端库缺失或不可加载 1. 使用开启 `OAK_ENABLE_DYNAMIC_RENDER_BACKEND` 的实验构建。 2. 在 Preferences 中选择 Vulkan 并重启。 -3. 如果 `liboakvulkan` 尚未实现,观察回退行为。 -4. 切回 OpenGL 并重启。 +3. 临时移走、重命名或替换为损坏的 `liboakvulkan`。 +4. 再次启动 Oak 并打开项目。 +5. 观察回退行为。 +6. 恢复 `liboakvulkan` 后切回 OpenGL 并重启。 -通过标准:Vulkan 后端未实现时不崩溃;日志明确说明 Vulkan 后端缺失并回退或拒绝初始化;切回 OpenGL 后项目可播放。 +通过标准:Vulkan 后端库缺失、损坏或符号不完整时不崩溃;日志明确说明 Vulkan 后端加载失败并回退或拒绝初始化;切回 OpenGL 后项目可播放。 + +### 10.17 Vulkan 与 OpenGL 结果记录 + +1. 对同一项目分别在 Vulkan 和 OpenGL 下执行 Viewer 播放、5 秒软件导出、代理启用导出。 +2. 记录每个环境的实际 backend、GPU、driver、Vulkan API 版本和是否发生回退。 +3. 对比导出文件的分辨率、帧率、时长、音频流和视觉结果。 +4. 将差异记录到缺陷模板。 + +通过标准:每次测试结果能明确区分“真实 Vulkan 后端通过”、“请求 Vulkan 但回退 OpenGL 通过”和“Vulkan 后端失败”;不能把回退 OpenGL 的结果记为 Vulkan 渲染通过。 + +### 10.18 回退链路恢复 + +1. 在可用 Vulkan 环境中选择 Vulkan 并确认实际使用 Vulkan。 +2. 退出应用,临时破坏 Vulkan runtime 或移走 `liboakvulkan`。 +3. 启动应用并确认回退 OpenGL。 +4. 切回 OpenGL 并重启。 +5. 恢复 Vulkan runtime 和 `liboakvulkan`,再次选择 Vulkan 重启。 + +通过标准:回退和恢复路径都不破坏用户配置和项目文件;日志能解释每次实际使用的 backend;用户始终能回到可播放的 OpenGL 状态。 ## 缺陷记录模板 @@ -452,12 +560,13 @@ - 预期结果和实际结果。 - 是否可稳定复现。 - 如果涉及导出,附 ffprobe 输出和导出设置截图。 +- 如果涉及 Vulkan,附 `vulkaninfo --summary` 输出、实际 backend 日志、是否发生 OpenGL 回退。 ## 发布前最低通过线 - 预检、LUT、三向色轮、三类 Scope、波形同步、BWF 时间码、音频表、代理生成/启用/删除、软件导出全部通过。 - 至少一个硬件编码环境通过 NVENC 或 VideoToolbox。 -- OpenGL/Vulkan 图形后端选择、持久化和 Vulkan 请求回退测试通过。 +- OpenGL/Vulkan 图形后端选择、持久化、Vulkan 可用环境实渲染和 Vulkan 不可用环境回退测试通过;若无可用 Vulkan 环境,发布记录必须明确标注 Vulkan 实渲染未验收。 - 批量队列至少通过多任务执行和取消测试。 - 组合回归测试中的完整剪辑链路通过。 - 所有失败项有明确 issue 或文档化限制,不存在“无提示崩溃”级别问题。 diff --git a/tests/gtest/dynamic_render_backend_test.cpp b/tests/gtest/dynamic_render_backend_test.cpp index 9ad797035..ad357c34d 100644 --- a/tests/gtest/dynamic_render_backend_test.cpp +++ b/tests/gtest/dynamic_render_backend_test.cpp @@ -10,6 +10,8 @@ #include "render/texture.h" #include "render/videoparams.h" +// Verifies that the dynamic adapter can load the private OpenGL backend and +// query its advertised C ABI capabilities without creating a viewer context. TEST(DynamicRenderBackend, LoadsExperimentalOpenGLBackend) { #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND @@ -30,6 +32,8 @@ TEST(DynamicRenderBackend, LoadsExperimentalOpenGLBackend) #endif } +// Verifies Vulkan backend discovery on systems with a working Vulkan ICD. The +// test skips when the runtime correctly reports Vulkan as unavailable. TEST(DynamicRenderBackend, LoadsExperimentalVulkanBackendWhenAvailable) { #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND @@ -52,6 +56,8 @@ TEST(DynamicRenderBackend, LoadsExperimentalVulkanBackendWhenAvailable) #endif } +// Verifies that requesting Vulkan on systems without a usable runtime falls +// back to OpenGL and reports the effective backend name. TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable) { #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND @@ -71,6 +77,8 @@ TEST(DynamicRenderBackend, FallsBackWhenExperimentalVulkanUnavailable) #endif } +// Exercises the minimal Vulkan render loop: upload a texture, run a pass-through +// shader blit, then download the destination and verify pixel data. TEST(DynamicRenderBackend, VulkanUploadBlitDownload) { #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND @@ -147,6 +155,8 @@ TEST(DynamicRenderBackend, VulkanUploadBlitDownload) #endif } +// Ensures the Vulkan backend handles null-destination blits by rendering to a +// temporary offscreen target instead of dereferencing a missing framebuffer. TEST(DynamicRenderBackend, VulkanNullDestinationBlitDoesNotCrash) { #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND @@ -210,6 +220,8 @@ TEST(DynamicRenderBackend, VulkanNullDestinationBlitDoesNotCrash) #endif } +// Verifies iterative shader support: the first pass writes to a temporary +// texture and the second pass samples it before writing the final destination. TEST(DynamicRenderBackend, VulkanIterativeBlitPingPong) { #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND @@ -289,6 +301,8 @@ TEST(DynamicRenderBackend, VulkanIterativeBlitPingPong) #endif } +// Verifies RGB upload/download when the Vulkan driver stores the texture in a +// wider renderable format such as RGBA. TEST(DynamicRenderBackend, VulkanUploadDownloadThreeChannel) { #ifndef OAK_ENABLE_DYNAMIC_RENDER_BACKEND diff --git a/tests/gtest/main.cpp b/tests/gtest/main.cpp index 63f451796..d40e67524 100644 --- a/tests/gtest/main.cpp +++ b/tests/gtest/main.cpp @@ -3,6 +3,9 @@ #include #include +// Configures process-wide Qt/OCIO state before running gtest. Tests default to +// the offscreen QPA plugin so headless or invalid DISPLAY sessions do not abort +// before gtest can report skips/failures. int main(int argc, char **argv) { Q_INIT_RESOURCE(ocioconf);