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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user