From 800859b0effb4915cc0e0a45cb5bf42aac9348ae Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 21 Jun 2026 00:28:20 +0800 Subject: [PATCH] Fix OCIO LUT async processor black screen and freeze - Pass through input texture when the LUT processor is not ready yet, preventing black frames while the processor is being generated. - Serialize processor generation with a single in-flight task to avoid concurrent OCIO lock contention that could freeze the UI. - Invalidate cached frames after the async processor is set so the viewer refreshes automatically without requiring the playhead to be moved. - Add OAK_DISABLE_HWACCEL environment variable to force software decoding. - Add FFmpegDecoderHW regression test for H.264 4:2:2 10-bit decoding. --- app/codec/ffmpeg/ffmpegdecoder.cpp | 10 +++- app/node/color/ociobase/ociobase.cpp | 16 ++++-- app/node/color/ociolut/ociolut.cpp | 35 +++++++++++-- app/node/color/ociolut/ociolut.h | 2 + tests/gtest/CMakeLists.txt | 1 + tests/gtest/ffmpeg_decoder_hw_test.cpp | 71 ++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 tests/gtest/ffmpeg_decoder_hw_test.cpp diff --git a/app/codec/ffmpeg/ffmpegdecoder.cpp b/app/codec/ffmpeg/ffmpegdecoder.cpp index 1d211e355..de971bae4 100644 --- a/app/codec/ffmpeg/ffmpegdecoder.cpp +++ b/app/codec/ffmpeg/ffmpegdecoder.cpp @@ -425,13 +425,17 @@ TexturePtr FFmpegDecoder::RetrieveVideoInternal(const RetrieveVideoParams &p) AVFramePtr ptr = PreProcessFrame(f, p); f=std::move(ptr); if (!f) { - // Error occurred while software scaling + qWarning() << "PreProcessFrame failed"; return nullptr; } // Finally, perform any GPU processing required TexturePtr texture = ProcessFrameIntoTexture(f, p, original); + if (!texture) { + qWarning() << "ProcessFrameIntoTexture returned null"; + } + return texture; } @@ -1476,6 +1480,10 @@ bool FFmpegDecoder::Instance::Open(const char *filename, int stream_index) AVHWDeviceType FFmpegDecoder::Instance::ChooseHardwareDevice() { + if (qEnvironmentVariableIsSet("OAK_DISABLE_HWACCEL")) { + return AV_HWDEVICE_TYPE_NONE; + } + #ifdef Q_OS_LINUX // Prefer NVIDIA's NVDEC where available, then VAAPI/VDPAU. for (AVHWDeviceType type : { AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_VAAPI, diff --git a/app/node/color/ociobase/ociobase.cpp b/app/node/color/ociobase/ociobase.cpp index 1467667af..628ff5648 100644 --- a/app/node/color/ociobase/ociobase.cpp +++ b/app/node/color/ociobase/ociobase.cpp @@ -63,13 +63,19 @@ void OCIOBaseNode::Value(const NodeValueRow &value, const NodeGlobals &globals, { auto tex_met = value[kTextureInput]; TexturePtr t = tex_met.toTexture(); - if (t && processor_) { - ColorTransformJob job; + if (t) { + if (processor_) { + ColorTransformJob job; - job.SetColorProcessor(processor_); - job.SetInputTexture(tex_met); + job.SetColorProcessor(processor_); + job.SetInputTexture(tex_met); - table->Push(NodeValue::kTexture, t->toJob(job), this); + table->Push(NodeValue::kTexture, t->toJob(job), this); + } else { + // Processor isn't ready yet (e.g. still being generated + // asynchronously), pass the input through unchanged. + table->Push(NodeValue::kTexture, QVariant::fromValue(t), this); + } } } diff --git a/app/node/color/ociolut/ociolut.cpp b/app/node/color/ociolut/ociolut.cpp index ee6c27c08..b9d778d73 100644 --- a/app/node/color/ociolut/ociolut.cpp +++ b/app/node/color/ociolut/ociolut.cpp @@ -154,10 +154,30 @@ void OCIOLutNode::GenerateProcessor() pending_path_ = path; pending_direction_ = direction; pending_generation_++; - const int generation = pending_generation_; - ColorManager *manager = this->manager(); - locker.unlock(); + MaybeStartNextTask(); +} + +void OCIOLutNode::MaybeStartNextTask() +{ + if (task_running_) { + return; + } + + if (pending_path_.isEmpty()) { + return; + } + + if (pending_path_ == last_path_ && pending_direction_ == last_direction_ && + last_processor_) { + return; + } + + task_running_ = true; + const int generation = pending_generation_; + const QString path = pending_path_; + const int direction = pending_direction_; + ColorManager *manager = this->manager(); QThreadPool::globalInstance()->start( new GenerateProcessorTask(this, path, direction, generation, manager)); @@ -169,12 +189,16 @@ void OCIOLutNode::SetProcessorResult(ColorProcessorPtr processor, { QMutexLocker locker(&gen_mutex_); + task_running_ = false; + if (generation != pending_generation_) { - // A newer request was issued while this one was in flight; ignore. + // A newer request was issued while this one was in flight; restart. + MaybeStartNextTask(); return; } if (path != pending_path_ || direction != pending_direction_) { + MaybeStartNextTask(); return; } @@ -182,6 +206,9 @@ void OCIOLutNode::SetProcessorResult(ColorProcessorPtr processor, last_direction_ = direction; last_processor_ = processor; set_processor(processor); + + // The processor has changed, ensure any cached frames are re-rendered. + InvalidateAll(kTextureInput); } OCIOLutNode::GenerateProcessorTask::GenerateProcessorTask( diff --git a/app/node/color/ociolut/ociolut.h b/app/node/color/ociolut/ociolut.h index 3380f059b..01048ea0c 100644 --- a/app/node/color/ociolut/ociolut.h +++ b/app/node/color/ociolut/ociolut.h @@ -80,6 +80,7 @@ private: }; void GenerateProcessor(); + void MaybeStartNextTask(); QMutex gen_mutex_; QString last_path_; @@ -89,6 +90,7 @@ private: QString pending_path_; int pending_direction_ = -1; int pending_generation_ = 0; + bool task_running_ = false; }; } // namespace olive diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index 45cdb1dd6..f9aa43869 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -38,6 +38,7 @@ add_executable(olive-gtest plugin_ofx_integration_test.cpp codec_frame_test.cpp codec_decoder_test.cpp + ffmpeg_decoder_hw_test.cpp codec_exportcodec_test.cpp codec_exportformat_test.cpp codec_encoder_test.cpp diff --git a/tests/gtest/ffmpeg_decoder_hw_test.cpp b/tests/gtest/ffmpeg_decoder_hw_test.cpp new file mode 100644 index 000000000..e88cd4a5d --- /dev/null +++ b/tests/gtest/ffmpeg_decoder_hw_test.cpp @@ -0,0 +1,71 @@ +#include + +#include +#include + +#include "codec/decoder.h" +#include "codec/ffmpeg/ffmpegdecoder.h" +#include "node/project/footage/footage.h" +#include "node/output/track/track.h" + +using namespace olive; + +TEST(FFmpegDecoderHW, H264_422_10bit_CPUFrame_IsNotBlack) +{ + const QString path = QStringLiteral("/home/mikesolar/Videos/dual_system_video.MOV"); + + if (!QFileInfo::exists(path)) { + GTEST_SKIP() << "Test footage not available: " << path.toStdString(); + } + + DecoderPtr decoder = Decoder::CreateFromID(QStringLiteral("ffmpeg")); + ASSERT_TRUE(decoder); + + Footage footage(path); + ASSERT_TRUE(footage.IsValid()); + + Decoder::CodecStream stream(path, footage.GetStreamIndex(Track::kVideo, 0), + nullptr); + ASSERT_TRUE(decoder->Open(stream)); + + Decoder::RetrieveVideoParams params; + params.time = rational(0); + params.maximum_format = PixelFormat::U16; + FramePtr frame = decoder->RetrieveVideoFrame(params); + ASSERT_TRUE(frame); + ASSERT_TRUE(frame->is_allocated()); + + const int width = frame->width(); + const int height = frame->height(); + ASSERT_GT(width, 0); + ASSERT_GT(height, 0); + + double avg = 0.0; + int samples = 0; + const int bpc = VideoParams::GetBytesPerChannel(frame->format()); + const int stride = frame->linesize_bytes(); + for (int y = 0; y < height && y < 1080; y += 120) { + for (int x = 0; x < width && x < 1920; x += 240) { + const uint8_t *p = reinterpret_cast( + frame->const_data() + y * stride + x * 4 * bpc); + if (bpc == 1) { + for (int c = 0; c < 3; ++c) { + avg += p[c] / 255.0; + } + } else { + const uint16_t *p16 = reinterpret_cast(p); + for (int c = 0; c < 3; ++c) { + avg += p16[c] / 65535.0; + } + } + samples += 3; + } + } + + const double brightness = samples ? avg / samples : 0.0; + std::cerr << "Frame size: " << width << "x" << height + << " format: " << static_cast(frame->format()) + << " brightness: " << brightness << std::endl; + + EXPECT_GT(brightness, 0.01); +}