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.
This commit is contained in:
2026-07-13 10:19:30 +08:00
parent 68c0477715
commit 800859b0ef
6 changed files with 125 additions and 10 deletions
+9 -1
View File
@@ -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,
+11 -5
View File
@@ -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);
}
}
}
+31 -4
View File
@@ -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(
+2
View File
@@ -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
+1
View File
@@ -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
+71
View File
@@ -0,0 +1,71 @@
#include <gtest/gtest.h>
#include <QCoreApplication>
#include <QDir>
#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<const uint8_t *>(
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<const uint16_t *>(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<int>(frame->format())
<< " brightness: " << brightness << std::endl;
EXPECT_GT(brightness, 0.01);
}