[color]: Added support for overriding the apparent (virtual) white point

Also, updated the steamdeck colorimetry to measured values
This commit is contained in:
Jeremy Selan 2023-08-15 09:38:35 -07:00
parent f2e925f5d6
commit da12ca6b76
8 changed files with 113 additions and 30 deletions

View file

@ -15,6 +15,7 @@ static void BenchmarkCalcColorTransform(EOTF inputEOTF, benchmark::State &state)
{
const primaries_t primaries = { { 0.602f, 0.355f }, { 0.340f, 0.574f }, { 0.164f, 0.121f } };
const glm::vec2 white = { 0.3070f, 0.3220f };
const glm::vec2 destVirtualWhite = { 0.f, 0.f };
displaycolorimetry_t inputColorimetry{};
inputColorimetry.primaries = primaries;
@ -35,6 +36,7 @@ static void BenchmarkCalcColorTransform(EOTF inputEOTF, benchmark::State &state)
for (auto _ : state) {
calcColorTransform( &lut1d_float, nLutSize1d, &lut3d_float, nLutEdgeSize3d, inputColorimetry, inputEOTF,
outputEncodingColorimetry, EOTF_Gamma22,
destVirtualWhite, k_EChromaticAdapatationMethod_XYZ,
colorMapping, nightmode, tonemapping, nullptr, flGain );
for ( size_t i=0, end = lut1d_float.dataR.size(); i<end; ++i )
{

View file

@ -75,18 +75,13 @@ glm::mat3 normalised_primary_matrix( const primaries_t & rgbPrimaries, const glm
return matPrimaries * whiteScale;
}
enum EChromaticAdaptationMethod
{
k_EChromaticAdapatationMethod_XYZ,
k_EChromaticAdapatationMethod_Bradford,
};
glm::mat3 chromatic_adaptation_matrix( const glm::vec3 & sourceWhite, const glm::vec3 & destWhite, EChromaticAdaptationMethod eMethod )
glm::mat3 chromatic_adaptation_matrix( const glm::vec3 & sourceWhiteXYZ, const glm::vec3 & destWhiteXYZ,
EChromaticAdaptationMethod eMethod )
{
static const glm::mat3 k_matBradford( 0.8951f,-0.7502f, 0.0389f, 0.2664f,1.7135f, -0.0685f, -0.1614f, 0.0367f, 1.0296f );
glm::mat3 matAdaptation = eMethod == k_EChromaticAdapatationMethod_XYZ ? glm::diagonal3x3( glm::vec3(1,1,1) ) : k_matBradford;
glm::vec3 coneResponseDest = matAdaptation * destWhite;
glm::vec3 coneResponseSource = matAdaptation * sourceWhite;
glm::vec3 coneResponseDest = matAdaptation * destWhiteXYZ;
glm::vec3 coneResponseSource = matAdaptation * sourceWhiteXYZ;
glm::vec3 scale = glm::vec3( coneResponseDest.x / coneResponseSource.x, coneResponseDest.y / coneResponseSource.y, coneResponseDest.z / coneResponseSource.z );
return glm::inverse( matAdaptation ) * glm::diagonal3x3( scale ) * matAdaptation;
}
@ -670,14 +665,10 @@ void calcColorTransform( lut1d_t * pShaper, int nLutSize1d,
lut3d_t * pLut3d, int nLutEdgeSize3d,
const displaycolorimetry_t & source, EOTF sourceEOTF,
const displaycolorimetry_t & dest, EOTF destEOTF,
const glm::vec2 & destVirtualWhite, EChromaticAdaptationMethod eMethod,
const colormapping_t & mapping, const nightmode_t & nightmode, const tonemapping_t & tonemapping,
const lut3d_t * pLook, float flGain )
{
glm::mat3 xyz_from_dest = normalised_primary_matrix( dest.primaries, dest.white, 1.f );
glm::mat3 dest_from_xyz = glm::inverse( xyz_from_dest );
glm::mat3 xyz_from_source = normalised_primary_matrix( source.primaries, source.white, 1.f );
glm::mat3 dest_from_source = dest_from_xyz * xyz_from_source; // Absolute colorimetric mapping
// Generate shaper lut
// Note: while this is typically a 1D approximation of our end to end transform,
// it need not be! Conceptually this is just to determine the interpolation properties...
@ -703,28 +694,54 @@ void calcColorTransform( lut1d_t * pShaper, int nLutSize1d,
if ( pLut3d )
{
pLut3d->resize( nLutEdgeSize3d );
glm::mat3 xyz_from_dest = normalised_primary_matrix( dest.primaries, dest.white, 1.f );
glm::mat3 dest_from_xyz = glm::inverse( xyz_from_dest );
float flScale = 1.f / ( (float) nLutEdgeSize3d - 1.f );
glm::mat3 xyz_from_source = normalised_primary_matrix( source.primaries, source.white, 1.f );
glm::mat3 dest_from_source = dest_from_xyz * xyz_from_source; // XYZ scaling for white point adjustment
// Precalc night mode scalars
// Precalc night mode scalars & digital gain
// amount and saturation are overdetermined but we separate the two as they conceptually represent
// different quantities, and this preserves forwards algorithmic compatibility
glm::vec3 nightModeMultHSV( nightmode.hue, clamp01( nightmode.saturation * nightmode.amount ), 1.f );
glm::vec3 vNightModeMultLinear = glm::pow( hsv_to_rgb( nightModeMultHSV ), glm::vec3( 2.2f ) );
glm::vec3 vMultLinear = glm::pow( hsv_to_rgb( nightModeMultHSV ), glm::vec3( 2.2f ) );
vMultLinear = vMultLinear * flGain;
glm::vec3 vSourceColorEOTFEncodedEdge[nLutEdgeSize3d];
// Calculate the virtual white point adaptation
glm::mat3x3 whitePointDestAdaptation = glm::mat3x3( 1.f ); // identity
if ( destVirtualWhite.x != 0.f && destVirtualWhite.y != 0.f )
{
// if source white is within tiny tolerance of sourceWhitePointOverride
// don't do the override? (aka two quantizations of d65)
glm::mat3x3 virtualWhiteXYZFromPhysicalWhiteXYZ = chromatic_adaptation_matrix(
xy_to_xyz( dest.white ), xy_to_xyz( destVirtualWhite ), k_EChromaticAdapatationMethod_Bradford );
whitePointDestAdaptation = dest_from_xyz * virtualWhiteXYZFromPhysicalWhiteXYZ * xyz_from_dest;
bool bLimitGain = true;
if ( bLimitGain )
{
glm::vec3 white = whitePointDestAdaptation * glm::vec3(1.f, 1.f, 1.f );
float whiteMax = std::max( white.r, std::max( white.g, white.b ) );
float normScale = 1.f / whiteMax;
fprintf( stderr, "normScale %f\n", normScale );
whitePointDestAdaptation = whitePointDestAdaptation * glm::diagonal3x3( glm::vec3( normScale ) );
}
}
// Precalculate source color EOTF encoded per-edge.
glm::vec3 vSourceColorEOTFEncodedEdge[nLutEdgeSize3d];
float flEdgeScale = 1.f / ( (float) nLutEdgeSize3d - 1.f );
for ( int nIndex = 0; nIndex < nLutEdgeSize3d; ++nIndex )
{
vSourceColorEOTFEncodedEdge[nIndex] = glm::vec3( nIndex * flScale );
vSourceColorEOTFEncodedEdge[nIndex] = glm::vec3( nIndex * flEdgeScale );
if ( pShaper )
{
vSourceColorEOTFEncodedEdge[nIndex] = ApplyLut1D_Inverse_Linear( *pShaper, vSourceColorEOTFEncodedEdge[nIndex] );
}
}
pLut3d->resize( nLutEdgeSize3d );
for ( int nBlue=0; nBlue<nLutEdgeSize3d; ++nBlue )
{
for ( int nGreen=0; nGreen<nLutEdgeSize3d; ++nGreen )
@ -751,8 +768,11 @@ void calcColorTransform( lut1d_t * pShaper, int nLutSize1d,
float amount = cfit( colorSaturation, mapping.blendEnableMinSat, mapping.blendEnableMaxSat, mapping.blendAmountMin, mapping.blendAmountMax );
destColorLinear = glm::mix( destColorLinear, sourceColorLinear, amount );
// Apply night mode
destColorLinear = vNightModeMultLinear * destColorLinear * flGain;
// Apply linear Mult
destColorLinear = vMultLinear * destColorLinear;
// Apply destination virtual white point mapping
destColorLinear = whitePointDestAdaptation * destColorLinear;
// Apply tonemapping
destColorLinear = tonemapping.apply( destColorLinear );
@ -783,6 +803,9 @@ void buildSDRColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *p
if (flSDRGamutWideness < 0 )
flSDRGamutWideness = 1.0f;
displaycolorimetry_t r709NativeWhite = displaycolorimetry_709;
r709NativeWhite.white = nativeDisplayOutput.white;
// 0.0: 709
// 1.0: Native
colormapping_t noRemap;
@ -791,7 +814,7 @@ void buildSDRColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *p
noRemap.blendAmountMin = 0.0f;
noRemap.blendAmountMax = 0.0f;
*pMapping = noRemap;
*pColorimetry = lerp( displaycolorimetry_709, nativeDisplayOutput, flSDRGamutWideness );
*pColorimetry = lerp( r709NativeWhite, nativeDisplayOutput, flSDRGamutWideness );
}
else
{
@ -820,16 +843,19 @@ void buildSDRColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *p
partialRemap.blendAmountMin = 0.0f;
partialRemap.blendAmountMax = 0.25;
displaycolorimetry_t wideGamutNativeWhite = displaycolorimetry_widegamutgeneric;
wideGamutNativeWhite.white = nativeDisplayOutput.white;
if ( flSDRGamutWideness < 0.5f )
{
float t = cfit( flSDRGamutWideness, 0.f, 0.5f, 0.0f, 1.0f );
*pColorimetry = lerp( nativeDisplayOutput, displaycolorimetry_widegamutgeneric, t );
*pColorimetry = lerp( nativeDisplayOutput, wideGamutNativeWhite, t );
*pMapping = smoothRemap;
}
else
{
float t = cfit( flSDRGamutWideness, 0.5f, 1.0f, 0.0f, 1.0f );
*pColorimetry = displaycolorimetry_widegamutgeneric;
*pColorimetry = wideGamutNativeWhite;
*pMapping = lerp( smoothRemap, partialRemap, t );
}
}

View file

@ -299,6 +299,16 @@ struct tonemapping_t
}
};
enum EChromaticAdaptationMethod
{
k_EChromaticAdapatationMethod_XYZ,
k_EChromaticAdapatationMethod_Bradford,
};
glm::mat3 chromatic_adaptation_matrix( const glm::vec3 & sourceWhiteXYZ, const glm::vec3 & destWhiteXYZ,
EChromaticAdaptationMethod eMethod );
struct lut1d_t
{
int lutSize = 0;
@ -354,10 +364,13 @@ void calcColorTransform( lut1d_t * pShaper, int nLutSize1d,
lut3d_t * pLut3d, int nLutEdgeSize3d,
const displaycolorimetry_t & source, EOTF sourceEOTF,
const displaycolorimetry_t & dest, EOTF destEOTF,
const glm::vec2 & destVirtualWhite, EChromaticAdaptationMethod eMethod,
const colormapping_t & mapping, const nightmode_t & nightmode, const tonemapping_t & tonemapping,
const lut3d_t * pLook, float flGain );
// Build colorimetry and a gamut mapping for the given SDR configuration
// Note: the output colorimetry will use the native display's white point
// Only the color gamut will change
void buildSDRColorimetry( displaycolorimetry_t * pColorimetry, colormapping_t *pMapping,
float flSDRGamutWideness, const displaycolorimetry_t & nativeDisplayOutput );
@ -411,12 +424,18 @@ nits_to_u16_dark(float nits)
return (uint16_t)round(nits * 10000.0f);
}
static constexpr displaycolorimetry_t displaycolorimetry_steamdeck
static constexpr displaycolorimetry_t displaycolorimetry_steamdeck_spec
{
.primaries = { { 0.602f, 0.355f }, { 0.340f, 0.574f }, { 0.164f, 0.121f } },
.white = { 0.3070f, 0.3220f }, // not D65
};
static constexpr displaycolorimetry_t displaycolorimetry_steamdeck_measured
{
.primaries = { { 0.603f, 0.349f }, { 0.335f, 0.571f }, { 0.163f, 0.115f } },
.white = { 0.296f, 0.307f }, // not D65
};
static constexpr displaycolorimetry_t displaycolorimetry_709
{
.primaries = { { 0.64f, 0.33f }, { 0.30f, 0.60f }, { 0.15f, 0.06f } },

View file

@ -1,6 +1,9 @@
#include "color_helpers.h"
#include <cstdio>
#include <glm/ext.hpp>
#include <glm/gtx/string_cast.hpp>
/*
const uint32_t nLutSize1d = 4096;
const uint32_t nLutEdgeSize3d = 17;
@ -76,13 +79,19 @@ int color_tests()
#endif
#if 0
#if 1
{
// chromatic adapatation
glm::vec3 d50XYZ = glm::vec3(0.96422f, 1.00000f, 0.82521f );
glm::vec3 d65XYZ = glm::vec3(0.95047f, 1.00000f, 1.08883f );
printf("d50XYZ %s\n", glm::to_string(d50XYZ).c_str() );
printf("d65XYZ %s\n", glm::to_string(d65XYZ).c_str() );
glm::mat3x3 d65FromF50_reference_bradford( 0.9555766, -0.0282895, 0.0122982,
-0.0230393, 1.0099416, -0.0204830,
0.0631636, 0.0210077, 1.3299098 );
printf("d65FromF50_reference_bradford %s\n", glm::to_string(d65FromF50_reference_bradford).c_str() );
{
glm::mat3x3 d65From50 = chromatic_adaptation_matrix( d50XYZ, d65XYZ, k_EChromaticAdapatationMethod_Bradford );
@ -91,12 +100,12 @@ int color_tests()
printf("bradford d65_2 %s\n", glm::to_string(d65_2).c_str() );
}
{
glm::mat3x3 d65From50 = chromatic_adaptation_matrix( d50XYZ, d65XYZ, k_EChromaticAdapatationMethod_XYZ );
printf("xyzscaling d65From50 %s\n", glm::to_string(d65From50).c_str() );
glm::vec3 d65_2 = d65From50 * d50XYZ;
printf("xyzscaling d65_2 %s\n", glm::to_string(d65_2).c_str() );
}
}
#endif
@ -224,6 +233,7 @@ void test_eetf2390_mono()
int main(int argc, char* argv[])
{
printf("color_tests\n");
test_eetf2390_mono();
// test_eetf2390_mono();
color_tests();
return 0;
}

View file

@ -454,7 +454,7 @@ drm_hdr_parse_edid(drm_t *drm, struct connector *connector, const struct di_edid
// Hardcode Steam Deck display info to support
// BIOSes with missing info for this in EDID.
drm_log.infof("[colorimetry]: using default steamdeck colorimetry");
metadata->colorimetry = displaycolorimetry_steamdeck;
metadata->colorimetry = displaycolorimetry_steamdeck_measured;
metadata->eotf = EOTF_Gamma22;
}
}

View file

@ -384,6 +384,9 @@ struct gamescope_color_mgmt_t
displaycolorimetry_t outputEncodingColorimetry;
EOTF outputEncodingEOTF;
// If non-zero, use this as the emulated "virtual" white point for the output
glm::vec2 outputVirtualWhite = { 0.f, 0.f };
std::shared_ptr<wlserver_hdr_metadata> appHDRMetadata = nullptr;
bool operator == (const gamescope_color_mgmt_t&) const = default;

View file

@ -244,6 +244,7 @@ create_color_mgmt_luts(const gamescope_color_mgmt_t& newColorMgmt, gamescope_col
calcColorTransform( &g_tmpLut1d, s_nLutSize1d, &g_tmpLut3d, s_nLutEdgeSize3d, inputColorimetry, inputEOTF,
outputEncodingColorimetry, newColorMgmt.outputEncodingEOTF,
newColorMgmt.outputVirtualWhite, k_EChromaticAdapatationMethod_XYZ,
colorMapping, newColorMgmt.nightmode, tonemapping, pLook, flGain );
// Create quantized output luts
@ -4823,6 +4824,11 @@ steamcompmgr_latch_frame_done( steamcompmgr_win_t *w, uint64_t vblank_idx )
}
}
static inline float santitize_float( float f )
{
return ( std::isfinite( f ) ? f : 0.f );
}
static void
handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev)
{
@ -5359,6 +5365,21 @@ handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev)
if ( set_color_look_g22( path.c_str() ) )
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeColorOutputVirtualWhite )
{
std::vector< uint32_t > user_vec;
if ( get_prop( ctx, ctx->root, ctx->atoms.gamescopeColorOutputVirtualWhite, user_vec ) && user_vec.size() >= 2 )
{
g_ColorMgmt.pending.outputVirtualWhite.x = santitize_float( bit_cast<float>( user_vec[0] ) );
g_ColorMgmt.pending.outputVirtualWhite.y = santitize_float( bit_cast<float>( user_vec[1] ) );
}
else
{
g_ColorMgmt.pending.outputVirtualWhite.x = 0.f;
g_ColorMgmt.pending.outputVirtualWhite.y = 0.f;
}
hasRepaint = true;
}
if ( ev->atom == ctx->atoms.gamescopeInternalDisplayBrightness )
{
uint32_t val = get_prop( ctx, ctx->root, ctx->atoms.gamescopeInternalDisplayBrightness, 0 );
@ -6628,6 +6649,7 @@ void init_xwayland_ctx(uint32_t serverId, gamescope_xwayland_server_t *xwayland_
ctx->atoms.gamescopeHDRItmTargetNits = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_ITM_TARGET_NITS", false );
ctx->atoms.gamescopeColorLookPQ = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_LOOK_PQ", false );
ctx->atoms.gamescopeColorLookG22 = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_LOOK_G22", false );
ctx->atoms.gamescopeColorOutputVirtualWhite = XInternAtom( ctx->dpy, "GAMESCOPE_DISPLAY_VIRTUAL_WHITE", false );
ctx->atoms.gamescopeHDRTonemapDisplayMetadata = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_DISPLAY_METADATA", false );
ctx->atoms.gamescopeHDRTonemapSourceMetadata = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_SOURCE_METADATA", false );
ctx->atoms.gamescopeHDRTonemapOperator = XInternAtom( ctx->dpy, "GAMESCOPE_HDR_TONEMAP_OPERATOR", false );

View file

@ -185,6 +185,7 @@ struct xwayland_ctx_t
Atom gamescopeHDRItmTargetNits;
Atom gamescopeColorLookPQ;
Atom gamescopeColorLookG22;
Atom gamescopeColorOutputVirtualWhite;
Atom gamescopeHDRTonemapDisplayMetadata;
Atom gamescopeHDRTonemapSourceMetadata;
Atom gamescopeHDRTonemapOperator;