diff --git a/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst b/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst index c4cba43d7b..9ca0501a23 100644 --- a/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst +++ b/docs/api/python/frozen/pyopencolorio_fixedfunctionstyle.rst @@ -36,6 +36,8 @@ FIXED_FUNCTION_ACES_GAMUT_COMP_13 : ACES 1.3 Parametric Gamut Compression (expects ACEScg values) + FIXED_FUNCTION_PQ_TO_LINEAR : SMPTE ST 2084:2014 EOTF Linearization Equation + .. py:method:: name() -> str :property: @@ -104,6 +106,11 @@ :value: + .. py:attribute:: FixedFunctionStyle.FIXED_FUNCTION_PQ_TO_LINEAR + :module: PyOpenColorIO + :value: + + .. py:property:: FixedFunctionStyle.value :module: PyOpenColorIO diff --git a/include/OpenColorIO/OpenColorTypes.h b/include/OpenColorIO/OpenColorTypes.h index 33654baf83..92da5656be 100644 --- a/include/OpenColorIO/OpenColorTypes.h +++ b/include/OpenColorIO/OpenColorTypes.h @@ -486,7 +486,8 @@ enum FixedFunctionStyle FIXED_FUNCTION_XYZ_TO_LUV, ///< CIE XYZ to 1976 CIELUV colour space (D65 white) FIXED_FUNCTION_ACES_GAMUTMAP_02, ///< ACES 0.2 Gamut clamping algorithm -- NOT IMPLEMENTED YET FIXED_FUNCTION_ACES_GAMUTMAP_07, ///< ACES 0.7 Gamut clamping algorithm -- NOT IMPLEMENTED YET - FIXED_FUNCTION_ACES_GAMUT_COMP_13 ///< ACES 1.3 Parametric Gamut Compression (expects ACEScg values) + FIXED_FUNCTION_ACES_GAMUT_COMP_13, ///< ACES 1.3 Parametric Gamut Compression (expects ACEScg values) + FIXED_FUNCTION_PQ_TO_LINEAR, ///< SMPTE ST-2084 EOTF linearization, scaled with 100 nits at 1.0, and with negative values mirrored }; /// Enumeration of the :cpp:class:`ExposureContrastTransform` transform algorithms. diff --git a/src/OpenColorIO/Config.cpp b/src/OpenColorIO/Config.cpp index 3d2a863986..b7dcb702ba 100644 --- a/src/OpenColorIO/Config.cpp +++ b/src/OpenColorIO/Config.cpp @@ -5300,17 +5300,27 @@ void Config::Impl::checkVersionConsistency(ConstTransformRcPtr & transform) cons } else if (ConstFixedFunctionTransformRcPtr ff = DynamicPtrCast(transform)) { + auto ffstyle = ff->getStyle(); if (m_majorVersion < 2) { throw Exception("Only config version 2 (or higher) can have " "FixedFunctionTransform."); } - if (m_majorVersion == 2 && m_minorVersion < 1 && ff->getStyle() == FIXED_FUNCTION_ACES_GAMUT_COMP_13) + if (m_majorVersion == 2 && m_minorVersion < 1 && ffstyle == FIXED_FUNCTION_ACES_GAMUT_COMP_13) { throw Exception("Only config version 2.1 (or higher) can have " "FixedFunctionTransform style 'ACES_GAMUT_COMP_13'."); } + + if (m_majorVersion == 2 && m_minorVersion < 4 ) + { + if(ffstyle == FIXED_FUNCTION_PQ_TO_LINEAR) + { + throw Exception("Only config version 2.4 (or higher) can have " + "FixedFunctionTransform style 'PQ_TO_LINEAR'."); + } + } } else if (DynamicPtrCast(transform)) { diff --git a/src/OpenColorIO/ParseUtils.cpp b/src/OpenColorIO/ParseUtils.cpp index eaeefc00e4..3c8b24db72 100644 --- a/src/OpenColorIO/ParseUtils.cpp +++ b/src/OpenColorIO/ParseUtils.cpp @@ -364,6 +364,7 @@ const char * FixedFunctionStyleToString(FixedFunctionStyle style) case FIXED_FUNCTION_XYZ_TO_xyY: return "XYZ_TO_xyY"; case FIXED_FUNCTION_XYZ_TO_uvY: return "XYZ_TO_uvY"; case FIXED_FUNCTION_XYZ_TO_LUV: return "XYZ_TO_LUV"; + case FIXED_FUNCTION_PQ_TO_LINEAR: return "PQ_TO_LINEAR"; case FIXED_FUNCTION_ACES_GAMUTMAP_02: case FIXED_FUNCTION_ACES_GAMUTMAP_07: throw Exception("Unimplemented fixed function types: " @@ -391,6 +392,7 @@ FixedFunctionStyle FixedFunctionStyleFromString(const char * style) else if(str == "xyz_to_xyy") return FIXED_FUNCTION_XYZ_TO_xyY; else if(str == "xyz_to_uvy") return FIXED_FUNCTION_XYZ_TO_uvY; else if(str == "xyz_to_luv") return FIXED_FUNCTION_XYZ_TO_LUV; + else if(str == "pq_to_linear") return FIXED_FUNCTION_PQ_TO_LINEAR; // Default style is meaningless. std::stringstream ss; diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOp.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOp.cpp index ac672e1190..4f9569ef1c 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOp.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOp.cpp @@ -119,10 +119,10 @@ std::string FixedFunctionOp::getCacheID() const return cacheIDStream.str(); } -ConstOpCPURcPtr FixedFunctionOp::getCPUOp(bool /*fastLogExpPow*/) const +ConstOpCPURcPtr FixedFunctionOp::getCPUOp(bool fastLogExpPow) const { ConstFixedFunctionOpDataRcPtr data = fnData(); - return GetFixedFunctionCPURenderer(data); + return GetFixedFunctionCPURenderer(data, fastLogExpPow); } void FixedFunctionOp::extractGpuShaderInfo(GpuShaderCreatorRcPtr & shaderCreator) const diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp index a974cb7880..12c9003b9b 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.cpp @@ -9,6 +9,8 @@ #include "BitDepthUtils.h" #include "MathUtils.h" #include "ops/fixedfunction/FixedFunctionOpCPU.h" +#include "SSE.h" +#include "CPUInfo.h" namespace OCIO_NAMESPACE @@ -210,6 +212,46 @@ class Renderer_LUV_TO_XYZ : public OpCPU void apply(const void * inImg, void * outImg, long numPixels) const override; }; +template +class Renderer_PQ_TO_LINEAR : public OpCPU { + public: + Renderer_PQ_TO_LINEAR() = delete; + explicit Renderer_PQ_TO_LINEAR(ConstFixedFunctionOpDataRcPtr &data); + + void apply(const void *inImg, void *outImg, long numPixels) const override; +}; + +template +class Renderer_LINEAR_TO_PQ : public OpCPU { + public: + Renderer_LINEAR_TO_PQ() = delete; + explicit Renderer_LINEAR_TO_PQ(ConstFixedFunctionOpDataRcPtr &data); + + void apply(const void *inImg, void *outImg, long numPixels) const override; +}; + +#if OCIO_USE_SSE2 +template +class Renderer_PQ_TO_LINEAR_SSE : public OpCPU { +public: + Renderer_PQ_TO_LINEAR_SSE() = delete; + explicit Renderer_PQ_TO_LINEAR_SSE(ConstFixedFunctionOpDataRcPtr& data); + + static inline __m128 myPower(__m128 x, __m128 exp); + void apply(const void* inImg, void* outImg, long numPixels) const override; +}; + + +template +class Renderer_LINEAR_TO_PQ_SSE : public OpCPU { +public: + Renderer_LINEAR_TO_PQ_SSE() = delete; + explicit Renderer_LINEAR_TO_PQ_SSE(ConstFixedFunctionOpDataRcPtr& data); + + static inline __m128 myPower(__m128 x, __m128 exp); + void apply(const void* inImg, void* outImg, long numPixels) const override; +}; +#endif /////////////////////////////////////////////////////////////////////////////// @@ -1177,15 +1219,217 @@ void Renderer_LUV_TO_XYZ::apply(const void * inImg, void * outImg, long numPixel } } +namespace +{ +namespace ST_2084 +{ + static constexpr double m1 = 0.25 * 2610. / 4096.; + static constexpr double m2 = 128. * 2523. / 4096.; + static constexpr double c2 = 32. * 2413. / 4096.; + static constexpr double c3 = 32. * 2392. / 4096.; + static constexpr double c1 = c3 - c2 + 1.; + +#if OCIO_USE_SSE2 + const __m128 abs_rgb_mask = _mm_castsi128_ps(_mm_setr_epi32(0x7fffffff, 0x7fffffff, 0x7fffffff, 0)); + const __m128 vm1 = _mm_set1_ps(m1); + const __m128 vm2 = _mm_set1_ps(m2); + const __m128 vm1_inv = _mm_set1_ps(1.0f / float(m1)); + const __m128 vm2_inv = _mm_set1_ps(1.0f / float(m2)); + const __m128 vc1 = _mm_set1_ps(c1); + const __m128 vc2 = _mm_set1_ps(c2); + const __m128 vc3 = _mm_set1_ps(c3); +#endif +} // ST_2084 +} // anonymous + +template +Renderer_PQ_TO_LINEAR::Renderer_PQ_TO_LINEAR(ConstFixedFunctionOpDataRcPtr & /*data*/) + : OpCPU() +{ +} + +template +void Renderer_PQ_TO_LINEAR::apply(const void *inImg, void *outImg, long numPixels) const +{ + using namespace ST_2084; + const float *in = (const float *)inImg; + float *out = (float *)outImg; + + for (long idx = 0; idx < numPixels; ++idx) + { + // RGB + for (int ch = 0; ch < 3; ++ch) + { + float v = *(in++); + const T vabs = std::abs(T(v)); + const T x = std::pow(vabs, T(1.) / T(m2)); + const T nits = std::pow(std::max(T(0), x - T(c1)) / (T(c2) - T(c3) * x), T(1.) / T(m1)); + // output scale is 1.0 = 10000 nits, we map it to make 1.0 = 100 nits. + *(out++) = std::copysign(float(T(100.0) * nits), v); + } + + // Alpha + *(out++) = *(in++); + } +} + +template +Renderer_LINEAR_TO_PQ::Renderer_LINEAR_TO_PQ(ConstFixedFunctionOpDataRcPtr& /*data*/) + : OpCPU() +{ +} + +template +void Renderer_LINEAR_TO_PQ::apply(const void* inImg, void* outImg, long numPixels) const +{ + using namespace ST_2084; + const float* in = (const float*)inImg; + float* out = (float*)outImg; + + for (long idx = 0; idx < numPixels; ++idx) + { + // RGB + for (int ch = 0; ch < 3; ++ch) + { + float v = *(in++); + // Input is in nits/100, convert to [0,1], where 1 is 10000 nits. + const T L = std::abs(v * T(0.01)); + const T y = std::pow(L, T(m1)); + const T ratpoly = (T(c1) + T(c2) * y) / (T(1.) + T(c3) * y); + const T N = std::pow(ratpoly, T(m2)); + *(out++) = std::copysign(float(N), v); + } + // Alpha + *(out++) = *(in++); + }; +} + +#if OCIO_USE_SSE2 +template +Renderer_PQ_TO_LINEAR_SSE::Renderer_PQ_TO_LINEAR_SSE(ConstFixedFunctionOpDataRcPtr& /*data*/) + : OpCPU() +{ +} + +// all platforms support ssePower() +template<> +__m128 Renderer_PQ_TO_LINEAR_SSE::myPower(__m128 x, __m128 exp) +{ + return ssePower(x, exp); +} + +#ifdef _WIN32 +// Only Windows compilers have built-in _mm_pow_ps() SVML intrinsic +// implementation, so non-fast SIMD version is available only on Windows for +// now. +template<> +__m128 Renderer_PQ_TO_LINEAR_SSE::myPower(__m128 x, __m128 exp) +{ + return _mm_pow_ps(x, exp); +} +#endif // _WIN32 + + +template +void Renderer_PQ_TO_LINEAR_SSE::apply(const void* inImg, void* outImg, long numPixels) const +{ + using namespace ST_2084; + const float* in = (const float*)inImg; + float* out = (float*)outImg; + + for (long idx = 0; idx < numPixels; ++idx, in+=4, out+=4) + { + // load + __m128 v = _mm_loadu_ps(in); + + // compute R, G and B channels + __m128 vabs = _mm_and_ps(abs_rgb_mask, v); // Clear sign bits of RGB and all bits of Alpha + __m128 x = myPower(vabs, vm2_inv); + __m128 nom = _mm_max_ps(_mm_setzero_ps(), _mm_sub_ps(x, vc1)); + __m128 denom = _mm_sub_ps(vc2, _mm_mul_ps(vc3, x)); + + // output scale is 1.0 = 10000 nits, we map it to make 1.0 = 100 nits. + __m128 nits100; + nits100 = _mm_mul_ps(_mm_set1_ps(100.0f), myPower(_mm_div_ps(nom, denom), vm1_inv)); + + // Restore the sign bits and Alpha channel. + // TODO: this can be further optimized by using separate SSE constants for alpha channel + __m128 nits100_signed = _mm_or_ps(_mm_and_ps(abs_rgb_mask, nits100), _mm_andnot_ps(abs_rgb_mask, v)); + + // store + _mm_storeu_ps(out, nits100_signed); + } +} + +template +Renderer_LINEAR_TO_PQ_SSE::Renderer_LINEAR_TO_PQ_SSE(ConstFixedFunctionOpDataRcPtr& /*data*/) + : OpCPU() +{ +} + +// all platforms support ssePower() +template<> +__m128 Renderer_LINEAR_TO_PQ_SSE::myPower(__m128 x, __m128 exp) +{ + return ssePower(x, exp); +} + +#ifdef _WIN32 +// Only Windows compilers have built-in _mm_pow_ps() SVML intrinsic +// implementation, so non-fast SIMD version is available only on Windows for +// now. +template<> +__m128 Renderer_LINEAR_TO_PQ_SSE::myPower(__m128 x, __m128 exp) +{ + return _mm_pow_ps(x, exp); +} +#endif // _WIN32 + + +template +void Renderer_LINEAR_TO_PQ_SSE::apply(const void* inImg, void* outImg, long numPixels) const +{ + using namespace ST_2084; + const float* in = (const float*)inImg; + float* out = (float*)outImg; + + for (long idx = 0; idx < numPixels; ++idx, in += 4, out += 4) + { + // load + __m128 v = _mm_loadu_ps(in); + + // Clear sign bits of RGB and all bits of Alpha + __m128 vabs = _mm_and_ps(abs_rgb_mask, v); + // Input is in nits/100, convert to [0,1], where 1 is 10000 nits. + __m128 L = _mm_mul_ps(_mm_set1_ps(0.01f), vabs); + __m128 y = myPower(L, vm1); + __m128 ratpoly = _mm_div_ps( + _mm_add_ps(vc1, _mm_mul_ps (vc2, y)), + _mm_add_ps(_mm_set1_ps(1.0f), _mm_mul_ps(vc3, y))); + __m128 N = myPower(ratpoly, vm2); + + // restore sign bits and the alpha channel + // TODO: this can be further optimized by using separate SSE constants for alpha channel + __m128 N_signed = _mm_or_ps(_mm_and_ps(abs_rgb_mask, N), _mm_andnot_ps(abs_rgb_mask, v)); + + // store + _mm_storeu_ps(out, N_signed); + } +} +#endif //OCIO_USE_SSE2 /////////////////////////////////////////////////////////////////////////////// -ConstOpCPURcPtr GetFixedFunctionCPURenderer(ConstFixedFunctionOpDataRcPtr & func) +ConstOpCPURcPtr GetFixedFunctionCPURenderer(ConstFixedFunctionOpDataRcPtr & func, bool fastLogExpPow) { + // prevent "unused-parameter" warning/error in case the using code is + // ifdef'ed out. + (void)fastLogExpPow; + switch(func->getStyle()) { case FixedFunctionOpData::ACES_RED_MOD_03_FWD: @@ -1278,6 +1522,38 @@ ConstOpCPURcPtr GetFixedFunctionCPURenderer(ConstFixedFunctionOpDataRcPtr & func { return std::make_shared(func); } + case FixedFunctionOpData::PQ_TO_LINEAR: + { +#if OCIO_USE_SSE2 + if (fastLogExpPow) + { + return std::make_shared>(func); + } +#ifdef _WIN32 + // On Windows we can use _mm_pow_ps() SVML "sequential" + // intrinsic which is slower than our ssePower but precise. This + // will still be faster than scalar implementation. + return std::make_shared>(func); +#endif // _WIN32 +#endif // OCIO_USE_SSE2 + return std::make_shared>(func); + } + case FixedFunctionOpData::LINEAR_TO_PQ: + { +#if OCIO_USE_SSE2 + if (fastLogExpPow) + { + return std::make_shared>(func); + } +#ifdef _WIN32 + // On Windows we can use _mm_pow_ps() SVML "sequential" + // intrinsic which is slower than our ssePower but precise. This + // will still be faster than scalar implementation. + return std::make_shared>(func); +#endif // _WIN32 +#endif // OCIO_USE_SSE2 + return std::make_shared>(func); + } } throw Exception("Unsupported FixedFunction style"); diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.h b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.h index f460774ffe..f2946eda42 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.h +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpCPU.h @@ -14,7 +14,7 @@ namespace OCIO_NAMESPACE { -ConstOpCPURcPtr GetFixedFunctionCPURenderer(ConstFixedFunctionOpDataRcPtr & func); +ConstOpCPURcPtr GetFixedFunctionCPURenderer(ConstFixedFunctionOpDataRcPtr & func, bool fastLogExpPow); } // namespace OCIO_NAMESPACE diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp index ce835fd6a8..8af703c028 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.cpp @@ -39,6 +39,8 @@ constexpr char XYZ_TO_uvY_STR[] = "XYZ_TO_uvY"; constexpr char uvY_TO_XYZ_STR[] = "uvY_TO_XYZ"; constexpr char XYZ_TO_LUV_STR[] = "XYZ_TO_LUV"; constexpr char LUV_TO_XYZ_STR[] = "LUV_TO_XYZ"; +constexpr char PQ_TO_LINEAR_STR[] = "PQ_TO_LINEAR"; +constexpr char LINEAR_TO_PQ_STR[] = "LINEAR_TO_PQ"; // NOTE: Converts the enumeration value to its string representation (i.e. CLF reader). @@ -94,6 +96,10 @@ const char * FixedFunctionOpData::ConvertStyleToString(Style style, bool detaile return XYZ_TO_LUV_STR; case LUV_TO_XYZ: return LUV_TO_XYZ_STR; + case PQ_TO_LINEAR: + return PQ_TO_LINEAR_STR; + case LINEAR_TO_PQ: + return LINEAR_TO_PQ_STR; } std::stringstream ss("Unknown FixedFunction style: "); @@ -196,6 +202,14 @@ FixedFunctionOpData::Style FixedFunctionOpData::GetStyle(const char * name) { return LUV_TO_XYZ; } + else if (0 == Platform::Strcasecmp(name, PQ_TO_LINEAR_STR)) + { + return PQ_TO_LINEAR; + } + else if (0 == Platform::Strcasecmp(name, LINEAR_TO_PQ_STR)) + { + return LINEAR_TO_PQ; + } } std::string st("Unknown FixedFunction style: "); @@ -270,6 +284,11 @@ FixedFunctionOpData::Style FixedFunctionOpData::ConvertStyle(FixedFunctionStyle "FIXED_FUNCTION_ACES_GAMUTMAP_02, " "FIXED_FUNCTION_ACES_GAMUTMAP_07."); } + case FIXED_FUNCTION_PQ_TO_LINEAR: + { + return isForward ? FixedFunctionOpData::PQ_TO_LINEAR : + FixedFunctionOpData::LINEAR_TO_PQ; + } } std::stringstream ss("Unknown FixedFunction transform style: "); @@ -326,6 +345,10 @@ FixedFunctionStyle FixedFunctionOpData::ConvertStyle(FixedFunctionOpData::Style case FixedFunctionOpData::XYZ_TO_LUV: case FixedFunctionOpData::LUV_TO_XYZ: return FIXED_FUNCTION_XYZ_TO_LUV; + + case FixedFunctionOpData::PQ_TO_LINEAR: + case FixedFunctionOpData::LINEAR_TO_PQ: + return FIXED_FUNCTION_PQ_TO_LINEAR; } std::stringstream ss("Unknown FixedFunction style: "); @@ -584,6 +607,17 @@ void FixedFunctionOpData::invert() noexcept setStyle(XYZ_TO_LUV); break; } + + case PQ_TO_LINEAR: + { + setStyle(LINEAR_TO_PQ); + break; + } + case LINEAR_TO_PQ: + { + setStyle(PQ_TO_LINEAR); + break; + } } // Note that any existing metadata could become stale at this point but @@ -614,6 +648,7 @@ TransformDirection FixedFunctionOpData::getDirection() const noexcept case FixedFunctionOpData::XYZ_TO_xyY: case FixedFunctionOpData::XYZ_TO_uvY: case FixedFunctionOpData::XYZ_TO_LUV: + case FixedFunctionOpData::PQ_TO_LINEAR: return TRANSFORM_DIR_FORWARD; case FixedFunctionOpData::ACES_RED_MOD_03_INV: @@ -627,6 +662,7 @@ TransformDirection FixedFunctionOpData::getDirection() const noexcept case FixedFunctionOpData::xyY_TO_XYZ: case FixedFunctionOpData::uvY_TO_XYZ: case FixedFunctionOpData::LUV_TO_XYZ: + case FixedFunctionOpData::LINEAR_TO_PQ: return TRANSFORM_DIR_INVERSE; } return TRANSFORM_DIR_FORWARD; diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h index a640c66161..df9d949941 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpData.h @@ -47,7 +47,9 @@ class FixedFunctionOpData : public OpData XYZ_TO_uvY, // CIE XYZ to 1976 u'v' chromaticity coordinates uvY_TO_XYZ, // Inverse of above XYZ_TO_LUV, // CIE XYZ to 1976 CIELUV colour space (D65 white) - LUV_TO_XYZ // Inverse of above + LUV_TO_XYZ, // Inverse of above + PQ_TO_LINEAR, // Perceptual Quantizer curve to linear + LINEAR_TO_PQ, // Inverse of above }; static const char * ConvertStyleToString(Style style, bool detailed); diff --git a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp index 20c1b1b1b4..c96f6c6d4c 100644 --- a/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp +++ b/src/OpenColorIO/ops/fixedfunction/FixedFunctionOpGPU.cpp @@ -523,6 +523,43 @@ void Add_LUV_TO_XYZ(GpuShaderCreatorRcPtr & shaderCreator, GpuShaderText & ss) ss.newLine() << pxl << ".rgb.g = Y;"; } + +namespace +{ + namespace ST_2084 + { + static constexpr double m1 = 0.25 * 2610. / 4096.; + static constexpr double m2 = 128. * 2523. / 4096.; + static constexpr double c2 = 32. * 2413. / 4096.; + static constexpr double c3 = 32. * 2392. / 4096.; + static constexpr double c1 = c3 - c2 + 1.; + } +} // anonymous + +void Add_PQ_TO_LINEAR(GpuShaderCreatorRcPtr& shaderCreator, GpuShaderText& ss) +{ + using namespace ST_2084; + const std::string pxl(shaderCreator->getPixelName()); + + ss.newLine() << ss.float3Decl("sign3") << " = sign(" << pxl << ".rgb);"; + ss.newLine() << ss.float3Decl("x") << " = pow(abs(" << pxl << ".rgb), " << ss.float3Const(1.0 / m2) << ");"; + ss.newLine() << pxl << ".rgb = 100. * sign3 * pow(max(" << ss.float3Const(0.0) << ", x - " << ss.float3Const(c1) << ") / (" + << ss.float3Const(c2) << " - " << c3 << " * x), " << ss.float3Const(1.0 / m1) << ");"; +} + +void Add_LINEAR_TO_PQ(GpuShaderCreatorRcPtr& shaderCreator, GpuShaderText& ss) +{ + using namespace ST_2084; + const std::string pxl(shaderCreator->getPixelName()); + + ss.newLine() << ss.float3Decl("sign3") << " = sign(" << pxl << ".rgb);"; + ss.newLine() << ss.float3Decl("L") << " = abs(0.01 * " << pxl << ".rgb);"; + ss.newLine() << ss.float3Decl("y") << " = pow(L, " << ss.float3Const(m1) << ");"; + ss.newLine() << ss.float3Decl("ratpoly") << " = (" << ss.float3Const(c1) << " + " << c2 << " * y) / (" + << ss.float3Const(1.0) << " + " << c3 << " * y);"; + ss.newLine() << pxl << ".rgb = sign3 * pow(ratpoly, " << ss.float3Const(m2) << ");"; +} + void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, ConstFixedFunctionOpDataRcPtr & func) { @@ -670,6 +707,17 @@ void GetFixedFunctionGPUShaderProgram(GpuShaderCreatorRcPtr & shaderCreator, case FixedFunctionOpData::LUV_TO_XYZ: { Add_LUV_TO_XYZ(shaderCreator, ss); + break; + } + case FixedFunctionOpData::PQ_TO_LINEAR: + { + Add_PQ_TO_LINEAR(shaderCreator, ss); + break; + } + case FixedFunctionOpData::LINEAR_TO_PQ: + { + Add_LINEAR_TO_PQ(shaderCreator, ss); + break; } } diff --git a/src/OpenColorIO/transforms/builtins/Displays.cpp b/src/OpenColorIO/transforms/builtins/Displays.cpp index 8d8af97826..6da0107302 100644 --- a/src/OpenColorIO/transforms/builtins/Displays.cpp +++ b/src/OpenColorIO/transforms/builtins/Displays.cpp @@ -15,6 +15,10 @@ #include "transforms/builtins/Displays.h" #include "transforms/builtins/OpHelpers.h" +// This is a preparation for OCIO-lite where LUT support may be turned off. +#ifndef OCIO_LUT_SUPPORT +# define OCIO_LUT_SUPPORT 1 +#endif namespace OCIO_NAMESPACE { @@ -31,40 +35,80 @@ static constexpr double c2 = 32. * 2413. / 4096.; static constexpr double c3 = 32. * 2392. / 4096.; static constexpr double c1 = c3 - c2 + 1.; -void GeneratePQToLinearOps(OpRcPtrVec & ops) +void GeneratePQToLinearOps(OpRcPtrVec& ops) { +#if OCIO_LUT_SUPPORT auto GenerateLutValues = [](double input) -> float - { - const double N = std::max(0., input); - const double x = std::pow(N, 1. / m2); - double L = std::pow( std::max(0., x - c1) / (c2 - c3 * x), 1. / m1 ); - // L is in nits/10000, convert to nits/100. - L *= 100.; + { + const double N = std::abs(input); + const double x = std::pow(N, 1. / m2); + double L = std::pow(std::max(0., x - c1) / (c2 - c3 * x), 1. / m1); + // L is in nits/10000, convert to nits/100. + L *= 100.; - return float(L); - }; + return float(std::copysign(L, input)); + }; CreateLut(ops, 4096, GenerateLutValues); +#else + CreateFixedFunctionOp(ops, FixedFunctionOpData::PQ_TO_LINEAR, {}); +#endif } -void GenerateLinearToPQOps(OpRcPtrVec & ops) +void GenerateLinearToPQOps(OpRcPtrVec& ops) { +#if OCIO_LUT_SUPPORT auto GenerateLutValues = [](double input) -> float - { - // Input is in nits/100, convert to [0,1], where 1 is 10000 nits. - const double L = std::max(0., input * 0.01); - const double y = std::pow(L, m1); - const double ratpoly = (c1 + c2 * y) / (1. + c3 * y); - const double N = std::pow( std::max(0., ratpoly), m2 ); + { + // Input is in nits/100, convert to [0,1], where 1 is 10000 nits. + const double L = std::abs(input * 0.01); + const double y = std::pow(L, m1); + const double ratpoly = (c1 + c2 * y) / (1. + c3 * y); + const double N = std::pow(std::max(0., ratpoly), m2); - return float(N); - }; + return float(std::copysign(N, input)); + }; CreateHalfLut(ops, GenerateLutValues); +#else + CreateFixedFunctionOp(ops, FixedFunctionOpData::LINEAR_TO_PQ, {}); +#endif } } // ST_2084 +namespace BT_2100 +{ +static constexpr double Lw = 1000.; +static constexpr double E_MAX = 3.; + +static constexpr double a = 0.17883277; +static constexpr double b = (1. - 4. * a) * E_MAX / 12.; +static const double c0 = 0.5 - a * std::log(4. * a); +static const double c = std::log(12. / E_MAX) * 0.17883277 + c0; +static constexpr double E_scale = 3. / E_MAX; +static constexpr double E_break = E_MAX / 12.; + +void GenerateLinearToHLGOps(OpRcPtrVec& ops) +{ + auto GenerateLutValues = [](double in) -> float + { + double out = 0.0; + const double E = std::abs(in); + if (in < E_break) + { + out = std::sqrt(E * E_scale); + } + else + { + out = a * std::log(E - b) + c; + } + return float(std::copysign(out, in)); + }; + + CreateHalfLut(ops, GenerateLutValues); +} +} // BT_2100 void RegisterAll(BuiltinTransformRegistryImpl & registry) noexcept { @@ -292,45 +336,21 @@ void RegisterAll(BuiltinTransformRegistryImpl & registry) noexcept = build_conversion_matrix_from_XYZ_D65(REC2020::primaries, ADAPTATION_NONE); CreateMatrixOp(ops, matrix, TRANSFORM_DIR_FORWARD); - static constexpr double Lw = 1000.; - static constexpr double E_MAX = 3.; - const double gamma = 1.2 + 0.42 * std::log10(Lw / 1000.); + const double gamma = 1.2 + 0.42 * std::log10(BT_2100::Lw / 1000.); { static constexpr double scale = 100.; static constexpr double scale4[4] = { scale, scale, scale, 1. }; CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD); } { - const double scale = std::pow(E_MAX, gamma) / Lw; + const double scale = std::pow(BT_2100::E_MAX, gamma) / BT_2100::Lw; const double scale4[4] = { scale, scale, scale, 1. }; CreateScaleOp(ops, scale4, TRANSFORM_DIR_FORWARD); } CreateFixedFunctionOp(ops, FixedFunctionOpData::REC2100_SURROUND_FWD, {1. / gamma}); - auto GenerateLutValues = [](double in) -> float - { - const double a = 0.17883277; - const double b = (1. - 4. * a) * E_MAX / 12.; - const double c0 = 0.5 - a * std::log(4. * a); - const double c = std::log(12. / E_MAX) * 0.17883277 + c0; - const double E_scale = 3. / E_MAX; - const double E_break = E_MAX / 12.; - double out = 0.0; - - const double E = std::max(in, 0.); - if (in < E_break) - { - out = std::sqrt( E * E_scale ); - } - else - { - out = std::min( 1., a * std::log(E - b) + c); - } - return float(out); - }; - - CreateHalfLut(ops, GenerateLutValues); + BT_2100::GenerateLinearToHLGOps(ops); }; registry.addBuiltin("DISPLAY - CIE-XYZ-D65_to_REC.2100-HLG-1000nit", diff --git a/src/bindings/python/PyTypes.cpp b/src/bindings/python/PyTypes.cpp index 760d715587..96f485ad05 100644 --- a/src/bindings/python/PyTypes.cpp +++ b/src/bindings/python/PyTypes.cpp @@ -583,6 +583,9 @@ void bindPyTypes(py::module & m) DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_ACES_GAMUTMAP_07)) .value("FIXED_FUNCTION_ACES_GAMUT_COMP_13", FIXED_FUNCTION_ACES_GAMUT_COMP_13, DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_ACES_GAMUT_COMP_13)) + .value("FIXED_FUNCTION_PQ_TO_LINEAR", FIXED_FUNCTION_PQ_TO_LINEAR, + DOC(PyOpenColorIO, FixedFunctionStyle, FIXED_FUNCTION_PQ_TO_LINEAR)) + .export_values(); py::enum_( diff --git a/tests/cpu/Config_tests.cpp b/tests/cpu/Config_tests.cpp index f0835eca82..0c29ecae1d 100644 --- a/tests/cpu/Config_tests.cpp +++ b/tests/cpu/Config_tests.cpp @@ -2172,6 +2172,22 @@ OCIO_ADD_TEST(Config, version_validation) namespace { +// Generic profile header generator for given version +template +const std::string PROFILE_V() +{ + std::string s = std::string("ocio_profile_version: ") + + std::to_string(Major) + std::string(".") + std::to_string(Minor) + "\n"; + + if(Major>=2) + { + s = s + "\n" + "environment:\n" + " {}\n"; + } + + return s; +} const std::string PROFILE_V1 = "ocio_profile_version: 1\n" @@ -2284,6 +2300,19 @@ const std::string PROFILE_V2_START = PROFILE_V2 + SIMPLE_PROFILE_A + const std::string PROFILE_V21_START = PROFILE_V21 + SIMPLE_PROFILE_A + DEFAULT_RULES + SIMPLE_PROFILE_B_V2; + +// Generic simple profile prolog for given major,minor version. +template +const std::string PROFILE_START_V() +{ + if(Major<=1) + { + return PROFILE_V() + SIMPLE_PROFILE_A + SIMPLE_PROFILE_B_V1; + } + + return PROFILE_V() + SIMPLE_PROFILE_A + DEFAULT_RULES + SIMPLE_PROFILE_B_V2; +} + } OCIO_ADD_TEST(Config, serialize_colorspace_displayview_transforms) @@ -4922,6 +4951,32 @@ OCIO_ADD_TEST(Config, fixed_function_serialization) OCIO_CHECK_THROW_WHAT(config = OCIO::Config::CreateFromStream(is), OCIO::Exception, "'FixedFunctionTransform' parsing failed: style value is missing."); } + + { + const std::string strEnd = + " from_scene_reference: !\n" + " children:\n" + " - ! {style: PQ_TO_LINEAR}\n"; + + { + const std::string str = PROFILE_START_V<2, 3>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO_CHECK_THROW_WHAT(OCIO::Config::CreateFromStream(is), OCIO::Exception, + "Only config version 2.4 (or higher) can have FixedFunctionTransform style 'PQ_TO_LINEAR'."); + } + + { + const std::string str = PROFILE_START_V<2, 4>() + strEnd; + + std::istringstream is; + is.str(str); + + OCIO_CHECK_NO_THROW(OCIO::Config::CreateFromStream(is)); + } + } } OCIO_ADD_TEST(Config, exposure_contrast_serialization) diff --git a/tests/cpu/fileformats/FileFormatCTF_tests.cpp b/tests/cpu/fileformats/FileFormatCTF_tests.cpp index fd7273e200..9ebdb8ab07 100644 --- a/tests/cpu/fileformats/FileFormatCTF_tests.cpp +++ b/tests/cpu/fileformats/FileFormatCTF_tests.cpp @@ -3838,6 +3838,8 @@ OCIO_ADD_TEST(FileFormatCTF, ff_load_save_ctf) ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::uvY_TO_XYZ , __LINE__); ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::XYZ_TO_LUV , __LINE__); ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::LUV_TO_XYZ , __LINE__); + ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::PQ_TO_LINEAR , __LINE__); + ValidateFixedFunctionStyleNoParam(OCIO::FixedFunctionOpData::LINEAR_TO_PQ , __LINE__); } OCIO_ADD_TEST(FileFormatCTF, load_ff_fail_version) diff --git a/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp b/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp index 7e61872443..73f4a6210f 100644 --- a/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp +++ b/tests/cpu/ops/fixedfunction/FixedFunctionOpCPU_tests.cpp @@ -19,10 +19,12 @@ void ApplyFixedFunction(float * input_32f, unsigned numSamples, OCIO::ConstFixedFunctionOpDataRcPtr & fnData, float errorThreshold, - int lineNo) + int lineNo, + bool fastLogExpPow = false +) { OCIO::ConstOpCPURcPtr op; - OCIO_CHECK_NO_THROW_FROM(op = OCIO::GetFixedFunctionCPURenderer(fnData), lineNo); + OCIO_CHECK_NO_THROW_FROM(op = OCIO::GetFixedFunctionCPURenderer(fnData, fastLogExpPow), lineNo); OCIO_CHECK_NO_THROW_FROM(op->apply(input_32f, input_32f, numSamples), lineNo); for(unsigned idx=0; idx<(numSamples*4); ++idx) @@ -557,3 +559,66 @@ OCIO_ADD_TEST(FixedFunctionOpCPU, XYZ_TO_LUV) img = outputFrame; ApplyFixedFunction(&img[0], &inputFrame[0], 2, dataFInv, 1e-5f, __LINE__); } + +OCIO_ADD_TEST(FixedFunctionOpCPU, PQ_TO_LINEAR) +{ + constexpr unsigned int NumPixels = 9; + const std::array inputFrame + { + -0.10f,-0.05f, 0.00f, 1.0f, // Negative Input + 0.05f, 0.10f, 0.15f, 1.0f, + 0.20f, 0.25f, 0.30f, 1.0f, + 0.35f, 0.40f, 0.45f, 0.5f, + 0.50f, 0.55f, 0.60f, 0.0f, + 0.65f, 0.70f, 0.75f, 1.0f, + 0.80f, 0.85f, 0.90f, 1.0f, + 0.95f, 1.00f, 1.05f, 1.0f, + 1.10f, 1.15f, 1.20f, 1.0f, // Over Range + }; + + const std::array outputFrame + { + -3.2456559e-03f,-6.0001636e-04f, 0.0f, 1.0f, + 6.0001636e-04f, 3.2456559e-03f, 1.0010649e-02f, 1.0f, + 2.4292633e-02f, 5.1541760e-02f, 1.0038226e-01f, 1.0f, + 1.8433567e-01f, 3.2447918e-01f, 5.5356688e-01f, 0.5f, + 9.2245709e-01f, 1.5102065e+00f, 2.4400519e+00f, 0.0f, + 3.9049474e+00f, 6.2087938e+00f, 9.8337786e+00f, 1.0f, + 1.5551784e+01f, 2.4611351e+01f, 3.9056447e+01f, 1.0f, + 6.2279535e+01f, 1.0000000e+02f, 1.6203272e+02f, 1.0f, + 2.6556253e+02f, 4.4137110e+02f, 7.4603927e+02f, 1.0f, + }; + + // Fast power enabled + { + auto img = inputFrame; + + OCIO::ConstFixedFunctionOpDataRcPtr dataFwd + = std::make_shared(OCIO::FixedFunctionOpData::PQ_TO_LINEAR); + + ApplyFixedFunction(img.data(), outputFrame.data(), NumPixels, dataFwd, 2.5e-3f, __LINE__, true); + + OCIO::ConstFixedFunctionOpDataRcPtr dataFInv + = std::make_shared(OCIO::FixedFunctionOpData::LINEAR_TO_PQ); + + img = outputFrame; + ApplyFixedFunction(&img[0], &inputFrame[0], NumPixels, dataFInv, 1e-3f, __LINE__, true); + } + + // Fast power disabled + { + auto img = inputFrame; + + OCIO::ConstFixedFunctionOpDataRcPtr dataFwd + = std::make_shared(OCIO::FixedFunctionOpData::PQ_TO_LINEAR); + + ApplyFixedFunction(img.data(), outputFrame.data(), NumPixels, dataFwd, 5e-5f, __LINE__, false); + + OCIO::ConstFixedFunctionOpDataRcPtr dataFInv + = std::make_shared(OCIO::FixedFunctionOpData::LINEAR_TO_PQ); + + img = outputFrame; + ApplyFixedFunction(&img[0], &inputFrame[0], NumPixels, dataFInv, 1e-5f, __LINE__, false); + } + +} diff --git a/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp b/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp index 1331be6f6c..ac25e64b12 100644 --- a/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp +++ b/tests/cpu/ops/fixedfunction/FixedFunctionOp_tests.cpp @@ -379,3 +379,29 @@ OCIO_ADD_TEST(FixedFunctionOps, XYZ_TO_LUV) const std::string typeName(typeid(c).name()); OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "Renderer_XYZ_TO_LUV")); } + +OCIO_ADD_TEST(FixedFunctionOps, PQ_TO_LINEAR) +{ + OCIO::OpRcPtrVec ops; + + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::PQ_TO_LINEAR, {})); + OCIO_CHECK_NO_THROW(OCIO::CreateFixedFunctionOp(ops, OCIO::FixedFunctionOpData::LINEAR_TO_PQ, {})); + + OCIO_CHECK_NO_THROW(ops.finalize()); + OCIO_REQUIRE_EQUAL(ops.size(), 2); + + OCIO::ConstOpRcPtr op0 = ops[0]; + OCIO::ConstOpRcPtr op1 = ops[1]; + + OCIO_CHECK_ASSERT(!op0->isIdentity()); + OCIO_CHECK_ASSERT(!op1->isIdentity()); + + OCIO_CHECK_ASSERT(op0->isSameType(op1)); + OCIO_CHECK_ASSERT(op0->isInverse(op1)); + OCIO_CHECK_ASSERT(op1->isInverse(op0)); + + OCIO::ConstOpCPURcPtr cpuOp = op0->getCPUOp(false); + const OCIO::OpCPU& c = *cpuOp; + const std::string typeName(typeid(c).name()); + OCIO_CHECK_NE(std::string::npos, StringUtils::Find(typeName, "Renderer_PQ_TO_LINEAR")); +} diff --git a/tests/gpu/FixedFunctionOp_test.cpp b/tests/gpu/FixedFunctionOp_test.cpp index 2ed4bdc1a3..b614a08f87 100644 --- a/tests/gpu/FixedFunctionOp_test.cpp +++ b/tests/gpu/FixedFunctionOp_test.cpp @@ -505,3 +505,37 @@ OCIO_ADD_GPU_TEST(FixedFunction, style_XYZ_TO_LUV_inv) test.setErrorThreshold(1e-5f); } + +OCIO_ADD_GPU_TEST(FixedFunction, style_PQ_TO_LINEAR_fwd) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_PQ_TO_LINEAR); + func->setDirection(OCIO::TRANSFORM_DIR_FORWARD); + + // Picking a tight epsilon is tricky with this function due to nested power + // operations and [0,100] output range for [0,1] input range. + + // MaxDiff in range [-0.1, 1.1] against... + // scalar double precision : 0.000094506 + // scalar single precision : 0.000144501 + // SSE2 (intrinsic pow) : 0.000144441 + // SSE2 (fastPower) : 0.002207260 + test.setWideRangeInterval(-0.1f, 1.1f); + test.setProcessor(func); + test.setRelativeComparison(true); // Since the output range will be 0..100, we set the relative epsilon. + test.setErrorThreshold(OCIO_USE_SSE2 ? 0.0023f : 1.5e-4f); +} + +OCIO_ADD_GPU_TEST(FixedFunction, style_PQ_TO_LINEAR_inv) +{ + OCIO::FixedFunctionTransformRcPtr func = + OCIO::FixedFunctionTransform::Create(OCIO::FIXED_FUNCTION_PQ_TO_LINEAR); + func->setDirection(OCIO::TRANSFORM_DIR_INVERSE); + + test.setWideRangeInterval(-0.1f, 100.1f); + test.setProcessor(func); + + // using large threshold for SSE2 as that will enable usage of fast but + // approximate power function ssePower. + test.setErrorThreshold(OCIO_USE_SSE2 ? 0.0008f : 2e-5f); +} diff --git a/tests/gpu/GPUUnitTest.cpp b/tests/gpu/GPUUnitTest.cpp index c6c1bd0715..07b727f034 100644 --- a/tests/gpu/GPUUnitTest.cpp +++ b/tests/gpu/GPUUnitTest.cpp @@ -224,7 +224,6 @@ namespace { // It means to generate the input values. - const bool testWideRange = test->getTestWideRange(); #if __APPLE__ && __aarch64__ // The Apple M1 chip handles differently the Nan and Inf processing introducing @@ -236,8 +235,12 @@ namespace const bool testInfinity = test->getTestInfinity(); #endif - const float min = testWideRange ? -1.0f : 0.0f; - const float max = testWideRange ? +2.0f : 1.0f; + float min = 0.0f; + float max = 1.0f; + if(test->getTestWideRange()) + { + test->getWideRangeInterval(min, max); + } const float range = max - min; OCIOGPUTest::CustomValues tmp; @@ -271,9 +274,9 @@ namespace // Compute the value step based on the remaining number of values. const float step = range / float(numEntries); - for (; idx < predefinedNumEntries; ++idx) + for (unsigned int i=0; i < numEntries; ++i, ++idx) { - tmp.m_inputValues[idx] = min + step * float(idx); + tmp.m_inputValues[idx] = min + step * float(i); } test->setCustomValues(tmp); diff --git a/tests/gpu/GPUUnitTest.h b/tests/gpu/GPUUnitTest.h index cf66798032..967325c943 100644 --- a/tests/gpu/GPUUnitTest.h +++ b/tests/gpu/GPUUnitTest.h @@ -62,6 +62,8 @@ class OCIOGPUTest // Set TestWideRange to true to use test values on [-1,2] rather than [0,1]. inline bool getTestWideRange() const { return m_testWideRange; } inline void setTestWideRange(bool use) { m_testWideRange = use; } + inline void getWideRangeInterval(float& rangeMin, float& rangeMax) const { rangeMin = m_rangeMin; rangeMax = m_rangeMax; }; + inline void setWideRangeInterval(float rangeMin, float rangeMax) { m_rangeMin = rangeMin; m_rangeMax = rangeMax; } // Set TestNaN to true to include NaNs in each channel of the test values. inline bool getTestNaN() const { return m_testNaN; } @@ -151,6 +153,8 @@ class OCIOGPUTest float m_maxDiff{ 0.f }; size_t m_idxDiff{ 0 }; bool m_testWideRange{ true }; + float m_rangeMin{ -1.0f }; + float m_rangeMax{ 2.0f }; bool m_testNaN{ true }; bool m_testInfinity{ true }; bool m_performRelativeComparison{ false };