color: global LUT library, LUT error reporting, clamp + display fixes

- Add a global LUT library: user-configurable directories (new
  Preferences > LUT tab) scanned recursively for .cube/.3dl files; LUT
  node file pickers offer the library dirs as sidebar shortcuts via a
  'lut_library' input property handled by the param view bridge
- OCIOLutNode no longer fails silently: missing files, unsupported
  extensions and OCIO load errors are recorded in last_error() and
  surfaced in the status bar (input still passes through for rendering
  safety)
- ColorDialog: re-enable the display -> reference conversion using
  ColorProcessor::kInverse with a validity guard, and re-enable the
  Display tab in ColorValuesWidget; covered by a round-trip regression
  test proving the old OCIO inverse crash no longer occurs
- OCIOGradingTransformLinearNode: enforce the OCIO clampWhite >
  clampBlack invariant per frame in Value() so keyframed/connected
  values cannot produce invalid grading transforms, and constrain the
  white clamp UI minimum whenever the black clamp is static
- Regression tests for LUT extension checks, direction switching, node
  error reporting, LUT library scanning, display inverse round-trip and
  clamp enforcement
This commit is contained in:
2026-07-16 22:53:33 +08:00
parent ddca6a5e01
commit 547c2480e0
18 changed files with 829 additions and 33 deletions
@@ -92,10 +92,10 @@ OCIOGradingTransformLinearNode::OCIOGradingTransformLinearNode()
GetStandardValue(kClampWhiteEnableInput).toBool());
SetInputProperty(kClampWhiteInput, QStringLiteral("base"), 0.01);
// FIXME: Temporarily disabled. This will break if "clamp black" is keyframed or connected to
// something and there's currently no solution to remedy that. If there is in the future,
// we can look into re-enabling this.
//SetInputProperty(kClampWhiteInput, QStringLiteral("min"), GetStandardValue(kClampBlackInput).toDouble() + 0.000001);
// Constrain the white clamp minimum to just above the (static) black clamp
// as per OCIO::GradingPrimary::validate. When the black clamp is keyframed
// or connected, Value() enforces the invariant per frame instead.
UpdateClampWhiteMinimum();
}
QString OCIOGradingTransformLinearNode::Name() const
@@ -149,16 +149,49 @@ void OCIOGradingTransformLinearNode::InputValueChangedEvent(
SetInputProperty(kClampBlackInput, QStringLiteral("enabled"),
GetStandardValue(kClampBlackEnableInput).toBool());
} else if (input == kClampBlackInput) {
// Ensure the white clamp is always greater than the black clamp as per OCIO::GradingPrimary::validate
// FIXME: Temporarily disabled. This will break if "clamp black" is keyframed or connected to
// something and there's currently no solution to remedy that. If there is in the future,
// we can look into re-enabling this.
//SetInputProperty(kClampWhiteInput, QStringLiteral("min"), GetStandardValue(kClampBlackInput).toDouble() + 0.000001);
// Ensure the white clamp is always greater than the black clamp as per
// OCIO::GradingPrimary::validate
UpdateClampWhiteMinimum();
}
GenerateProcessor();
}
void OCIOGradingTransformLinearNode::InputConnectedEvent(const QString &input,
int element, Node *output)
{
super::InputConnectedEvent(input, element, output);
if (input == kClampBlackInput) {
UpdateClampWhiteMinimum();
}
}
void OCIOGradingTransformLinearNode::InputDisconnectedEvent(const QString &input,
int element,
Node *output)
{
super::InputDisconnectedEvent(input, element, output);
if (input == kClampBlackInput) {
UpdateClampWhiteMinimum();
}
}
void OCIOGradingTransformLinearNode::UpdateClampWhiteMinimum()
{
// A static UI minimum cannot follow an animated black clamp; for keyframed
// or connected values the white>black invariant is enforced per frame in
// Value() instead
if (IsInputKeyframing(kClampBlackInput) ||
IsInputConnected(kClampBlackInput)) {
return;
}
SetInputProperty(kClampWhiteInput, QStringLiteral("min"),
GetStandardValue(kClampBlackInput).toDouble() + 0.000001);
}
void OCIOGradingTransformLinearNode::GenerateProcessor()
{
if (manager()) {
@@ -241,6 +274,21 @@ void OCIOGradingTransformLinearNode::Value(const NodeValueRow &value,
OCIO::GradingPrimary::NoClampWhite()));
}
if (value[kClampBlackEnableInput].toBool() &&
value[kClampWhiteEnableInput].toBool()) {
// OCIO::GradingPrimary::validate requires the white clamp to be
// greater than the black clamp. Keyframed or connected values
// can violate this at arbitrary times, so enforce the invariant
// per frame here.
const double clamp_black = value[kClampBlackInput].toDouble();
const double clamp_white = value[kClampWhiteInput].toDouble();
if (clamp_white <= clamp_black) {
job.Insert(kClampWhiteInput,
NodeValue(NodeValue::kFloat,
clamp_black + 0.000001));
}
}
table->Push(NodeValue::kTexture, tex->toJob(job), this);
}
}
@@ -43,6 +43,10 @@ public:
virtual void Retranslate() override;
virtual void InputValueChangedEvent(const QString &input,
int element) override;
virtual void InputConnectedEvent(const QString &input, int element,
Node *output) override;
virtual void InputDisconnectedEvent(const QString &input, int element,
Node *output) override;
void GenerateProcessor();
virtual void Value(const NodeValueRow &value, const NodeGlobals &globals,
@@ -63,6 +67,16 @@ protected slots:
private:
void SetVec4InputColors(const QString &input);
/**
* @brief Constrains the white clamp UI minimum to just above the black
* clamp, as required by OCIO::GradingPrimary::validate
*
* Only applies while the black clamp is a static value; when it is
* keyframed or connected the invariant is enforced per frame in Value()
* instead.
*/
void UpdateClampWhiteMinimum();
};
} // olive
+32 -7
View File
@@ -25,7 +25,9 @@
#include <QApplication>
#include "core.h"
#include "node/color/colormanager/colormanager.h"
#include "render/lutlibrary.h"
#include "render/previewautocacher.h"
#include "render/rendermanager.h"
@@ -40,12 +42,6 @@ const QString OCIOLutNode::kDirectionInput = QStringLiteral("lut_dir_in");
namespace
{
bool IsSupportedLutExtension(const QString &suffix)
{
const QString lower = suffix.toLower();
return lower == QStringLiteral("cube") || lower == QStringLiteral("3dl");
}
bool IsMainProcess()
{
return qobject_cast<QApplication *>(QCoreApplication::instance()) !=
@@ -86,6 +82,8 @@ OCIOLutNode::OCIOLutNode()
tr("LUT Files (*.cube *.3dl);;Cube LUT (*.cube);;3DL LUT (*.3dl);;All Files (*)"));
SetInputProperty(kFileInput, QStringLiteral("placeholder"),
tr("Select a .cube or .3dl LUT file"));
// Allow the UI to offer the global LUT library for this input
SetInputProperty(kFileInput, QStringLiteral("lut_library"), true);
AddInput(kDirectionInput, NodeValue::kCombo, 0,
InputFlags(kInputFlagNotKeyframable | kInputFlagNotConnectable));
@@ -194,6 +192,21 @@ void OCIOLutNode::EnsureProcessor() const
CreateProcessorFromInputs();
}
void OCIOLutNode::SetLastError(const QString &error) const
{
if (last_error_ == error) {
return;
}
last_error_ = error;
// Make the error visible to the user instead of failing silently, but only
// from the main process (the render worker has no status bar)
if (!error.isEmpty() && IsMainProcess() && Core::instance()) {
Core::instance()->ShowStatusBarMessage(error, 10000);
}
}
bool OCIOLutNode::CreateProcessorFromInputs() const
{
if (!manager()) {
@@ -214,6 +227,7 @@ bool OCIOLutNode::CreateProcessorFromInputs() const
last_path_.clear();
last_direction_ = -1;
processor_dirty_ = false;
SetLastError(QString());
return false;
}
@@ -231,17 +245,22 @@ bool OCIOLutNode::CreateProcessorFromInputs() const
last_path_.clear();
last_direction_ = -1;
processor_dirty_ = false;
SetLastError(tr("OCIO LUT: file does not exist: %1").arg(path));
return false;
}
const QString suffix = info.suffix();
if (!IsSupportedLutExtension(suffix)) {
if (!LUTLibrary::IsSupportedExtension(suffix)) {
qWarning() << "Unsupported OCIO LUT file extension:" << path;
const_cast<OCIOLutNode *>(this)->set_processor(nullptr);
last_processor_.reset();
last_path_.clear();
last_direction_ = -1;
processor_dirty_ = false;
SetLastError(
tr("OCIO LUT: unsupported LUT file extension (expected .cube or "
".3dl): %1")
.arg(path));
return false;
}
@@ -267,6 +286,12 @@ bool OCIOLutNode::CreateProcessorFromInputs() const
processor = nullptr;
}
if (!processor) {
SetLastError(tr("OCIO LUT: failed to load LUT file: %1").arg(path));
} else {
SetLastError(QString());
}
last_path_ = path;
last_direction_ = direction;
last_processor_ = processor;
+15
View File
@@ -50,6 +50,18 @@ public:
static const QString kFileInput;
static const QString kDirectionInput;
/**
* @brief Human-readable description of why no LUT processor is active
*
* Empty when a valid LUT processor is in use or no LUT file has been
* selected yet. This allows the UI (and tests) to surface silent
* passthrough states (missing file, unsupported extension, OCIO errors).
*/
const QString &last_error() const
{
return last_error_;
}
protected slots:
virtual void ConfigChanged() override;
@@ -58,11 +70,14 @@ private:
void EnsureProcessor() const;
bool CreateProcessorFromInputs() const;
void SetLastError(const QString &error) const;
mutable QMutex gen_mutex_;
mutable bool processor_dirty_ = true;
mutable QString last_path_;
mutable int last_direction_ = -1;
mutable ColorProcessorPtr last_processor_;
mutable QString last_error_;
};
} // namespace olive