Use 10-bit packed preview format and add OCIO LUT regression tests
Renderer / preview: - Switch the preview/display readback format from F16 to packed 10-bit RGBA (PixelFormat::U10) to halve GPU->CPU/IPC bandwidth while keeping 10-bit panel precision. - Add U10 support to VideoParams, FFmpeg/OIIO/OCIO utility mappings, OpenGL (GL_RGB10_A2), Vulkan (VK_FORMAT_A2B10G10R10_UNORM_PACK32), and plugin bit-depth lookups. - Update preview autocacher comment to reflect the new behavior. OCIO LUT tests: - Add four E2E-style ColorLutNode gtests that drive a SolidGenerator -> OCIOLutNode graph through NodeTraverser and compare resulting pixels on the CPU. They cover forward/inverse transforms and verify that switching LUT direction and LUT file updates both the processor and the output pixels. Cleanup: - Remove a leftover SolidGenerator::Value debug fprintf. - Capture render worker stderr in RenderWorkerFootageTest for better diagnostics.
This commit is contained in:
@@ -167,6 +167,8 @@ AVPixelFormat FFmpegUtils::GetFFmpegPixelFormat(const PixelFormat &pix_fmt,
|
||||
switch (pix_fmt) {
|
||||
case PixelFormat::U8:
|
||||
return AV_PIX_FMT_RGB24;
|
||||
case PixelFormat::U10:
|
||||
return AV_PIX_FMT_NONE;
|
||||
case PixelFormat::U16:
|
||||
return AV_PIX_FMT_RGB48;
|
||||
case PixelFormat::F16:
|
||||
@@ -185,6 +187,8 @@ AVPixelFormat FFmpegUtils::GetFFmpegPixelFormat(const PixelFormat &pix_fmt,
|
||||
switch (pix_fmt) {
|
||||
case PixelFormat::U8:
|
||||
return AV_PIX_FMT_RGBA;
|
||||
case PixelFormat::U10:
|
||||
return AV_PIX_FMT_NONE;
|
||||
case PixelFormat::U16:
|
||||
return AV_PIX_FMT_RGBA64;
|
||||
case PixelFormat::F16:
|
||||
@@ -209,6 +213,8 @@ PixelFormat FFmpegUtils::GetCompatiblePixelFormat(const PixelFormat &pix_fmt)
|
||||
switch (pix_fmt) {
|
||||
case PixelFormat::U8:
|
||||
return PixelFormat::U8;
|
||||
case PixelFormat::U10:
|
||||
return PixelFormat::U8;
|
||||
case PixelFormat::U16:
|
||||
case PixelFormat::F16:
|
||||
case PixelFormat::F32:
|
||||
|
||||
@@ -29,6 +29,8 @@ OCIO::BitDepth OCIOUtils::GetOCIOBitDepthFromPixelFormat(PixelFormat format)
|
||||
switch (format) {
|
||||
case PixelFormat::U8:
|
||||
return OCIO::BIT_DEPTH_UINT8;
|
||||
case PixelFormat::U10:
|
||||
return OCIO::BIT_DEPTH_UINT10;
|
||||
case PixelFormat::U16:
|
||||
return OCIO::BIT_DEPTH_UINT16;
|
||||
break;
|
||||
|
||||
@@ -39,6 +39,8 @@ public:
|
||||
switch (format) {
|
||||
case PixelFormat::U8:
|
||||
return OIIO::TypeDesc::UINT8;
|
||||
case PixelFormat::U10:
|
||||
return OIIO::TypeDesc::UNKNOWN;
|
||||
case PixelFormat::U16:
|
||||
return OIIO::TypeDesc::UINT16;
|
||||
case PixelFormat::F16:
|
||||
|
||||
@@ -66,10 +66,6 @@ void SolidGenerator::Value(const NodeValueRow &value,
|
||||
const NodeGlobals &globals,
|
||||
NodeValueTable *table) const
|
||||
{
|
||||
Color c = value[kColorInput].toColor();
|
||||
fprintf(stderr,
|
||||
"SolidGenerator::Value color=%f %f %f %f\n",
|
||||
c.red(), c.green(), c.blue(), c.alpha());
|
||||
table->Push(NodeValue::kTexture,
|
||||
Texture::Job(globals.vparams(), ShaderJob(value)), this);
|
||||
}
|
||||
|
||||
@@ -305,6 +305,8 @@ const std::string &olive::plugin::OliveClipInstance::getUnmappedBitDepth() const
|
||||
return kBitDepthNoneStr;
|
||||
case PixelFormat::U8:
|
||||
return kBitDepthByteStr;
|
||||
case PixelFormat::U10:
|
||||
return kBitDepthNoneStr;
|
||||
case PixelFormat::U16:
|
||||
return kBitDepthShortStr;
|
||||
case PixelFormat::F16:
|
||||
|
||||
@@ -433,6 +433,8 @@ bool FrameHashCache::SaveCacheFrame(const QString &filename,
|
||||
fmt = QImage::Format_RGB888;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::U10:
|
||||
break;
|
||||
case PixelFormat::U16:
|
||||
if (frame->channel_count() == VideoParams::kRGBAChannelCount) {
|
||||
fmt = QImage::Format_RGBA64_Premultiplied;
|
||||
|
||||
@@ -835,6 +835,11 @@ GLint OpenGLRenderer::GetInternalFormat(PixelFormat format, int channel_layout)
|
||||
return GL_RGBA8;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::U10:
|
||||
if (channel_layout == 4) {
|
||||
return GL_RGB10_A2;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::U16:
|
||||
switch (channel_layout) {
|
||||
case 1:
|
||||
@@ -884,6 +889,8 @@ GLenum OpenGLRenderer::GetPixelType(PixelFormat format)
|
||||
switch (format) {
|
||||
case PixelFormat::U8:
|
||||
return GL_UNSIGNED_BYTE;
|
||||
case PixelFormat::U10:
|
||||
return GL_UNSIGNED_INT_2_10_10_10_REV;
|
||||
case PixelFormat::U16:
|
||||
return GL_UNSIGNED_SHORT;
|
||||
case PixelFormat::F16:
|
||||
|
||||
@@ -688,9 +688,11 @@ RenderTicketWatcher *PreviewAutoCacher::RenderFrame(Node *node,
|
||||
rvp.AddCache(frame_cache);
|
||||
} else {
|
||||
// Preview/display frames are rendered at reduced precision to cut the
|
||||
// GPU->CPU readback and IPC transfer bandwidth in half. The internal
|
||||
// render pipeline stays F32/ACEScg; only the final preview copy is F16.
|
||||
rvp.force_format = PixelFormat::F16;
|
||||
// GPU->CPU readback and IPC transfer bandwidth. The internal render
|
||||
// pipeline stays F32/ACEScg; the final preview copy is packed 10-bit
|
||||
// RGBA (4 bytes/pixel) to preserve 10-bit panel precision while halving
|
||||
// bandwidth compared to F16.
|
||||
rvp.force_format = PixelFormat::U10;
|
||||
rvp.force_channel_count = VideoParams::kRGBAChannelCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +209,8 @@ int VideoParams::GetBytesPerChannel(PixelFormat format)
|
||||
break;
|
||||
case PixelFormat::U8:
|
||||
return 1;
|
||||
case PixelFormat::U10:
|
||||
return 0; // packed format, use GetBytesPerPixel instead
|
||||
case PixelFormat::U16:
|
||||
case PixelFormat::F16:
|
||||
return 2;
|
||||
@@ -221,6 +223,10 @@ int VideoParams::GetBytesPerChannel(PixelFormat format)
|
||||
|
||||
int VideoParams::GetBytesPerPixel(PixelFormat format, int channels)
|
||||
{
|
||||
if (format == PixelFormat::U10) {
|
||||
// Packed 10-bit RGBA10A2: 4 bytes per RGBA pixel regardless of channel count
|
||||
return channels == VideoParams::kRGBAChannelCount ? 4 : 0;
|
||||
}
|
||||
return GetBytesPerChannel(format) * channels;
|
||||
}
|
||||
|
||||
@@ -238,6 +244,8 @@ QString VideoParams::GetFormatName(PixelFormat format)
|
||||
switch (format) {
|
||||
case PixelFormat::U8:
|
||||
return QCoreApplication::translate("VideoParams", "8-bit");
|
||||
case PixelFormat::U10:
|
||||
return QCoreApplication::translate("VideoParams", "10-bit Packed");
|
||||
case PixelFormat::U16:
|
||||
return QCoreApplication::translate("VideoParams", "16-bit Integer");
|
||||
case PixelFormat::F16:
|
||||
|
||||
@@ -1127,6 +1127,11 @@ VkFormat VulkanRenderer::PixelFormatToVkFormat(PixelFormat format,
|
||||
case 4: return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::U10:
|
||||
if (channel_count == 4) {
|
||||
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
}
|
||||
break;
|
||||
case PixelFormat::U16:
|
||||
switch (channel_count) {
|
||||
case 1: return VK_FORMAT_R16_UNORM;
|
||||
@@ -1203,6 +1208,8 @@ int VulkanRenderer::GetVkFormatBytesPerPixel(VkFormat format) const
|
||||
return 3;
|
||||
case VK_FORMAT_R8G8B8A8_UNORM:
|
||||
return 4;
|
||||
case VK_FORMAT_A2B10G10R10_UNORM_PACK32:
|
||||
return 4;
|
||||
case VK_FORMAT_R16_UNORM:
|
||||
case VK_FORMAT_R16_SFLOAT:
|
||||
return 2;
|
||||
|
||||
+1
-1
Submodule ext/core updated: faf4ba9d3f...c4f8f7bde1
@@ -11,6 +11,9 @@
|
||||
#include "node/color/ociolut/ociolut.h"
|
||||
#include "node/color/threewaycolor/threewaycolor.h"
|
||||
#include "node/factory.h"
|
||||
#include "node/generator/solid/solid.h"
|
||||
#include "node/project.h"
|
||||
#include "node/traverser.h"
|
||||
#include "render/colorprocessor.h"
|
||||
|
||||
namespace OCIO = OCIO_NAMESPACE;
|
||||
@@ -46,6 +49,117 @@ QString WriteTestCube(QTemporaryDir *dir)
|
||||
return path;
|
||||
}
|
||||
|
||||
// CPU-only traverser that resolves SolidGenerator and ColorTransformJob jobs
|
||||
// into real frames so we can compare pixels without a GPU/worker process.
|
||||
class PixelColorTransformTraverser : public olive::NodeTraverser {
|
||||
public:
|
||||
void Resolve(olive::NodeValue &value)
|
||||
{
|
||||
ResolveJobs(value);
|
||||
}
|
||||
|
||||
olive::FramePtr source_frame;
|
||||
olive::FramePtr output_frame;
|
||||
|
||||
protected:
|
||||
virtual void ProcessShader(olive::TexturePtr destination,
|
||||
const olive::Node *node,
|
||||
const olive::ShaderJob *job) override
|
||||
{
|
||||
Q_UNUSED(destination)
|
||||
Q_UNUSED(node)
|
||||
|
||||
const olive::Color c =
|
||||
job->GetValues().value(olive::SolidGenerator::kColorInput).toColor();
|
||||
|
||||
olive::VideoParams p = destination->params();
|
||||
p.set_format(olive::core::PixelFormat::F32);
|
||||
p.set_channel_count(olive::VideoParams::kRGBAChannelCount);
|
||||
|
||||
source_frame = olive::Frame::Create();
|
||||
source_frame->set_video_params(p);
|
||||
source_frame->allocate();
|
||||
|
||||
for (int y = 0; y < p.effective_height(); ++y) {
|
||||
for (int x = 0; x < p.effective_width(); ++x) {
|
||||
source_frame->set_pixel(x, y, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void ProcessColorTransform(olive::TexturePtr destination,
|
||||
const olive::Node *node,
|
||||
const olive::ColorTransformJob *job) override
|
||||
{
|
||||
Q_UNUSED(destination)
|
||||
Q_UNUSED(node)
|
||||
|
||||
olive::ColorProcessorPtr processor = job->GetColorProcessor();
|
||||
if (!processor || !source_frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
output_frame = olive::Frame::Create();
|
||||
output_frame->set_video_params(source_frame->video_params());
|
||||
output_frame->allocate();
|
||||
|
||||
std::memcpy(output_frame->data(), source_frame->const_data(),
|
||||
source_frame->allocated_size());
|
||||
|
||||
processor->ConvertFrame(output_frame);
|
||||
}
|
||||
};
|
||||
|
||||
QString WriteTestCubeLut(QTemporaryDir *dir, const char *title,
|
||||
float low, float high)
|
||||
{
|
||||
const QString path = QDir(dir->path()).filePath(
|
||||
QStringLiteral("%1.cube").arg(QString::fromUtf8(title)));
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
QTextStream stream(&file);
|
||||
stream << "TITLE \"" << title << "\"\n";
|
||||
stream << "LUT_1D_SIZE 2\n";
|
||||
stream << "DOMAIN_MIN 0.0 0.0 0.0\n";
|
||||
stream << "DOMAIN_MAX 1.0 1.0 1.0\n";
|
||||
stream << high << " " << high << " " << high << "\n";
|
||||
stream << low << " " << low << " " << low << "\n";
|
||||
file.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
// Non-symmetric 1D LUT so forward and inverse produce different, predictable
|
||||
// results. Table:
|
||||
// 0.0 -> 0.00
|
||||
// 0.5 -> 0.75
|
||||
// 1.0 -> 1.00
|
||||
// Forward: (0.25, 0.50, 0.75) -> (0.375, 0.750, 0.875)
|
||||
// Inverse: (0.25, 0.50, 0.75) -> (0.167, 0.333, 0.500)
|
||||
QString WriteAsymmetricCube(QTemporaryDir *dir)
|
||||
{
|
||||
const QString path =
|
||||
QDir(dir->path()).filePath(QStringLiteral("asymmetric.cube"));
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
const QByteArray data =
|
||||
"TITLE \"Oak test asymmetric\"\n"
|
||||
"LUT_1D_SIZE 3\n"
|
||||
"DOMAIN_MIN 0.0 0.0 0.0\n"
|
||||
"DOMAIN_MAX 1.0 1.0 1.0\n"
|
||||
"0.00 0.00 0.00\n"
|
||||
"0.75 0.75 0.75\n"
|
||||
"1.00 1.00 1.00\n";
|
||||
file.write(data);
|
||||
file.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ColorLut, OcioSupportsCubeAnd3dlExtensions)
|
||||
@@ -111,3 +225,263 @@ TEST(ColorV04, FactoryCreatesColorNodes)
|
||||
EXPECT_FLOAT_EQ(neutral.blue(), 0.5f);
|
||||
EXPECT_FLOAT_EQ(neutral.alpha(), 1.0f);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// E2E-style regression tests for OCIOLutNode.
|
||||
//
|
||||
// These tests build a tiny node graph (SolidGenerator -> OCIOLutNode), drive it
|
||||
// through NodeTraverser, and compare the resulting pixels. They specifically
|
||||
// exercise the previously broken paths:
|
||||
// * OCIOLutNode::Value() must ensure the processor exists before rendering.
|
||||
// * Switching the LUT direction must update the processor and change pixels.
|
||||
// * Switching the LUT file must update the processor and change pixels.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
TEST(ColorLutNode, ForwardDirectionInvertsPixels)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
olive::Project project;
|
||||
project.Initialize();
|
||||
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = WriteTestCubeLut(&dir, "invert", 0.0f, 1.0f);
|
||||
ASSERT_FALSE(path.isEmpty());
|
||||
|
||||
auto *solid = new olive::SolidGenerator();
|
||||
solid->setParent(&project);
|
||||
solid->SetStandardValue(
|
||||
olive::SolidGenerator::kColorInput,
|
||||
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
|
||||
|
||||
auto *lut = new olive::OCIOLutNode();
|
||||
lut->setParent(&project);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, path);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput, 0); // Forward
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
|
||||
|
||||
const olive::VideoParams params(
|
||||
16, 16, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount);
|
||||
|
||||
PixelColorTransformTraverser traverser;
|
||||
traverser.SetCacheVideoParams(params);
|
||||
|
||||
olive::NodeValueTable table = traverser.GenerateTable(
|
||||
lut, olive::TimeRange(olive::core::rational(0),
|
||||
olive::core::rational(1, 30)));
|
||||
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
|
||||
traverser.Resolve(tex_val);
|
||||
|
||||
ASSERT_TRUE(traverser.output_frame);
|
||||
const olive::Color out = traverser.output_frame->get_pixel(0, 0);
|
||||
|
||||
EXPECT_NEAR(out.red(), 0.75f, 0.02f);
|
||||
EXPECT_NEAR(out.green(), 0.50f, 0.02f);
|
||||
EXPECT_NEAR(out.blue(), 0.25f, 0.02f);
|
||||
EXPECT_NEAR(out.alpha(), 1.0f, 0.001f);
|
||||
}
|
||||
|
||||
TEST(ColorLutNode, InverseDirectionReversesForwardTransform)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
olive::Project project;
|
||||
project.Initialize();
|
||||
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = WriteTestCubeLut(&dir, "invert", 0.0f, 1.0f);
|
||||
ASSERT_FALSE(path.isEmpty());
|
||||
|
||||
auto *solid = new olive::SolidGenerator();
|
||||
solid->setParent(&project);
|
||||
solid->SetStandardValue(
|
||||
olive::SolidGenerator::kColorInput,
|
||||
QVariant::fromValue(olive::Color(0.75f, 0.50f, 0.25f, 1.0f)));
|
||||
|
||||
auto *lut = new olive::OCIOLutNode();
|
||||
lut->setParent(&project);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, path);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput, 1); // Inverse
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
|
||||
|
||||
const olive::VideoParams params(
|
||||
16, 16, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount);
|
||||
|
||||
PixelColorTransformTraverser traverser;
|
||||
traverser.SetCacheVideoParams(params);
|
||||
|
||||
olive::NodeValueTable table = traverser.GenerateTable(
|
||||
lut, olive::TimeRange(olive::core::rational(0),
|
||||
olive::core::rational(1, 30)));
|
||||
olive::NodeValue tex_val = table.Get(olive::NodeValue::kTexture);
|
||||
traverser.Resolve(tex_val);
|
||||
|
||||
ASSERT_TRUE(traverser.output_frame);
|
||||
const olive::Color out = traverser.output_frame->get_pixel(0, 0);
|
||||
|
||||
EXPECT_NEAR(out.red(), 0.25f, 0.02f);
|
||||
EXPECT_NEAR(out.green(), 0.50f, 0.02f);
|
||||
EXPECT_NEAR(out.blue(), 0.75f, 0.02f);
|
||||
EXPECT_NEAR(out.alpha(), 1.0f, 0.001f);
|
||||
}
|
||||
|
||||
TEST(ColorLutNode, SwitchingDirectionUpdatesProcessorAndPixels)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
olive::Project project;
|
||||
project.Initialize();
|
||||
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
const QString path = WriteAsymmetricCube(&dir);
|
||||
ASSERT_FALSE(path.isEmpty());
|
||||
|
||||
auto *solid = new olive::SolidGenerator();
|
||||
solid->setParent(&project);
|
||||
solid->SetStandardValue(
|
||||
olive::SolidGenerator::kColorInput,
|
||||
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
|
||||
|
||||
auto *lut = new olive::OCIOLutNode();
|
||||
lut->setParent(&project);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, path);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput, 0); // Forward
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
|
||||
|
||||
const olive::VideoParams params(
|
||||
16, 16, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount);
|
||||
|
||||
// First render: forward direction.
|
||||
PixelColorTransformTraverser forward_traverser;
|
||||
forward_traverser.SetCacheVideoParams(params);
|
||||
olive::NodeValueTable forward_table = forward_traverser.GenerateTable(
|
||||
lut, olive::TimeRange(olive::core::rational(0),
|
||||
olive::core::rational(1, 30)));
|
||||
olive::NodeValue forward_tex =
|
||||
forward_table.Get(olive::NodeValue::kTexture);
|
||||
forward_traverser.Resolve(forward_tex);
|
||||
|
||||
ASSERT_TRUE(forward_traverser.output_frame);
|
||||
const olive::Color forward_out =
|
||||
forward_traverser.output_frame->get_pixel(0, 0);
|
||||
EXPECT_NEAR(forward_out.red(), 0.375f, 0.02f);
|
||||
EXPECT_NEAR(forward_out.green(), 0.750f, 0.02f);
|
||||
EXPECT_NEAR(forward_out.blue(), 0.875f, 0.02f);
|
||||
|
||||
// Switch direction. Before the Value()/EnsureProcessor() fix, the node
|
||||
// would keep using the old forward processor.
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput, 1); // Inverse
|
||||
|
||||
PixelColorTransformTraverser inverse_traverser;
|
||||
inverse_traverser.SetCacheVideoParams(params);
|
||||
olive::NodeValueTable inverse_table = inverse_traverser.GenerateTable(
|
||||
lut, olive::TimeRange(olive::core::rational(0),
|
||||
olive::core::rational(1, 30)));
|
||||
olive::NodeValue inverse_tex =
|
||||
inverse_table.Get(olive::NodeValue::kTexture);
|
||||
inverse_traverser.Resolve(inverse_tex);
|
||||
|
||||
ASSERT_TRUE(inverse_traverser.output_frame);
|
||||
const olive::Color inverse_out =
|
||||
inverse_traverser.output_frame->get_pixel(0, 0);
|
||||
|
||||
// Inverse of the asymmetric LUT moves the same color to a darker result.
|
||||
EXPECT_NEAR(inverse_out.red(), 0.167f, 0.02f);
|
||||
EXPECT_NEAR(inverse_out.green(), 0.333f, 0.02f);
|
||||
EXPECT_NEAR(inverse_out.blue(), 0.500f, 0.02f);
|
||||
|
||||
// The two outputs must differ from each other.
|
||||
EXPECT_GT(std::abs(forward_out.red() - inverse_out.red()), 0.1f);
|
||||
EXPECT_GT(std::abs(forward_out.green() - inverse_out.green()), 0.1f);
|
||||
EXPECT_GT(std::abs(forward_out.blue() - inverse_out.blue()), 0.1f);
|
||||
}
|
||||
|
||||
TEST(ColorLutNode, SwitchingFileUpdatesProcessorAndPixels)
|
||||
{
|
||||
olive::ColorManager::SetUpDefaultConfig();
|
||||
|
||||
olive::Project project;
|
||||
project.Initialize();
|
||||
|
||||
QTemporaryDir dir;
|
||||
ASSERT_TRUE(dir.isValid());
|
||||
|
||||
const QString invert_path = WriteTestCubeLut(
|
||||
&dir, "invert", 0.0f, 1.0f);
|
||||
const QString boost_path = WriteTestCubeLut(
|
||||
&dir, "boost", 0.5f, 1.0f);
|
||||
ASSERT_FALSE(invert_path.isEmpty());
|
||||
ASSERT_FALSE(boost_path.isEmpty());
|
||||
|
||||
auto *solid = new olive::SolidGenerator();
|
||||
solid->setParent(&project);
|
||||
solid->SetStandardValue(
|
||||
olive::SolidGenerator::kColorInput,
|
||||
QVariant::fromValue(olive::Color(0.25f, 0.50f, 0.75f, 1.0f)));
|
||||
|
||||
auto *lut = new olive::OCIOLutNode();
|
||||
lut->setParent(&project);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, invert_path);
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kDirectionInput, 0); // Forward
|
||||
|
||||
olive::Node::ConnectEdge(
|
||||
solid, olive::NodeInput(lut, olive::OCIOLutNode::kTextureInput));
|
||||
|
||||
const olive::VideoParams params(
|
||||
16, 16, olive::core::PixelFormat::F32,
|
||||
olive::VideoParams::kRGBAChannelCount);
|
||||
|
||||
// Render with invert LUT.
|
||||
PixelColorTransformTraverser invert_traverser;
|
||||
invert_traverser.SetCacheVideoParams(params);
|
||||
olive::NodeValueTable invert_table = invert_traverser.GenerateTable(
|
||||
lut, olive::TimeRange(olive::core::rational(0),
|
||||
olive::core::rational(1, 30)));
|
||||
olive::NodeValue invert_tex = invert_table.Get(olive::NodeValue::kTexture);
|
||||
invert_traverser.Resolve(invert_tex);
|
||||
|
||||
ASSERT_TRUE(invert_traverser.output_frame);
|
||||
const olive::Color invert_out =
|
||||
invert_traverser.output_frame->get_pixel(0, 0);
|
||||
EXPECT_NEAR(invert_out.red(), 0.75f, 0.02f);
|
||||
|
||||
// Switch to a different LUT file. Before the fix, the node could keep the
|
||||
// old invert processor cached and the output would not change.
|
||||
lut->SetStandardValue(olive::OCIOLutNode::kFileInput, boost_path);
|
||||
|
||||
PixelColorTransformTraverser boost_traverser;
|
||||
boost_traverser.SetCacheVideoParams(params);
|
||||
olive::NodeValueTable boost_table = boost_traverser.GenerateTable(
|
||||
lut, olive::TimeRange(olive::core::rational(0),
|
||||
olive::core::rational(1, 30)));
|
||||
olive::NodeValue boost_tex = boost_table.Get(olive::NodeValue::kTexture);
|
||||
boost_traverser.Resolve(boost_tex);
|
||||
|
||||
ASSERT_TRUE(boost_traverser.output_frame);
|
||||
const olive::Color boost_out =
|
||||
boost_traverser.output_frame->get_pixel(0, 0);
|
||||
|
||||
// boost LUT maps:
|
||||
// 0.0 -> 1.0, 1.0 -> 0.5 (linear: f(x) = 1 - 0.5*x)
|
||||
// so (0.25, 0.50, 0.75) -> (0.875, 0.750, 0.625).
|
||||
EXPECT_NEAR(boost_out.red(), 0.875f, 0.02f);
|
||||
EXPECT_NEAR(boost_out.green(), 0.750f, 0.02f);
|
||||
EXPECT_NEAR(boost_out.blue(), 0.625f, 0.02f);
|
||||
|
||||
// The two outputs must differ (boost raises all channels vs invert).
|
||||
EXPECT_GT(std::abs(boost_out.red() - invert_out.red()), 0.1f);
|
||||
EXPECT_GT(std::abs(boost_out.green() - invert_out.green()), 0.1f);
|
||||
EXPECT_GT(std::abs(boost_out.blue() - invert_out.blue()), 0.1f);
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ protected:
|
||||
}
|
||||
|
||||
// ---- spawn worker ----
|
||||
worker_.setProcessChannelMode(QProcess::ForwardedErrorChannel);
|
||||
worker_.setProcessChannelMode(QProcess::SeparateChannels);
|
||||
worker_.start(worker_path_, QStringList{QStringLiteral("--backend"), backend});
|
||||
if (!worker_.waitForStarted(kTimeoutMs)) {
|
||||
return false;
|
||||
@@ -362,6 +362,13 @@ protected:
|
||||
if (worker_.state() == QProcess::NotRunning) {
|
||||
std::cerr << "WaitForMessage: worker exited with code "
|
||||
<< worker_.exitCode() << std::endl;
|
||||
std::cerr << "Worker stdout buffer: "
|
||||
<< read_buffer_.toStdString() << std::endl;
|
||||
QByteArray err = worker_.readAllStandardError();
|
||||
if (!err.isEmpty()) {
|
||||
std::cerr << "Worker stderr:\n"
|
||||
<< err.toStdString() << std::endl;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user