Blender V5.0
libocio_display_processor.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2025 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#if defined(WITH_OPENCOLORIO)
6
7# include <cfloat>
8# include <optional>
9# include <sstream>
10
11# include "BLI_colorspace.hh"
12# include "BLI_math_matrix.hh"
13
14# include "OCIO_config.hh"
15# include "OCIO_matrix.hh"
16# include "OCIO_view.hh"
17
18# include "error_handling.hh"
19# include "libocio_config.hh"
20# include "libocio_display.hh"
22
23# include "../white_point.hh"
24
25# include "CLG_log.h"
26
27static CLG_LogRef LOG = {"color_management"};
28
29namespace blender::ocio {
30
31static TransferFunction system_extended_srgb_transfer_function(const LibOCIOView *view,
32 const bool use_hdr_buffer)
33{
34# ifdef __APPLE__
35 /* The Metal backend always uses sRGB or extended sRGB buffer.
36 *
37 * How this will be decoded depends on the macOS display preset, but from testing
38 * on a MacBook P3 M3 it appears:
39 * - Apple XDR Display (P3 - 1600 nits): Decode with gamma 2.2
40 * - HDR Video (P3-ST 2084): Decode with sRGB. As we encode with the sRGB transfer
41 * function, this will be cancelled out, and linear values will be passed on
42 * effectively unmodified.
43 */
44 UNUSED_VARS(use_hdr_buffer, view);
46# elif defined(_WIN32)
47 /* The Vulkan backend uses either sRGB for SDR, or linear extended sRGB for HDR.
48 *
49 * - Windows HDR mode off: use_hdr_buffer will be false, and we encode with sRGB.
50 * By default Windows will decode with gamma 2.2.
51 * - Windows HDR mode on: use_hdr_buffer will be true, and we encode with sRGB.
52 * The Vulkan HDR swapchain blitting will decode with sRGB to cancel this out
53 * exactly, meaning we effectively pass on linear values unmodified.
54 *
55 * Note this means that both the user interface and SDR content will not be
56 * displayed the same in HDR mode off and on. However it is consistent with other
57 * software. To match, gamma 2.2 would have to be used.
58 */
59 UNUSED_VARS(use_hdr_buffer, view);
61# else
62 /* The Vulkan backend uses either sRGB for SDR, or linear extended sRGB for HDR.
63 *
64 * - When using a HDR swapchain and the display + view is HDR, ensure we pass on
65 * values linearly by doing gamma 2.2 encode here + gamma 2.2 decode in the
66 * Vulkan HDR swapchain blitting.
67 * - When using HDR swapain and the display + view is SDR, use sRGB encode to
68 * emulate what happens on a typical SDR monitor.
69 * - When using an SDR swapchain, the buffer is always sRGB.
70 */
71 return (use_hdr_buffer && view && view->is_hdr()) ? TransferFunction::Gamma22 :
73# endif
74}
75
76static OCIO_NAMESPACE::TransformRcPtr create_extended_srgb_transform(
77 const TransferFunction transfer_function)
78{
79 if (transfer_function == TransferFunction::sRGB) {
80 /* Piecewise sRGB transfer function. */
81 auto to_ui = OCIO_NAMESPACE::ExponentWithLinearTransform::Create();
82 to_ui->setGamma({2.4, 2.4, 2.4, 1.0});
83 to_ui->setOffset({0.055, 0.055, 0.055, 0.0});
84 /* Mirrored for negative as specified by scRGB and extended sRGB. */
85 to_ui->setNegativeStyle(OCIO_NAMESPACE::NEGATIVE_MIRROR);
86 to_ui->setDirection(OCIO_NAMESPACE::TRANSFORM_DIR_INVERSE);
87 return to_ui;
88 }
89
90 /* Pure gamma 2.2 function. */
91 auto to_ui = OCIO_NAMESPACE::ExponentTransform::Create();
92 to_ui->setValue({2.2, 2.2, 2.2, 1.0});
93 /* Mirrored for negative as specified by scRGB and extended sRGB. */
94 to_ui->setNegativeStyle(OCIO_NAMESPACE::NEGATIVE_MIRROR);
95 to_ui->setDirection(OCIO_NAMESPACE::TRANSFORM_DIR_INVERSE);
96 return to_ui;
97}
98
99static void adjust_for_hdr_image_file(const LibOCIOConfig &config,
100 OCIO_NAMESPACE::GroupTransformRcPtr &group,
101 StringRefNull display_name,
102 StringRefNull view_name)
103{
104 /* Convert HDR PQ and HLG images from 100 nits to 203 nits convention. */
105 const LibOCIODisplay *display = static_cast<const LibOCIODisplay *>(
106 config.get_display_by_name(display_name));
107 const LibOCIOView *view = (display) ? static_cast<const LibOCIOView *>(
108 display->get_view_by_name(view_name)) :
109 nullptr;
110 const LibOCIOColorSpace *display_colorspace = static_cast<const LibOCIOColorSpace *>(
111 view->display_colorspace());
112
113 if (display_colorspace == nullptr || !display_colorspace->is_display_referred()) {
114 return;
115 }
116
117 const ColorSpace *image_display_colorspace = config.get_color_space_for_hdr_image(
118 display_colorspace->name());
119 if (ELEM(image_display_colorspace, nullptr, display_colorspace)) {
120 return;
121 }
122
123 auto to_display_linear = OCIO_NAMESPACE::ColorSpaceTransform::Create();
124 to_display_linear->setSrc(display_colorspace->name().c_str());
125 to_display_linear->setDst(image_display_colorspace->name().c_str());
126 group->appendTransform(to_display_linear);
127}
128
129static void display_as_extended_srgb(const LibOCIOConfig &config,
130 OCIO_NAMESPACE::GroupTransformRcPtr &group,
131 StringRefNull display_name,
132 StringRefNull view_name,
133 const bool use_hdr_buffer)
134{
135 /* Emulate the user specified display on an extended sRGB display, conceptually:
136 * - Apply the view and display transform
137 * - Clamp colors to be within gamut
138 * - Convert to cie_xyz_d65_interchange
139 * - Convert to extended sRGB or gamma 2.2 scRGB
140 *
141 * When possible, we do equivalent but faster transforms. */
142
143 /* TODO: Optimization: Often the view transform will already clamp. Maybe we can have a
144 * few hardcoded checks for known view transforms? This helps eliminate a clamp and
145 * in some cases a matrix multiplication. */
146
147 const LibOCIODisplay *display = static_cast<const LibOCIODisplay *>(
148 config.get_display_by_name(display_name));
149 const LibOCIOView *view = (display) ? static_cast<const LibOCIOView *>(
150 display->get_view_by_name(view_name)) :
151 nullptr;
152 if (view == nullptr) {
153 CLOG_WARN(&LOG,
154 "Unable to find display '%s' and view '%s', display may be incorrect",
155 display_name.c_str(),
156 view_name.c_str());
157 return;
158 }
159
160 const TransferFunction target_transfer_function = system_extended_srgb_transfer_function(
161 view, use_hdr_buffer);
162
163 /* If we are already in the desired display colorspace, all we have to do is clamp. */
164 if ((view->transfer_function() == target_transfer_function ||
165 (view->transfer_function() == TransferFunction::ExtendedsRGB &&
166 target_transfer_function == TransferFunction::sRGB)) &&
167 view->gamut() == Gamut::Rec709)
168 {
169 auto clamp = OCIO_NAMESPACE::RangeTransform::Create();
170 clamp->setStyle(OCIO_NAMESPACE::RANGE_CLAMP);
171 clamp->setMinInValue(0.0);
172 clamp->setMinOutValue(0.0);
173 if (view->transfer_function() != TransferFunction::ExtendedsRGB) {
174 clamp->setMaxInValue(1.0);
175 clamp->setMaxOutValue(1.0);
176 }
177 group->appendTransform(clamp);
178 return;
179 }
180
181 const LibOCIOColorSpace *lin_cie_xyz_d65 = static_cast<const LibOCIOColorSpace *>(
182 config.get_color_space(OCIO_NAMESPACE::ROLE_INTERCHANGE_DISPLAY));
183 const LibOCIOColorSpace *display_colorspace = static_cast<const LibOCIOColorSpace *>(
184 view->display_colorspace());
185
186 /* Verify if all conditions are met to do automatic display color management. */
187 if (lin_cie_xyz_d65 == nullptr) {
189 "Failed to find %s colorspace, disabling automatic display color management",
190 OCIO_NAMESPACE::ROLE_INTERCHANGE_DISPLAY);
191 return;
192 }
193 if (display_colorspace == nullptr) {
195 "Failed to find display colorspace for view %s, disabling automatic display color "
196 "management",
197 view_name.c_str());
198 return;
199 }
200 if (!display_colorspace->is_display_referred()) {
202 "Color space %s is not a display color space, disabling automatic display color "
203 "management",
204 display_colorspace->name().c_str());
205 return;
206 }
207
208 /* Find the matrix to convert to linear colorspace with gamut of the display colorspace. */
209 std::optional<double4x4> xyz_to_display_gamut;
210 switch (view->gamut()) {
211 case Gamut::Rec709: {
212 xyz_to_display_gamut = OCIO_XYZ_TO_REC709;
213 break;
214 }
215 case Gamut::P3D65: {
216 xyz_to_display_gamut = OCIO_XYZ_TO_P3;
217 break;
218 }
219 case Gamut::Rec2020: {
220 xyz_to_display_gamut = OCIO_XYZ_TO_REC2020;
221 break;
222 }
223 case Gamut::Unknown: {
224 break;
225 }
226 }
227
228 if (xyz_to_display_gamut.has_value() && view->transfer_function() != TransferFunction::Unknown) {
229 /* Optimized path for known gamut and transfer function. We want OpenColorIO to cancel out
230 * out the transfer function of the chosen display, but this is not possible when clamping
231 * happens in the middle of it.
232 *
233 * So here we transform to the linear colorspace with the gamut of the display colorspace,
234 * and clamp there. This means there will be only matrix multiplications, or nothing at
235 * all for Rec.709. */
236 auto to_cie_xyz_d65 = OCIO_NAMESPACE::ColorSpaceTransform::Create();
237 to_cie_xyz_d65->setSrc(display_colorspace->name().c_str());
238 to_cie_xyz_d65->setDst(lin_cie_xyz_d65->name().c_str());
239 group->appendTransform(to_cie_xyz_d65);
240
241 auto to_lin_gamut = OCIO_NAMESPACE::MatrixTransform::Create();
242 to_lin_gamut->setMatrix(math::transpose(xyz_to_display_gamut.value()).base_ptr());
243 group->appendTransform(to_lin_gamut);
244
245 /* Clamp colors to the chosen display colorspace, to emulate it on the actual display that
246 * may have a wider gamut or HDR. */
247 double clamp_max = 0.0;
248 switch (view->transfer_function()) {
254 clamp_max = 1.0;
255 break;
257 clamp_max = 100.0; /* 10000 peak nits / 100 nits. */
258 break;
260 clamp_max = 10.0; /* 1000 peak nits / 100 nits. */
261 break;
263 clamp_max = DBL_MAX; /* Allow HDR > 1.0. */
264 break;
266 break;
267 }
268
269 auto clamp = OCIO_NAMESPACE::RangeTransform::Create();
270 clamp->setStyle(OCIO_NAMESPACE::RANGE_CLAMP);
271 clamp->setMinInValue(0.0);
272 clamp->setMinOutValue(0.0);
273 if (clamp_max != DBL_MAX) {
274 clamp->setMaxInValue(clamp_max);
275 clamp->setMaxOutValue(clamp_max);
276 }
277 group->appendTransform(clamp);
278
279 /* Transform to linear Rec.709. */
280 if (view->gamut() != Gamut::Rec709) {
281 auto to_rec709 = OCIO_NAMESPACE::MatrixTransform::Create();
282 to_rec709->setMatrix(
283 math::transpose(OCIO_XYZ_TO_REC709 * math::invert(xyz_to_display_gamut.value()))
284 .base_ptr());
285 group->appendTransform(to_rec709);
286 }
287 }
288 else {
289 /* Clamp colors to the chosen display colorspace, to emulate it on the actual display that
290 * may have a wider gamut or HDR. Only do it for transfer functions where we know it's
291 * correct, if unknown we hope the view transform already did it. */
292 if (view->transfer_function() != TransferFunction::Unknown) {
293 auto clamp = OCIO_NAMESPACE::RangeTransform::Create();
294 clamp->setStyle(OCIO_NAMESPACE::RANGE_CLAMP);
295 clamp->setMinInValue(0.0);
296 clamp->setMinOutValue(0.0);
297 if (view->transfer_function() != TransferFunction::ExtendedsRGB) {
298 clamp->setMaxInValue(1.0);
299 clamp->setMaxOutValue(1.0);
300 }
301 group->appendTransform(clamp);
302 }
303
304 /* Convert from display colorspace to linear Rec.709. */
305 auto to_cie_xyz_d65 = OCIO_NAMESPACE::ColorSpaceTransform::Create();
306 to_cie_xyz_d65->setSrc(display_colorspace->name().c_str());
307 to_cie_xyz_d65->setDst(lin_cie_xyz_d65->name().c_str());
308 group->appendTransform(to_cie_xyz_d65);
309
310 auto to_rec709 = OCIO_NAMESPACE::MatrixTransform::Create();
311 to_rec709->setMatrix(math::transpose(OCIO_XYZ_TO_REC709).base_ptr());
312 group->appendTransform(to_rec709);
313 }
314
315 group->appendTransform(create_extended_srgb_transform(target_transfer_function));
316}
317
318OCIO_NAMESPACE::TransformRcPtr create_ocio_display_transform(
319 const OCIO_NAMESPACE::ConstConfigRcPtr &ocio_config,
320 StringRefNull display,
321 StringRefNull view,
322 StringRefNull look,
323 StringRefNull from_colorspace)
324{
325 OCIO_NAMESPACE::GroupTransformRcPtr group = OCIO_NAMESPACE::GroupTransform::Create();
326
327 /* Add look transform. */
328 bool use_look = (look != nullptr && look[0] != '\0' && look != "None");
329 if (use_look) {
330 const char *look_output = nullptr;
331
332 try {
333 look_output = OCIO_NAMESPACE::LookTransform::GetLooksResultColorSpace(
334 ocio_config, ocio_config->getCurrentContext(), look.c_str());
335 }
336 catch (OCIO_NAMESPACE::Exception &exception) {
337 report_exception(exception);
338 return nullptr;
339 }
340
341 if (look_output != nullptr && look_output[0] != 0) {
342 OCIO_NAMESPACE::LookTransformRcPtr lt = OCIO_NAMESPACE::LookTransform::Create();
343 lt->setSrc(from_colorspace.c_str());
344 lt->setDst(look_output);
345 lt->setLooks(look.c_str());
346 group->appendTransform(lt);
347
348 /* Make further transforms aware of the color space change. */
349 from_colorspace = look_output;
350 }
351 else {
352 /* For empty looks, no output color space is returned. */
353 use_look = false;
354 }
355 }
356
357 /* Add view and display transform. */
358 OCIO_NAMESPACE::DisplayViewTransformRcPtr dvt = OCIO_NAMESPACE::DisplayViewTransform::Create();
359 dvt->setSrc(from_colorspace.c_str());
360 dvt->setLooksBypass(use_look);
361 dvt->setView(view.c_str());
362 dvt->setDisplay(display.c_str());
363 group->appendTransform(dvt);
364
365 return group;
366}
367
368static OCIO_NAMESPACE::TransformRcPtr create_untonemapped_ocio_display_transform(
369 const LibOCIOConfig &config,
370 StringRefNull display_name,
371 StringRefNull from_colorspace,
372 bool use_hdr_buffer)
373{
374 /* Convert to extended sRGB without any tone mapping. */
375 const auto group = OCIO_NAMESPACE::GroupTransform::Create();
376
377 const auto to_scene_linear = OCIO_NAMESPACE::ColorSpaceTransform::Create();
378 to_scene_linear->setSrc(from_colorspace.c_str());
379 to_scene_linear->setDst(OCIO_NAMESPACE::ROLE_SCENE_LINEAR);
380 group->appendTransform(to_scene_linear);
381
382 const auto to_rec709 = OCIO_NAMESPACE::MatrixTransform::Create();
383 to_rec709->setMatrix(math::transpose(double4x4(colorspace::scene_linear_to_rec709)).base_ptr());
384 group->appendTransform(to_rec709);
385
386 const LibOCIODisplay *display = static_cast<const LibOCIODisplay *>(
387 config.get_display_by_name(display_name));
388 const LibOCIOView *view = (display) ? static_cast<const LibOCIOView *>(
389 display->get_untonemapped_view()) :
390 nullptr;
391 group->appendTransform(create_extended_srgb_transform(
392 system_extended_srgb_transfer_function(view, use_hdr_buffer)));
393 return group;
394}
395
396OCIO_NAMESPACE::ConstProcessorRcPtr create_ocio_display_processor(
397 const LibOCIOConfig &config, const DisplayParameters &display_parameters)
398{
399 using namespace OCIO_NAMESPACE;
400
401 const ConstConfigRcPtr &ocio_config = config.get_ocio_config();
402
403 GroupTransformRcPtr group = GroupTransform::Create();
404
405 const char *from_colorspace = display_parameters.from_colorspace.c_str();
406
407 /* Linear transforms. */
408 if (display_parameters.scale != 1.0f || display_parameters.use_white_balance) {
409 /* Always apply exposure and/or white balance in scene linear. */
410 ColorSpaceTransformRcPtr ct = ColorSpaceTransform::Create();
411 ct->setSrc(from_colorspace);
412 ct->setDst(ROLE_SCENE_LINEAR);
413 group->appendTransform(ct);
414
415 /* Make further transforms aware of the color space change. */
416 from_colorspace = ROLE_SCENE_LINEAR;
417
418 /* Apply scale. */
419 MatrixTransformRcPtr mt = MatrixTransform::Create();
420 float3x3 matrix = float3x3::identity() * display_parameters.scale;
421
422 /* Apply white balance. */
423 if (display_parameters.use_white_balance) {
425 config, display_parameters.temperature, display_parameters.tint);
426 }
427
428 mt->setMatrix(double4x4(math::transpose(matrix)).base_ptr());
429 group->appendTransform(mt);
430 }
431
432 if (!display_parameters.view.is_empty()) {
433 /* Core display processor. */
434 group->appendTransform(create_ocio_display_transform(ocio_config,
435 display_parameters.display,
436 display_parameters.view,
437 display_parameters.look,
438 from_colorspace));
439 /* Gamma. */
440 if (display_parameters.exponent != 1.0f) {
441 ExponentTransformRcPtr et = ExponentTransform::Create();
442 const double value[4] = {display_parameters.exponent,
443 display_parameters.exponent,
444 display_parameters.exponent,
445 1.0};
446 et->setValue(value);
447 group->appendTransform(et);
448 }
449
450 if (display_parameters.is_image_output) {
451 adjust_for_hdr_image_file(
452 config, group, display_parameters.display, display_parameters.view);
453 }
454
455 /* Convert to extended sRGB to match the system graphics buffer. */
456 if (display_parameters.use_display_emulation) {
457 display_as_extended_srgb(config,
458 group,
459 display_parameters.display,
460 display_parameters.view,
461 display_parameters.use_hdr_buffer);
462 }
463 }
464 else {
465 /* Untonemapped case, directly to extended sRGB. */
466 group->appendTransform(create_untonemapped_ocio_display_transform(
467 config, display_parameters.display, from_colorspace, display_parameters.use_hdr_buffer));
468 }
469
470 if (display_parameters.inverse) {
471 group->setDirection(TRANSFORM_DIR_INVERSE);
472 }
473
475 std::stringstream sstream;
476 sstream << *group;
477 CLOG_TRACE(&LOG, "Creating display transform:\n%s", sstream.str().c_str());
478 }
479
480 /* Create processor from transform. This is the moment were OCIO validates the entire transform,
481 * no need to check for the validity of inputs above. */
482 ConstProcessorRcPtr p;
483 try {
484 p = ocio_config->getProcessor(group);
485 }
486 catch (Exception &exception) {
487 report_exception(exception);
488 return nullptr;
489 }
490
491 return p;
492}
493
494} // namespace blender::ocio
495
496#endif
#define UNUSED_VARS(...)
#define ELEM(...)
#define CLOG_DEBUG(clg_ref,...)
Definition CLG_log.h:191
#define CLOG_WARN(clg_ref,...)
Definition CLG_log.h:189
#define CLOG_CHECK(clg_ref, verbose_level,...)
Definition CLG_log.h:147
@ CLG_LEVEL_TRACE
Definition CLG_log.h:64
#define CLOG_TRACE(clg_ref,...)
Definition CLG_log.h:192
static AppView * view
constexpr const char * c_str() const
constexpr const char * c_str() const
virtual StringRefNull name() const =0
constexpr T clamp(T, U, U) RET
#define LOG(level)
Definition log.h:97
BLI_INLINE ColorSceneLinear4f< eAlpha::Straight > to_scene_linear(const ColorTheme4f &theme4f)
Definition BLI_color.hh:126
float3x3 scene_linear_to_rec709
MatBase< T, NumCol, NumRow > transpose(const MatBase< T, NumRow, NumCol > &mat)
CartesianBasis invert(const CartesianBasis &basis)
float3x3 calculate_white_point_matrix(const Config &config, const float temperature, const float tint)
static const double4x4 OCIO_XYZ_TO_P3
static const double4x4 OCIO_XYZ_TO_REC2020
static const double4x4 OCIO_XYZ_TO_REC709
MatBase< float, 3, 3 > float3x3
MatBase< double, 4, 4 > double4x4