From c556e3cf97e1f0587fc98d4408dc9a54d722d4be Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sun, 19 May 2024 12:30:36 +0200 Subject: [PATCH 01/18] drm: fetch plane color pipelines --- src/Backends/DRMBackend.cpp | 91 ++++++++++++++++++++++++++++++++++++- src/drm_include.h | 4 ++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 108333e5a5..17766c4645 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -267,6 +267,7 @@ namespace gamescope static std::optional Instantiate( const char *pszName, CDRMAtomicObject *pObject, const DRMObjectRawProperties& rawProperties ); + uint32_t GetPropertyId() const { return m_uPropertyId; } uint64_t GetPendingValue() const { return m_ulPendingValue; } uint64_t GetCurrentValue() const { return m_ulCurrentValue; } uint64_t GetInitialValue() const { return m_ulInitialValue; } @@ -326,6 +327,7 @@ namespace gamescope std::optional AMD_PLANE_LUT3D; std::optional AMD_PLANE_BLEND_TF; std::optional AMD_PLANE_BLEND_LUT; + std::optional COLOR_PIPELINE; std::optional DUMMY_END; }; PlaneProperties &GetProperties() { return m_Props; } @@ -537,6 +539,28 @@ namespace gamescope ConnectorProperties m_Props; }; + class CDRMColorOp final : public CDRMAtomicTypedObject + { + public: + CDRMColorOp( uint32_t uColorOpId ); + + void RefreshState(); + + struct ColorOpProperties + { + std::optional TYPE; // Immutable + std::optional NEXT; // Immutable + std::optional BYPASS; // Immutable + + std::optional DATA; + std::optional MULTIPLIER; + }; + ColorOpProperties &GetProperties() { return m_Props; } + const ColorOpProperties &GetProperties() const { return m_Props; } + private: + ColorOpProperties m_Props; + }; + class CDRMFb final : public CBaseBackendFb { public: @@ -544,7 +568,7 @@ namespace gamescope ~CDRMFb(); uint32_t GetFbId() const { return m_uFbId; } - + private: uint32_t m_uFbId = 0; }; @@ -915,6 +939,43 @@ static bool refresh_state( drm_t *drm ) return true; } +static bool get_color_pipeline( struct drm_t *drm, uint32_t uHeadId ) +{ + std::vector> pipeline; + + uint32_t uColorOpId = uHeadId; + while ( uColorOpId != 0 ) + { + auto pColorOp = std::make_unique( uHeadId ); + pColorOp->RefreshState(); + uColorOpId = pColorOp->GetProperties().NEXT.value().GetInitialValue(); + pipeline.emplace_back( std::move(pColorOp) ); + } + + return true; +} + +static bool get_plane_color_pipelines( struct drm_t *drm, std::unique_ptr< gamescope::CDRMPlane > &pPlane ) +{ + auto pColorPipelineProp = pPlane->GetProperties().COLOR_PIPELINE; + if ( !pColorPipelineProp ) + return true; + + drmModePropertyRes *pProperty = drmModeGetProperty( g_DRM.fd, pColorPipelineProp->GetPropertyId() ); + if ( !pProperty ) + return false; + defer( drmModeFreeProperty( pProperty ) ); + + for ( int i = 0; i < pProperty->count_enums; i++ ) + { + auto entry = pProperty->enums[ i ]; + if ( !get_color_pipeline( drm, entry.value ) ) + return false; + } + + return true; +} + static bool get_resources(struct drm_t *drm) { { @@ -951,6 +1012,12 @@ static bool get_resources(struct drm_t *drm) } } + for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) + { + if ( !get_plane_color_pipelines( drm, pPlane ) ) + return false; + } + return refresh_state( drm ); } @@ -2113,6 +2180,7 @@ namespace gamescope m_Props.AMD_PLANE_LUT3D = CDRMAtomicProperty::Instantiate( "AMD_PLANE_LUT3D", this, *rawProperties ); m_Props.AMD_PLANE_BLEND_TF = CDRMAtomicProperty::Instantiate( "AMD_PLANE_BLEND_TF", this, *rawProperties ); m_Props.AMD_PLANE_BLEND_LUT = CDRMAtomicProperty::Instantiate( "AMD_PLANE_BLEND_LUT", this, *rawProperties ); + m_Props.COLOR_PIPELINE = CDRMAtomicProperty::Instantiate( "COLOR PIPELINE", this, *rawProperties ); } } @@ -2594,6 +2662,27 @@ namespace gamescope } } + ///////////////////////// + // CDRMConnector + ///////////////////////// + CDRMColorOp::CDRMColorOp( uint32_t uColorOpId ) + : CDRMAtomicTypedObject( uColorOpId ) + { + } + + void CDRMColorOp::RefreshState() + { + auto rawProperties = GetRawProperties(); + if ( rawProperties ) + { + m_Props.TYPE = CDRMAtomicProperty::Instantiate( "TYPE", this, *rawProperties ); + m_Props.NEXT = CDRMAtomicProperty::Instantiate( "NEXT", this, *rawProperties ); + m_Props.BYPASS = CDRMAtomicProperty::Instantiate( "BYPASS", this, *rawProperties ); + m_Props.DATA = CDRMAtomicProperty::Instantiate( "DATA", this, *rawProperties ); + m_Props.MULTIPLIER = CDRMAtomicProperty::Instantiate( "MULTIPLIER", this, *rawProperties ); + } + } + ///////////////////////// // CDRMFb ///////////////////////// diff --git a/src/drm_include.h b/src/drm_include.h index d0ed3ce9e2..3924692748 100644 --- a/src/drm_include.h +++ b/src/drm_include.h @@ -86,3 +86,7 @@ enum drm_colorspace { #define DRM_MODE_CONTENT_TYPE_PHOTO 2 #define DRM_MODE_CONTENT_TYPE_CINEMA 3 #define DRM_MODE_CONTENT_TYPE_GAME 4 + +#define DRM_MODE_OBJECT_COLOROP 0xfafafafa + +#define DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE 7 From 5ab34f1ed91203a87c18259ab2cfe3a5a8255bc1 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Thu, 3 Jul 2025 18:20:02 -0300 Subject: [PATCH 02/18] drm: fixes to fetch color pipelines Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 17766c4645..0bb29eab50 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -946,7 +946,7 @@ static bool get_color_pipeline( struct drm_t *drm, uint32_t uHeadId ) uint32_t uColorOpId = uHeadId; while ( uColorOpId != 0 ) { - auto pColorOp = std::make_unique( uHeadId ); + auto pColorOp = std::make_unique( uColorOpId ); pColorOp->RefreshState(); uColorOpId = pColorOp->GetProperties().NEXT.value().GetInitialValue(); pipeline.emplace_back( std::move(pColorOp) ); @@ -2180,7 +2180,7 @@ namespace gamescope m_Props.AMD_PLANE_LUT3D = CDRMAtomicProperty::Instantiate( "AMD_PLANE_LUT3D", this, *rawProperties ); m_Props.AMD_PLANE_BLEND_TF = CDRMAtomicProperty::Instantiate( "AMD_PLANE_BLEND_TF", this, *rawProperties ); m_Props.AMD_PLANE_BLEND_LUT = CDRMAtomicProperty::Instantiate( "AMD_PLANE_BLEND_LUT", this, *rawProperties ); - m_Props.COLOR_PIPELINE = CDRMAtomicProperty::Instantiate( "COLOR PIPELINE", this, *rawProperties ); + m_Props.COLOR_PIPELINE = CDRMAtomicProperty::Instantiate( "COLOR_PIPELINE", this, *rawProperties ); } } From 78af33e52503ae002510935e6c8d074f385df6e0 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 4 Jul 2025 09:51:18 -0300 Subject: [PATCH 03/18] drm: check if color pipeline api is supported Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 0bb29eab50..ce08a13424 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -607,6 +607,7 @@ struct drm_color_ctm2 { bool g_bSupportsAsyncFlips = false; bool g_bSupportsSyncObjs = false; +bool g_bSupportsColorPipeline = false; extern gamescope::GamescopeModeGeneration g_eGamescopeModeGeneration; extern GamescopePanelOrientation g_DesiredInternalOrientation; @@ -1012,10 +1013,13 @@ static bool get_resources(struct drm_t *drm) } } - for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) + if ( g_bSupportsColorPipeline ) { - if ( !get_plane_color_pipelines( drm, pPlane ) ) - return false; + for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) + { + if ( !get_plane_color_pipelines( drm, pPlane ) ) + return false; + } } return refresh_state( drm ); @@ -1392,6 +1396,8 @@ bool init_drm(struct drm_t *drm, int width, int height, int refresh) drm_log.errorf("Immediate flips disabled from environment"); } + g_bSupportsColorPipeline = drmSetClientCap(drm->fd, DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE, 1) == 0; + if (!get_resources(drm)) { return false; } @@ -3512,12 +3518,29 @@ bool drm_supports_color_mgmt(struct drm_t *drm) if ( g_bForceDisableColorMgmt ) return false; + if ( g_bSupportsColorPipeline ) + return false; + if ( !drm->pPrimaryPlane ) return false; return drm->pPrimaryPlane->GetProperties().AMD_PLANE_CTM.has_value() && drm->pPrimaryPlane->GetProperties().AMD_PLANE_BLEND_TF.has_value(); } +bool drm_supports_color_pipeline(struct drm_t *drm) +{ + if ( g_bForceDisableColorMgmt ) + return false; + + if ( !g_bSupportsColorPipeline ) + return false; + + if ( !drm->pPrimaryPlane ) + return false; + + return drm->pPrimaryPlane->GetProperties().COLOR_PIPELINE.has_value() ; +} + std::span drm_get_valid_refresh_rates( struct drm_t *drm ) { if ( drm && drm->pConnector ) From f132aea4400378e690f70fc8032dc0e95dd38dd1 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 15 May 2024 16:55:53 +0200 Subject: [PATCH 04/18] drm: check pipeline shape --- src/Backends/DRMBackend.cpp | 43 +++++++++++++++++++++++++++++++++---- src/drm_include.h | 18 ++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index ce08a13424..6847a3adf2 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -561,6 +561,16 @@ namespace gamescope ColorOpProperties m_Props; }; + struct CDRMColorPipeline + { + std::unique_ptr degamma; + std::unique_ptr CTM; + std::unique_ptr HDRMult; + std::unique_ptr shaper; + std::unique_ptr lut3D; + std::unique_ptr blend; + }; + class CDRMFb final : public CBaseBackendFb { public: @@ -940,7 +950,7 @@ static bool refresh_state( drm_t *drm ) return true; } -static bool get_color_pipeline( struct drm_t *drm, uint32_t uHeadId ) +static std::optional get_color_pipeline( struct drm_t *drm, uint32_t uHeadId ) { std::vector> pipeline; @@ -953,7 +963,31 @@ static bool get_color_pipeline( struct drm_t *drm, uint32_t uHeadId ) pipeline.emplace_back( std::move(pColorOp) ); } - return true; + // Check if the pipeline has what we need + if ( pipeline.size() != 6 ) + return {}; + if ( pipeline[0]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) + return {}; + if ( pipeline[1]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_CTM_3X4 ) + return {}; + if ( pipeline[2]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_MULTIPLIER ) + return {}; + if ( pipeline[3]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) + return {}; + if ( pipeline[4]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_3D_LUT ) + return {}; + if ( pipeline[5]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) + return {}; + + gamescope::CDRMColorPipeline p { + .degamma = std::move(pipeline[0]), + .CTM = std::move(pipeline[1]), + .HDRMult = std::move(pipeline[2]), + .shaper = std::move(pipeline[3]), + .lut3D = std::move(pipeline[4]), + .blend = std::move(pipeline[5]), + }; + return p; } static bool get_plane_color_pipelines( struct drm_t *drm, std::unique_ptr< gamescope::CDRMPlane > &pPlane ) @@ -970,8 +1004,7 @@ static bool get_plane_color_pipelines( struct drm_t *drm, std::unique_ptr< games for ( int i = 0; i < pProperty->count_enums; i++ ) { auto entry = pProperty->enums[ i ]; - if ( !get_color_pipeline( drm, entry.value ) ) - return false; + get_color_pipeline( drm, entry.value ); } return true; @@ -1356,6 +1389,8 @@ bool init_drm(struct drm_t *drm, int width, int height, int refresh) return false; } + drmSetClientCap(drm->fd, DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE, 1); + if (drmGetCap(drm->fd, DRM_CAP_CURSOR_WIDTH, &drm->cursor_width) != 0) { drm->cursor_width = 64; } diff --git a/src/drm_include.h b/src/drm_include.h index 3924692748..86d9a5ab08 100644 --- a/src/drm_include.h +++ b/src/drm_include.h @@ -90,3 +90,21 @@ enum drm_colorspace { #define DRM_MODE_OBJECT_COLOROP 0xfafafafa #define DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE 7 + +enum drm_colorop_type { + DRM_COLOROP_1D_CURVE, + DRM_COLOROP_1D_LUT, + DRM_COLOROP_CTM_3X4, + DRM_COLOROP_MULTIPLIER, + DRM_COLOROP_3D_LUT, /* TODO: check value w/ kernel */ +}; + +enum drm_colorop_curve_1d_type { + DRM_COLOROP_1D_CURVE_SRGB_EOTF, + DRM_COLOROP_1D_CURVE_SRGB_INV_EOTF, + DRM_COLOROP_1D_CURVE_BT2020_INV_OETF, + DRM_COLOROP_1D_CURVE_BT2020_OETF, + DRM_COLOROP_1D_CURVE_PQ_125_EOTF, + DRM_COLOROP_1D_CURVE_PQ_125_INV_EOTF, + DRM_COLOROP_1D_CURVE_COUNT +}; From 0cd50a4155c38757dfa26e8723c4a2c66966732d Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Thu, 3 Jul 2025 18:33:30 -0300 Subject: [PATCH 05/18] drm: update plane color pipeline Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 44 +++++++++++++++++++++++-------------- src/drm_include.h | 22 +++++-------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 6847a3adf2..c991ba3a46 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -564,11 +564,13 @@ namespace gamescope struct CDRMColorPipeline { std::unique_ptr degamma; - std::unique_ptr CTM; std::unique_ptr HDRMult; + std::unique_ptr CTM; std::unique_ptr shaper; + std::unique_ptr shaperLut; std::unique_ptr lut3D; std::unique_ptr blend; + std::unique_ptr blendLut; }; class CDRMFb final : public CBaseBackendFb @@ -964,50 +966,59 @@ static std::optional get_color_pipeline( struct dr } // Check if the pipeline has what we need - if ( pipeline.size() != 6 ) + if ( pipeline.size() != 8 ) return {}; if ( pipeline[0]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) return {}; - if ( pipeline[1]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_CTM_3X4 ) + if ( pipeline[1]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_MULTIPLIER ) return {}; - if ( pipeline[2]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_MULTIPLIER ) + if ( pipeline[2]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_CTM_3X4 ) return {}; if ( pipeline[3]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) return {}; - if ( pipeline[4]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_3D_LUT ) + if ( pipeline[4]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_LUT ) + return {}; + if ( pipeline[5]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_3D_LUT ) return {}; - if ( pipeline[5]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) + if ( pipeline[6]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) + return {}; + if ( pipeline[7]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_LUT ) return {}; gamescope::CDRMColorPipeline p { .degamma = std::move(pipeline[0]), - .CTM = std::move(pipeline[1]), - .HDRMult = std::move(pipeline[2]), + .HDRMult = std::move(pipeline[1]), + .CTM = std::move(pipeline[2]), .shaper = std::move(pipeline[3]), - .lut3D = std::move(pipeline[4]), - .blend = std::move(pipeline[5]), + .shaperLut = std::move(pipeline[4]), + .lut3D = std::move(pipeline[5]), + .blend = std::move(pipeline[6]), + .blendLut = std::move(pipeline[7]), }; return p; } -static bool get_plane_color_pipelines( struct drm_t *drm, std::unique_ptr< gamescope::CDRMPlane > &pPlane ) +static std::optional get_plane_color_pipelines( struct drm_t *drm, std::unique_ptr< gamescope::CDRMPlane > &pPlane ) { auto pColorPipelineProp = pPlane->GetProperties().COLOR_PIPELINE; if ( !pColorPipelineProp ) - return true; + return {}; drmModePropertyRes *pProperty = drmModeGetProperty( g_DRM.fd, pColorPipelineProp->GetPropertyId() ); if ( !pProperty ) - return false; + return {}; + defer( drmModeFreeProperty( pProperty ) ); for ( int i = 0; i < pProperty->count_enums; i++ ) { auto entry = pProperty->enums[ i ]; - get_color_pipeline( drm, entry.value ); + std::optional p = get_color_pipeline( drm, entry.value ); + if ( p.has_value() ) + return p; } - return true; + return {}; } static bool get_resources(struct drm_t *drm) @@ -1050,7 +1061,8 @@ static bool get_resources(struct drm_t *drm) { for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) { - if ( !get_plane_color_pipelines( drm, pPlane ) ) + // AMD Cursor plane doesn't support color management + if ( pPlane->GetProperties().type->GetCurrentValue() != DRM_PLANE_TYPE_CURSOR && !get_plane_color_pipelines( drm, pPlane ) ) return false; } } diff --git a/src/drm_include.h b/src/drm_include.h index 86d9a5ab08..6b5fcc8cfd 100644 --- a/src/drm_include.h +++ b/src/drm_include.h @@ -87,24 +87,14 @@ enum drm_colorspace { #define DRM_MODE_CONTENT_TYPE_CINEMA 3 #define DRM_MODE_CONTENT_TYPE_GAME 4 -#define DRM_MODE_OBJECT_COLOROP 0xfafafafa - -#define DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE 7 - -enum drm_colorop_type { - DRM_COLOROP_1D_CURVE, - DRM_COLOROP_1D_LUT, - DRM_COLOROP_CTM_3X4, - DRM_COLOROP_MULTIPLIER, - DRM_COLOROP_3D_LUT, /* TODO: check value w/ kernel */ -}; - enum drm_colorop_curve_1d_type { - DRM_COLOROP_1D_CURVE_SRGB_EOTF, - DRM_COLOROP_1D_CURVE_SRGB_INV_EOTF, + DRM_COLOROP_1D_CURVE_SRGB_EOTF, + DRM_COLOROP_1D_CURVE_SRGB_INV_EOTF, + DRM_COLOROP_1D_CURVE_PQ_125_EOTF, + DRM_COLOROP_1D_CURVE_PQ_125_INV_EOTF, DRM_COLOROP_1D_CURVE_BT2020_INV_OETF, DRM_COLOROP_1D_CURVE_BT2020_OETF, - DRM_COLOROP_1D_CURVE_PQ_125_EOTF, - DRM_COLOROP_1D_CURVE_PQ_125_INV_EOTF, + DRM_COLOROP_1D_CURVE_GAMMA22, + DRM_COLOROP_1D_CURVE_GAMMA22_INV, DRM_COLOROP_1D_CURVE_COUNT }; From 886bca6842193b9d30e2ed950a50b1020efb2826 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Thu, 5 Mar 2026 14:12:39 -0300 Subject: [PATCH 06/18] drm: store Color Pipeline id Store the value of the color pipeline enum for a given plane that fits color pipeline needs. So that we can set plane COLOR_PIPELINE property with this value. Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index c991ba3a46..0a3c792b52 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -563,6 +563,7 @@ namespace gamescope struct CDRMColorPipeline { + uint32_t id; std::unique_ptr degamma; std::unique_ptr HDRMult; std::unique_ptr CTM; @@ -986,6 +987,7 @@ static std::optional get_color_pipeline( struct dr return {}; gamescope::CDRMColorPipeline p { + .id = uHeadId, .degamma = std::move(pipeline[0]), .HDRMult = std::move(pipeline[1]), .CTM = std::move(pipeline[2]), From b29029c8712c786aeee5d57ed9118c0fa608bffe Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Thu, 5 Mar 2026 14:15:52 -0300 Subject: [PATCH 07/18] drm: set color pipeline that fits plane color mgmt needs Check if there is any color pipeline with the properties needed for plane color mgmt. Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 40 ++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 0a3c792b52..1338fde8b1 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -608,6 +608,7 @@ extern std::string g_reshade_effect; bool drm_update_color_mgmt(struct drm_t *drm); bool drm_supports_color_mgmt(struct drm_t *drm); +bool drm_supports_color_pipeline(struct drm_t *drm); bool drm_set_connector( struct drm_t *drm, gamescope::CDRMConnector *conn ); struct drm_color_ctm2 { @@ -2976,6 +2977,39 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo } } + if ( ret == 0 && drm_supports_color_pipeline( drm ) ) + { + auto entry = FrameInfoToLiftoffStateCacheEntry( drm, frameInfo ); + for ( int i = 0; i < frameInfo->layerCount; i++ ) + { + if ( !frameInfo->layers[i].applyColorMgmt ) + continue; + + struct liftoff_plane *plane = liftoff_layer_get_plane( drm->lo_layers[ i ] ); + uint32_t plane_id = plane ? liftoff_plane_get_id( plane ) : 0; + + if ( plane_id == 0 ) + continue; + + for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) + { + if ( pPlane->GetObjectId() != plane_id ) + continue; + + bool bYCbCr = entry.layerState[i].ycbcr; + std::optional p = get_plane_color_pipelines( drm, pPlane ); + if ( !p ) { + drm_log.debugf( "drm_prepare_liftoff: No color pipeline fits color mgmt needs of plane_id %d", plane_id ); + break; + } + + pPlane->GetProperties().COLOR_PIPELINE->SetPendingValue( drm->req, p->id, true ); ++ + break; + } + } + } + if ( ret == 0 ) { // We don't support partial composition yet @@ -3088,7 +3122,7 @@ int drm_prepare( struct drm_t *drm, bool async, const struct FrameInfo_t *frameI bool bSinglePlane = frameInfo->layerCount < 2 && cv_drm_single_plane_optimizations; - if ( drm_supports_color_mgmt( &g_DRM ) && frameInfo->applyOutputColorMgmt ) + if ( ( drm_supports_color_mgmt( &g_DRM ) || drm_supports_color_pipeline( &g_DRM ) ) && frameInfo->applyOutputColorMgmt ) { if ( !cv_drm_debug_disable_output_tf && !bSinglePlane ) { @@ -3365,7 +3399,7 @@ gamescope::GamescopeScreenType drm_get_screen_type(struct drm_t *drm) bool drm_update_color_mgmt(struct drm_t *drm) { - if ( !drm_supports_color_mgmt( drm ) ) + if ( !drm_supports_color_mgmt( drm ) && !drm_supports_color_pipeline ( &g_DRM ) ) return true; if ( g_ColorMgmt.serial == drm->current.color_mgmt_serial ) @@ -4111,7 +4145,7 @@ namespace gamescope bool SupportsColorManagement() const { - return drm_supports_color_mgmt( &g_DRM ); + return drm_supports_color_mgmt( &g_DRM ) || drm_supports_color_pipeline( &g_DRM ); } int Commit( const FrameInfo_t *pFrameInfo ) From ca85b057ee01b4a0389a6c43edd4b605988e6a37 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 13 Mar 2026 21:16:26 -0300 Subject: [PATCH 08/18] drm: add 1D degamma curve colorop Add CURVE_1D_TYPE property to colorop, helper for degamma curve using DRM_COLOROP_* predefined curve instead of AMD_TRANSFER_FUNCTIONs, add set degamma 1D curve accordingly Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 40 ++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 1338fde8b1..8b8b97dcbe 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -550,8 +550,8 @@ namespace gamescope { std::optional TYPE; // Immutable std::optional NEXT; // Immutable - std::optional BYPASS; // Immutable - + std::optional BYPASS; + std::optional CURVE_1D_TYPE; std::optional DATA; std::optional MULTIPLIER; }; @@ -1971,6 +1971,21 @@ static inline amdgpu_transfer_function colorspace_to_plane_degamma_tf(GamescopeA } } +static inline std::optional colorspace_to_drm_plane_degamma_curve(GamescopeAppTextureColorspace colorspace) +{ + switch ( colorspace ) + { + default: // Linear in this sense is SRGB. Linear = sRGB image view doing automatic sRGB -> Linear which doesn't happen on DRM side. + case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB: + return DRM_COLOROP_1D_CURVE_SRGB_EOTF; + case GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU: + case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: + return std::nullopt; // DEFAULT doesn't exist anymore + case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ: + return DRM_COLOROP_1D_CURVE_PQ_125_EOTF; + } +} + static inline amdgpu_transfer_function colorspace_to_plane_shaper_tf(GamescopeAppTextureColorspace colorspace) { switch ( colorspace ) @@ -2735,6 +2750,7 @@ namespace gamescope m_Props.NEXT = CDRMAtomicProperty::Instantiate( "NEXT", this, *rawProperties ); m_Props.BYPASS = CDRMAtomicProperty::Instantiate( "BYPASS", this, *rawProperties ); m_Props.DATA = CDRMAtomicProperty::Instantiate( "DATA", this, *rawProperties ); + m_Props.CURVE_1D_TYPE = CDRMAtomicProperty::Instantiate( "CURVE_1D_TYPE", this, *rawProperties ); m_Props.MULTIPLIER = CDRMAtomicProperty::Instantiate( "MULTIPLIER", this, *rawProperties ); } } @@ -3004,7 +3020,25 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo } pPlane->GetProperties().COLOR_PIPELINE->SetPendingValue( drm->req, p->id, true ); -+ + + GamescopeAppTextureColorspace colorspace = entry.layerState[i].colorspace; + std::optional degamma_tf = colorspace_to_drm_plane_degamma_curve( colorspace ); + + if ( bYCbCr ) + { + degamma_tf = DRM_COLOROP_1D_CURVE_BT2020_INV_OETF; + } + + bool bUseDegamma = !cv_drm_debug_disable_degamma_tf; + if ( bUseDegamma && degamma_tf.has_value() ) + { + p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->degamma->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *degamma_tf, true ); + } + else + { + p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } break; } } From 2cc24a2a43aa554fe60833167edf98d6c048728f Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 13 Mar 2026 21:22:03 -0300 Subject: [PATCH 09/18] drm: add shaper 1D curve Create helper to use DRM_COLOROP curves instead of AMD_TRANSFER_FUNCTIONs and set properties accordingly Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 8b8b97dcbe..98fcc508a2 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -2001,6 +2001,22 @@ static inline amdgpu_transfer_function colorspace_to_plane_shaper_tf(GamescopeAp } } +static inline std::optional colorspace_to_drm_plane_shaper_curve(GamescopeAppTextureColorspace colorspace) +{ + switch ( colorspace ) + { + default: // Linear in this sense is SRGB. Linear = sRGB image view doing automatic sRGB -> Linear which doesn't happen on DRM side. + case GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB: + return DRM_COLOROP_1D_CURVE_SRGB_INV_EOTF; + case GAMESCOPE_APP_TEXTURE_COLORSPACE_HDR10_PQ: + case GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB: + return DRM_COLOROP_1D_CURVE_PQ_125_INV_EOTF; + case GAMESCOPE_APP_TEXTURE_COLORSPACE_PASSTHRU: + return std::nullopt; // DEFAULT doesn't exist anymore + } +} + + static inline amdgpu_transfer_function inverse_tf(amdgpu_transfer_function tf) { switch ( tf ) @@ -3023,10 +3039,12 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo GamescopeAppTextureColorspace colorspace = entry.layerState[i].colorspace; std::optional degamma_tf = colorspace_to_drm_plane_degamma_curve( colorspace ); + std::optional shaper_tf = colorspace_to_drm_plane_shaper_curve( colorspace ); if ( bYCbCr ) { degamma_tf = DRM_COLOROP_1D_CURVE_BT2020_INV_OETF; + shaper_tf = DRM_COLOROP_1D_CURVE_BT2020_OETF; } bool bUseDegamma = !cv_drm_debug_disable_degamma_tf; @@ -3039,6 +3057,18 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo { p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + + bool bUseShaperAnd3DLUT = !cv_drm_debug_disable_shaper_and_3dlut; + if ( bUseShaperAnd3DLUT && shaper_tf.has_value() ) + { + p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->shaper->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *shaper_tf, true ); + } + else + { + p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } + break; } } From f9e675b7030a3faf7ef44cb1f42299c07b636732 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 13 Mar 2026 21:25:17 -0300 Subject: [PATCH 10/18] drm: add blend 1D curve and LUT Create a helper to translate output tf of AMD_TRANSFER_FUNCTION enum type to DRM_COLOROP type Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 98fcc508a2..29d5e24894 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -2053,6 +2053,27 @@ static inline amdgpu_transfer_function inverse_tf(amdgpu_transfer_function tf) } } +static inline std::optional amd_tf_to_drm_curve ( amdgpu_transfer_function tf ) +{ + switch ( tf ) + { + case AMDGPU_TRANSFER_FUNCTION_SRGB_EOTF: + return DRM_COLOROP_1D_CURVE_SRGB_EOTF; + case AMDGPU_TRANSFER_FUNCTION_PQ_EOTF: + return DRM_COLOROP_1D_CURVE_PQ_125_EOTF; + case AMDGPU_TRANSFER_FUNCTION_SRGB_INV_EOTF: + return DRM_COLOROP_1D_CURVE_SRGB_INV_EOTF; + case AMDGPU_TRANSFER_FUNCTION_PQ_INV_EOTF: + return DRM_COLOROP_1D_CURVE_PQ_125_INV_EOTF; + case AMDGPU_TRANSFER_FUNCTION_GAMMA22_EOTF: + return DRM_COLOROP_1D_CURVE_GAMMA22; + case AMDGPU_TRANSFER_FUNCTION_GAMMA22_INV_EOTF: + return DRM_COLOROP_1D_CURVE_GAMMA22_INV; + default: + return std::nullopt; + } +} + static inline uint32_t ColorSpaceToEOTFIndex( GamescopeAppTextureColorspace colorspace ) { switch ( colorspace ) @@ -3069,6 +3090,19 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + std::optional blend_tf = amd_tf_to_drm_curve(drm->pending.output_tf); + if (!cv_drm_debug_disable_blend_tf && !bSinglePlane && blend_tf.has_value() ) + { + p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->blend->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *blend_tf, true ); + } + else + { + p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } + + p->blendLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + break; } } From 140229d4c11e49ab47fc3e4c6ae08e088d46d1c5 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 13 Mar 2026 21:28:22 -0300 Subject: [PATCH 11/18] drm: add CTM colorop Gamescope already use 3x4 matrix, so doesn't needed additional helper. Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 29d5e24894..855503d9fd 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -3103,6 +3103,16 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo p->blendLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + if ( frameInfo->layers[i].ctm != nullptr ) + { + p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->CTM->GetProperties().DATA->SetPendingValue( drm->req, frameInfo->layers[i].ctm->GetBlobValue(), true ); + } + else + { + p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } + break; } } From 594048a9beed4d1727aaf4d877a4b351d5c9f3fc Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 13 Mar 2026 21:32:20 -0300 Subject: [PATCH 12/18] drm: add HDR multiplier colorop Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 855503d9fd..072dc434b9 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -3113,6 +3113,9 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + p->HDRMult->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->HDRMult->GetProperties().MULTIPLIER->SetPendingValue( drm->req, 0x100000000ULL, true ); + break; } } From fc8cf7d915d6af805a75f595c0615066fc35b356 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 13 Mar 2026 21:33:24 -0300 Subject: [PATCH 13/18] drm: add shaper LUT and 3D LUT colorops Instead of struct drm_color_lut, shaper and 3D LUTs follow struct drm_color_lut32, which means 32-bit for each channel, not 16-bit anymore. Create a helper to make possible reuse LUTs already created for AMD driver-specific color properties. Assisted-by: Claude Code Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 072dc434b9..d46ebb7d02 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -135,6 +135,8 @@ struct drm_t { uint32_t color_mgmt_serial; std::shared_ptr lut3d_id[ EOTF_Count ]; std::shared_ptr shaperlut_id[ EOTF_Count ]; + std::shared_ptr lut3d_colorop_id[ EOTF_Count ]; + std::shared_ptr shaperlut_colorop_id[ EOTF_Count ]; amdgpu_transfer_function output_tf = AMDGPU_TRANSFER_FUNCTION_DEFAULT; } current, pending; @@ -3090,6 +3092,20 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + if ( bUseShaperAnd3DLUT ) + { + p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->shaperLut->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + + p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->lut3D->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + } + else + { + p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } + std::optional blend_tf = amd_tf_to_drm_curve(drm->pending.output_tf); if (!cv_drm_debug_disable_blend_tf && !bSinglePlane && blend_tf.has_value() ) { @@ -3508,6 +3524,15 @@ gamescope::GamescopeScreenType drm_get_screen_type(struct drm_t *drm) return drm->pConnector->GetScreenType(); } +template +static std::array lut_convert_16bit_to_32bit( const uint16_t (&lut)[N] ) +{ + std::array out; + for ( size_t i = 0; i < N; i++ ) + out[i] = (uint32_t)lut[i] * 0x10001u; + return out; +} + bool drm_update_color_mgmt(struct drm_t *drm) { if ( !drm_supports_color_mgmt( drm ) && !drm_supports_color_pipeline ( &g_DRM ) ) @@ -3522,6 +3547,8 @@ bool drm_update_color_mgmt(struct drm_t *drm) { drm->pending.shaperlut_id[ i ] = 0; drm->pending.lut3d_id[ i ] = 0; + drm->pending.shaperlut_colorop_id[ i ] = 0; + drm->pending.lut3d_colorop_id[ i ] = 0; } for ( uint32_t i = 0; i < EOTF_Count; i++ ) @@ -3531,6 +3558,9 @@ bool drm_update_color_mgmt(struct drm_t *drm) drm->pending.shaperlut_id[ i ] = GetBackend()->CreateBackendBlob( g_ColorMgmtLuts[i].lut1d ); drm->pending.lut3d_id[ i ] = GetBackend()->CreateBackendBlob( g_ColorMgmtLuts[i].lut3d ); + + drm->pending.shaperlut_colorop_id[ i ] = GetBackend()->CreateBackendBlob( lut_convert_16bit_to_32bit( g_ColorMgmtLuts[i].lut1d ) ); + drm->pending.lut3d_colorop_id[ i ] = GetBackend()->CreateBackendBlob( lut_convert_16bit_to_32bit( g_ColorMgmtLuts[i].lut3d ) ); } return true; From 700b31141d051783d25f54a9d7ab5525ae8027f6 Mon Sep 17 00:00:00 2001 From: Melissa Wen Date: Fri, 13 Mar 2026 22:32:10 -0300 Subject: [PATCH 14/18] drm: bypass COLOR PIPELINE prop when finishing drm Signed-off-by: Melissa Wen --- src/Backends/DRMBackend.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index d46ebb7d02..746b495868 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -1696,6 +1696,10 @@ void finish_drm(struct drm_t *drm) if ( pPlane->GetProperties().AMD_PLANE_BLEND_LUT ) pPlane->GetProperties().AMD_PLANE_BLEND_LUT->SetPendingValue( req, 0, true ); + + if ( pPlane->GetProperties().COLOR_PIPELINE ) + pPlane->GetProperties().COLOR_PIPELINE->SetPendingValue( req, 0, true ); + } // We can't do a non-blocking commit here or else risk EBUSY in case the From 993b761c8aefa4c4e9b8ccdd90e6542e1993d9ee Mon Sep 17 00:00:00 2001 From: Matthew Schwartz Date: Thu, 19 Mar 2026 15:05:52 -0700 Subject: [PATCH 15/18] drm: reset COLOR_PIPELINE to bypass for non-color-managed layers Match the existing behavior of the legacy AMD plane properties, which reset layers to DEFAULT without color management applied to them. Otherwise, stale color pipeline state from the previous scanout frame persists during composition, resulting in double color management and an overly-saturated output image. Signed-off-by: Matthew Schwartz --- src/Backends/DRMBackend.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 746b495868..ca8d72e41c 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -3041,9 +3041,6 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo auto entry = FrameInfoToLiftoffStateCacheEntry( drm, frameInfo ); for ( int i = 0; i < frameInfo->layerCount; i++ ) { - if ( !frameInfo->layers[i].applyColorMgmt ) - continue; - struct liftoff_plane *plane = liftoff_layer_get_plane( drm->lo_layers[ i ] ); uint32_t plane_id = plane ? liftoff_plane_get_id( plane ) : 0; @@ -3055,6 +3052,12 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo if ( pPlane->GetObjectId() != plane_id ) continue; + if ( !frameInfo->layers[i].applyColorMgmt ) + { + pPlane->GetProperties().COLOR_PIPELINE->SetPendingValue( drm->req, 0, true ); + break; + } + bool bYCbCr = entry.layerState[i].ycbcr; std::optional p = get_plane_color_pipelines( drm, pPlane ); if ( !p ) { From e7e3eee8def3ffb62c28f236f74cd150adb3320c Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Tue, 16 Jun 2026 11:27:09 +0000 Subject: [PATCH 16/18] drm: accept any non-empty color pipeline, don't require AMD's 8-op layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous code validated that the COLOR_PIPELINE colorop chain is exactly 8 ops with AMD-specific types in a fixed order. Any other shape silently returned {} causing COLOR_PIPELINE=Bypass for any non-AMD hardware that advertises the property. Replace the hard shape check with a type-based field assignment: walk the colorop chain and assign each named CDRMColorPipeline field to the first colorop whose DRM TYPE matches. Fields with no matching op in the chain are left nullptr. Add null-checks before every named field access in the pipeline programming code so that hardware with fewer ops (e.g. Intel's 1D_LUT → CTM_3X4 → 1D_LUT) is handled gracefully: present ops are programmed, absent ops are silently skipped. bUseShaperAnd3DLUT now requires both shaperLut and lut3D to be non-null so the AMD 3D LUT path cannot be accidentally enabled on pipelines that don't have a 3D LUT. Assisted-by: Claude:claude-sonnet-4.6 Signed-off-by: Naveen Kumar --- src/Backends/DRMBackend.cpp | 172 +++++++++++++++++++++--------------- 1 file changed, 101 insertions(+), 71 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index ca8d72e41c..a6a9dd5fe9 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -969,37 +969,33 @@ static std::optional get_color_pipeline( struct dr pipeline.emplace_back( std::move(pColorOp) ); } - // Check if the pipeline has what we need - if ( pipeline.size() != 8 ) - return {}; - if ( pipeline[0]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) - return {}; - if ( pipeline[1]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_MULTIPLIER ) - return {}; - if ( pipeline[2]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_CTM_3X4 ) - return {}; - if ( pipeline[3]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) - return {}; - if ( pipeline[4]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_LUT ) - return {}; - if ( pipeline[5]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_3D_LUT ) - return {}; - if ( pipeline[6]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_CURVE ) - return {}; - if ( pipeline[7]->GetProperties().TYPE.value().GetInitialValue() != DRM_COLOROP_1D_LUT ) + // Accept any non-empty pipeline regardless of shape. + // Named fields are assigned by matching the DRM colorop TYPE; any type not + // present in this pipeline leaves the field nullptr. The programming code + // null-checks each field before use so that non-AMD pipelines with fewer or + // different ops are handled gracefully rather than refused outright. + if ( pipeline.empty() ) return {}; - gamescope::CDRMColorPipeline p { - .id = uHeadId, - .degamma = std::move(pipeline[0]), - .HDRMult = std::move(pipeline[1]), - .CTM = std::move(pipeline[2]), - .shaper = std::move(pipeline[3]), - .shaperLut = std::move(pipeline[4]), - .lut3D = std::move(pipeline[5]), - .blend = std::move(pipeline[6]), - .blendLut = std::move(pipeline[7]), - }; + gamescope::CDRMColorPipeline p { .id = uHeadId }; + for ( auto &op : pipeline ) + { + uint64_t uType = op->GetProperties().TYPE.value().GetInitialValue(); + if ( uType == DRM_COLOROP_1D_CURVE ) + { + if ( !p.degamma ) { p.degamma = std::move(op); continue; } + else if ( !p.shaper ) { p.shaper = std::move(op); continue; } + else if ( !p.blend ) { p.blend = std::move(op); continue; } + } + else if ( uType == DRM_COLOROP_MULTIPLIER && !p.HDRMult ) { p.HDRMult = std::move(op); continue; } + else if ( uType == DRM_COLOROP_CTM_3X4 && !p.CTM ) { p.CTM = std::move(op); continue; } + else if ( uType == DRM_COLOROP_1D_LUT ) + { + if ( !p.shaperLut ) { p.shaperLut = std::move(op); continue; } + else if ( !p.blendLut ) { p.blendLut = std::move(op); continue; } + } + else if ( uType == DRM_COLOROP_3D_LUT && !p.lut3D ) { p.lut3D = std::move(op); continue; } + } return p; } @@ -3077,68 +3073,102 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo shaper_tf = DRM_COLOROP_1D_CURVE_BT2020_OETF; } - bool bUseDegamma = !cv_drm_debug_disable_degamma_tf; - if ( bUseDegamma && degamma_tf.has_value() ) + // --- Degamma (1st 1D_CURVE) --- + if ( p->degamma ) { - p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->degamma->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *degamma_tf, true ); - } - else - { - p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + if ( !cv_drm_debug_disable_degamma_tf && degamma_tf.has_value() ) + { + p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->degamma->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *degamma_tf, true ); + } + else + { + p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } } - bool bUseShaperAnd3DLUT = !cv_drm_debug_disable_shaper_and_3dlut; - if ( bUseShaperAnd3DLUT && shaper_tf.has_value() ) + // --- HDR multiplier (MULTIPLIER) --- + if ( p->HDRMult ) { - p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->shaper->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *shaper_tf, true ); - } - else - { - p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + p->HDRMult->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->HDRMult->GetProperties().MULTIPLIER->SetPendingValue( drm->req, 0x100000000ULL, true ); } - if ( bUseShaperAnd3DLUT ) + // --- Color transform matrix (CTM_3X4) --- + if ( p->CTM ) { - p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->shaperLut->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); - - p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->lut3D->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + if ( !cv_drm_debug_disable_ctm && frameInfo->layers[i].ctm != nullptr ) + { + p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->CTM->GetProperties().DATA->SetPendingValue( drm->req, frameInfo->layers[i].ctm->GetBlobValue(), true ); + } + else + { + p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } } - else + + // --- Shaper curve (2nd 1D_CURVE) --- + bool bUseShaperAnd3DLUT = !cv_drm_debug_disable_shaper_and_3dlut + && shaper_tf.has_value() + && p->shaperLut != nullptr + && p->lut3D != nullptr; + if ( p->shaper ) { - p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); - p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + if ( bUseShaperAnd3DLUT ) + { + p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->shaper->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *shaper_tf, true ); + } + else + { + p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } } - - std::optional blend_tf = amd_tf_to_drm_curve(drm->pending.output_tf); - if (!cv_drm_debug_disable_blend_tf && !bSinglePlane && blend_tf.has_value() ) + if ( p->shaperLut ) { - p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->blend->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *blend_tf, true ); + if ( bUseShaperAnd3DLUT ) + { + p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->shaperLut->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + } + else + { + p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } } - else + if ( p->lut3D ) { - p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + if ( bUseShaperAnd3DLUT ) + { + p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->lut3D->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + } + else + { + p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } } - p->blendLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); - - if ( frameInfo->layers[i].ctm != nullptr ) + // --- Blend curve (3rd 1D_CURVE) --- + if ( p->blend ) { - p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->CTM->GetProperties().DATA->SetPendingValue( drm->req, frameInfo->layers[i].ctm->GetBlobValue(), true ); + std::optional blend_tf = amd_tf_to_drm_curve( drm->pending.output_tf ); + if ( !cv_drm_debug_disable_blend_tf && !bSinglePlane && blend_tf.has_value() ) + { + p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + p->blend->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *blend_tf, true ); + } + else + { + p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + } } - else + if ( p->blendLut ) { - p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + p->blendLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } - p->HDRMult->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->HDRMult->GetProperties().MULTIPLIER->SetPendingValue( drm->req, 0x100000000ULL, true ); - break; } } From da0bb7b4b6f82ac87e8b0cdc165cf60e5c344610 Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Tue, 16 Jun 2026 11:28:52 +0000 Subject: [PATCH 17/18] drm: replace named pipeline fields with generic FindByType walker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the CDRMColorPipeline named fields (degamma, HDRMult, CTM, …) with a generic ops vector and a FindByType(type, index) lookup method. This mirrors the search-by-type approach used in KWin and Weston: callers locate the nth op of a given DRM type by walking the chain, then bypass every op not explicitly handled via a catch-all loop at the end of the programming block. No behaviour change for AMD hardware — the same ops are programmed in the same way. The approach is now vendor-agnostic: any pipeline that exposes the required DRM types in its chain will be handled correctly regardless of chain length or ordering. bUseShaperAnd3DLUT is additionally gated on the shaperlut and lut3d colorop blobs being populated so that it can only fire when the full colour-management LUT chain has been computed. Assisted-by: Claude:claude-sonnet-4.6 Signed-off-by: Naveen Kumar --- src/Backends/DRMBackend.cpp | 155 +++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 62 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index a6a9dd5fe9..2701892a40 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -566,14 +566,29 @@ namespace gamescope struct CDRMColorPipeline { uint32_t id; - std::unique_ptr degamma; - std::unique_ptr HDRMult; - std::unique_ptr CTM; - std::unique_ptr shaper; - std::unique_ptr shaperLut; - std::unique_ptr lut3D; - std::unique_ptr blend; - std::unique_ptr blendLut; + + // Ordered colorop chain discovered from KMS. Callers locate specific + // ops by DRM type + occurrence index via FindByType(), then bypass + // everything not explicitly programmed. This mirrors the + // search-by-type approach used in KWin and Weston and works for any + // vendor pipeline shape without vendor-specific enums. + std::vector> ops; + + // Return the nSkip-th (0-based) colorop whose DRM TYPE matches uType, + // or nullptr if no such op exists in the chain. + gamescope::CDRMColorOp *FindByType( uint64_t uType, size_t nSkip = 0 ) const + { + for ( const auto &pOp : ops ) + { + if ( pOp->GetProperties().TYPE.value().GetInitialValue() == uType ) + { + if ( nSkip == 0 ) + return pOp.get(); + --nSkip; + } + } + return nullptr; + } }; class CDRMFb final : public CBaseBackendFb @@ -970,32 +985,14 @@ static std::optional get_color_pipeline( struct dr } // Accept any non-empty pipeline regardless of shape. - // Named fields are assigned by matching the DRM colorop TYPE; any type not - // present in this pipeline leaves the field nullptr. The programming code - // null-checks each field before use so that non-AMD pipelines with fewer or - // different ops are handled gracefully rather than refused outright. + // The programming code uses FindByType() to locate the ops it cares + // about and bypasses everything else, making the logic generic for + // any hardware without a vendor-specific enum. if ( pipeline.empty() ) return {}; gamescope::CDRMColorPipeline p { .id = uHeadId }; - for ( auto &op : pipeline ) - { - uint64_t uType = op->GetProperties().TYPE.value().GetInitialValue(); - if ( uType == DRM_COLOROP_1D_CURVE ) - { - if ( !p.degamma ) { p.degamma = std::move(op); continue; } - else if ( !p.shaper ) { p.shaper = std::move(op); continue; } - else if ( !p.blend ) { p.blend = std::move(op); continue; } - } - else if ( uType == DRM_COLOROP_MULTIPLIER && !p.HDRMult ) { p.HDRMult = std::move(op); continue; } - else if ( uType == DRM_COLOROP_CTM_3X4 && !p.CTM ) { p.CTM = std::move(op); continue; } - else if ( uType == DRM_COLOROP_1D_LUT ) - { - if ( !p.shaperLut ) { p.shaperLut = std::move(op); continue; } - else if ( !p.blendLut ) { p.blendLut = std::move(op); continue; } - } - else if ( uType == DRM_COLOROP_3D_LUT && !p.lut3D ) { p.lut3D = std::move(op); continue; } - } + p.ops = std::move(pipeline); return p; } @@ -3073,100 +3070,134 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo shaper_tf = DRM_COLOROP_1D_CURVE_BT2020_OETF; } + // Generic color pipeline programming. + // + // Locate roles by DRM colorop type + occurrence index. + // Works for any hw pipeline shape (AMD, Intel, …) without + // a vendor-specific enum. After programming all known roles, + // every op not explicitly handled is bypassed. + gamescope::CDRMColorOp *pDegammaOp = p->FindByType( DRM_COLOROP_1D_CURVE ); // 1st 1D_CURVE + gamescope::CDRMColorOp *pHDRMultOp = p->FindByType( DRM_COLOROP_MULTIPLIER ); // MULTIPLIER + gamescope::CDRMColorOp *pCTMOp = p->FindByType( DRM_COLOROP_CTM_3X4 ); // CTM + gamescope::CDRMColorOp *pShaperOp = p->FindByType( DRM_COLOROP_1D_CURVE, 1 ); // 2nd 1D_CURVE + gamescope::CDRMColorOp *pShaperLutOp= p->FindByType( DRM_COLOROP_1D_LUT ); // 1st 1D_LUT + gamescope::CDRMColorOp *pLut3DOp = p->FindByType( DRM_COLOROP_3D_LUT ); // 3D_LUT + gamescope::CDRMColorOp *pBlendOp = p->FindByType( DRM_COLOROP_1D_CURVE, 2 ); // 3rd 1D_CURVE + gamescope::CDRMColorOp *pBlendLutOp = p->FindByType( DRM_COLOROP_1D_LUT, 1 ); // 2nd 1D_LUT + + // Track which ops we explicitly handle so we can bypass the rest. + std::unordered_set handledOps; + // --- Degamma (1st 1D_CURVE) --- - if ( p->degamma ) + if ( pDegammaOp ) { if ( !cv_drm_debug_disable_degamma_tf && degamma_tf.has_value() ) { - p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->degamma->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *degamma_tf, true ); + pDegammaOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pDegammaOp->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *degamma_tf, true ); } else { - p->degamma->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + pDegammaOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + handledOps.insert( pDegammaOp ); } // --- HDR multiplier (MULTIPLIER) --- - if ( p->HDRMult ) + if ( pHDRMultOp ) { - p->HDRMult->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->HDRMult->GetProperties().MULTIPLIER->SetPendingValue( drm->req, 0x100000000ULL, true ); + pHDRMultOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pHDRMultOp->GetProperties().MULTIPLIER->SetPendingValue( drm->req, 0x100000000ULL, true ); + handledOps.insert( pHDRMultOp ); } // --- Color transform matrix (CTM_3X4) --- - if ( p->CTM ) + if ( pCTMOp ) { if ( !cv_drm_debug_disable_ctm && frameInfo->layers[i].ctm != nullptr ) { - p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->CTM->GetProperties().DATA->SetPendingValue( drm->req, frameInfo->layers[i].ctm->GetBlobValue(), true ); + pCTMOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pCTMOp->GetProperties().DATA->SetPendingValue( drm->req, frameInfo->layers[i].ctm->GetBlobValue(), true ); } else { - p->CTM->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + pCTMOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + handledOps.insert( pCTMOp ); } - // --- Shaper curve (2nd 1D_CURVE) --- + // --- Shaper curve (2nd 1D_CURVE) + Shaper LUT (1st 1D_LUT) + 3D LUT --- bool bUseShaperAnd3DLUT = !cv_drm_debug_disable_shaper_and_3dlut && shaper_tf.has_value() - && p->shaperLut != nullptr - && p->lut3D != nullptr; - if ( p->shaper ) + && pLut3DOp != nullptr + && drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ] != nullptr + && drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ] != nullptr; + if ( pShaperOp ) { if ( bUseShaperAnd3DLUT ) { - p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->shaper->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *shaper_tf, true ); + pShaperOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pShaperOp->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *shaper_tf, true ); } else { - p->shaper->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + pShaperOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + handledOps.insert( pShaperOp ); } - if ( p->shaperLut ) + if ( pShaperLutOp ) { if ( bUseShaperAnd3DLUT ) { - p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->shaperLut->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + pShaperLutOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pShaperLutOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); } else { - p->shaperLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + pShaperLutOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + handledOps.insert( pShaperLutOp ); } - if ( p->lut3D ) + if ( pLut3DOp ) { if ( bUseShaperAnd3DLUT ) { - p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->lut3D->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + pLut3DOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pLut3DOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); } else { - p->lut3D->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + pLut3DOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + handledOps.insert( pLut3DOp ); } // --- Blend curve (3rd 1D_CURVE) --- - if ( p->blend ) + if ( pBlendOp ) { std::optional blend_tf = amd_tf_to_drm_curve( drm->pending.output_tf ); if ( !cv_drm_debug_disable_blend_tf && !bSinglePlane && blend_tf.has_value() ) { - p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - p->blend->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *blend_tf, true ); + pBlendOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pBlendOp->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *blend_tf, true ); } else { - p->blend->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + pBlendOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } + handledOps.insert( pBlendOp ); } - if ( p->blendLut ) + if ( pBlendLutOp ) + { + pBlendLutOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + handledOps.insert( pBlendLutOp ); + } + + // Bypass any op not covered by the roles above. + for ( const auto &pOp : p->ops ) { - p->blendLut->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + if ( handledOps.count( pOp.get() ) == 0 && pOp->GetProperties().BYPASS.has_value() ) + pOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); } break; From d8abde7607c7b6eb747a3404c128c195dfb7097d Mon Sep 17 00:00:00 2001 From: Naveen Kumar Date: Tue, 16 Jun 2026 11:29:30 +0000 Subject: [PATCH 18/18] drm: add 1D fallback path with CRTC pipe gamma for non-3D-LUT pipelines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For hardware that exposes a color pipeline without a 3D LUT (e.g. Intel Arc: 1D_LUT → CTM_3X4 → 1D_LUT), add a fallback path that provides correct HDR and SDR color output: - Plane 1D_LUT (1st, degamma): EOTF decode (sRGB→linear or PQ→linear) - Plane CTM_3X4 (gamut): BT.709→BT.2020 when HDR output and BT.709 source content; bypassed otherwise - Plane 1D_LUT (2nd, POST_CSC): always bypassed - CRTC GAMMA_LUT (pipe gamma): OETF encode (linear→PQ for HDR, linear→sRGB for SDR) applied after all planes are composited The OETF is applied at the CRTC level rather than per-plane so that it fires once after blending — matching the offload-blend-to-output design in Weston. On AMD the CRTC GAMMA_LUT is left at 0 (AMD uses AMD_CRTC_REGAMMA_TF instead). The auto-gamut CTM (BT.709→BT.2020) is gated on pLut3DOp == nullptr so it is never injected on AMD where the 3D LUT already encodes the full gamut+OETF transform. Implementation details: - CDRMColorOp gains a SIZE property so that 1D LUT blobs are generated at the exact entry count the driver expects (Intel: 128 degamma, 1024 GAMMA_LUT) rather than a hardcoded AMD count. - CDRMCRTC gains a GAMMA_LUT_SIZE property read at RefreshState time. - drm_state_t gains gamut_ctm_colorop_id (cached BT.709→BT.2020 blob) and pipe_gamma_id (CRTC GAMMA_LUT blob); pipegamma_colorop_id is removed. - make_pipe_gamma_blob() generates drm_color_lut (U0.16) blobs for the CRTC GAMMA_LUT, distinct from the U0.32 colorop blobs. - CRTC GAMMA_LUT is set after drm_prepare_liftoff() returns, once pipe_gamma_id is known. Assisted-by: Claude:claude-sonnet-4.6 Signed-off-by: Naveen Kumar --- src/Backends/DRMBackend.cpp | 287 ++++++++++++++++++++++++++++++------ 1 file changed, 242 insertions(+), 45 deletions(-) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 2701892a40..55b05ab42b 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -137,6 +137,16 @@ struct drm_t { std::shared_ptr shaperlut_id[ EOTF_Count ]; std::shared_ptr lut3d_colorop_id[ EOTF_Count ]; std::shared_ptr shaperlut_colorop_id[ EOTF_Count ]; + // Degamma (EOTF) LUT blob for the Intel/simple fallback path: + // 1D_LUT(degamma) → CTM → CRTC GAMMA_LUT (pipe gamma). + // Pure-math curve, independent of the 3D LUT path. + std::shared_ptr degamma_colorop_id[ EOTF_Count ]; + // BT.709→BT.2020 gamut mapping CTM for Intel fallback path when HDR output + // is active. Shared across planes/frames; invalidated on color_mgmt_serial change. + std::shared_ptr gamut_ctm_colorop_id; + // CRTC pipe gamma LUT blob for the Intel fallback path (set instead of plane POST_CSC_LUT). + // Uses drm_color_lut (16-bit per channel) at GAMMA_LUT_SIZE entries. + std::shared_ptr pipe_gamma_id; amdgpu_transfer_function output_tf = AMDGPU_TRANSFER_FUNCTION_DEFAULT; } current, pending; @@ -356,6 +366,7 @@ namespace gamescope std::optional ACTIVE; std::optional MODE_ID; std::optional GAMMA_LUT; + std::optional GAMMA_LUT_SIZE; std::optional DEGAMMA_LUT; std::optional CTM; std::optional VRR_ENABLED; @@ -552,6 +563,7 @@ namespace gamescope { std::optional TYPE; // Immutable std::optional NEXT; // Immutable + std::optional SIZE; // Immutable: number of LUT entries std::optional BYPASS; std::optional CURVE_1D_TYPE; std::optional DATA; @@ -563,15 +575,14 @@ namespace gamescope ColorOpProperties m_Props; }; + // Generic color pipeline: owns the ordered colorop chain discovered from KMS. + // Callers locate specific ops by DRM type + occurrence index via FindByType(), + // then bypass every op that is not explicitly programmed. This mirrors the + // search-by-type approach used in KWin and Weston and works for any vendor + // pipeline shape (AMD, Intel, …) without a vendor-specific enum. struct CDRMColorPipeline { uint32_t id; - - // Ordered colorop chain discovered from KMS. Callers locate specific - // ops by DRM type + occurrence index via FindByType(), then bypass - // everything not explicitly programmed. This mirrors the - // search-by-type approach used in KWin and Weston and works for any - // vendor pipeline shape without vendor-specific enums. std::vector> ops; // Return the nSkip-th (0-based) colorop whose DRM TYPE matches uType, @@ -589,6 +600,8 @@ namespace gamescope } return nullptr; } + + bool HasType( uint64_t uType ) const { return FindByType( uType ) != nullptr; } }; class CDRMFb final : public CBaseBackendFb @@ -973,7 +986,12 @@ static bool refresh_state( drm_t *drm ) static std::optional get_color_pipeline( struct drm_t *drm, uint32_t uHeadId ) { - std::vector> pipeline; + // Build the ordered colorop chain from the linked list starting at uHeadId. + // No shape validation here – the pipeline is accepted as long as it is + // non-empty. The programming code uses FindByType() to locate the ops it + // cares about and bypasses everything else, making the logic generic for any + // hardware (AMD, Intel, …) without a vendor-specific enum. + std::vector> ops; uint32_t uColorOpId = uHeadId; while ( uColorOpId != 0 ) @@ -981,18 +999,13 @@ static std::optional get_color_pipeline( struct dr auto pColorOp = std::make_unique( uColorOpId ); pColorOp->RefreshState(); uColorOpId = pColorOp->GetProperties().NEXT.value().GetInitialValue(); - pipeline.emplace_back( std::move(pColorOp) ); + ops.emplace_back( std::move(pColorOp) ); } - // Accept any non-empty pipeline regardless of shape. - // The programming code uses FindByType() to locate the ops it cares - // about and bypasses everything else, making the logic generic for - // any hardware without a vendor-specific enum. - if ( pipeline.empty() ) + if ( ops.empty() ) return {}; - gamescope::CDRMColorPipeline p { .id = uHeadId }; - p.ops = std::move(pipeline); + gamescope::CDRMColorPipeline p{ .id = uHeadId, .ops = std::move(ops) }; return p; } @@ -1011,6 +1024,9 @@ static std::optional get_plane_color_pipelines( st for ( int i = 0; i < pProperty->count_enums; i++ ) { auto entry = pProperty->enums[ i ]; + if ( entry.value == 0 ) + continue; + std::optional p = get_color_pipeline( drm, entry.value ); if ( p.has_value() ) return p; @@ -1060,8 +1076,8 @@ static bool get_resources(struct drm_t *drm) for ( std::unique_ptr< gamescope::CDRMPlane > &pPlane : drm->planes ) { // AMD Cursor plane doesn't support color management - if ( pPlane->GetProperties().type->GetCurrentValue() != DRM_PLANE_TYPE_CURSOR && !get_plane_color_pipelines( drm, pPlane ) ) - return false; + if ( pPlane->GetProperties().type->GetCurrentValue() != DRM_PLANE_TYPE_CURSOR ) + get_plane_color_pipelines( drm, pPlane ); } } @@ -2310,6 +2326,7 @@ namespace gamescope m_Props.ACTIVE = CDRMAtomicProperty::Instantiate( "ACTIVE", this, *rawProperties ); m_Props.MODE_ID = CDRMAtomicProperty::Instantiate( "MODE_ID", this, *rawProperties ); m_Props.GAMMA_LUT = CDRMAtomicProperty::Instantiate( "GAMMA_LUT", this, *rawProperties ); + m_Props.GAMMA_LUT_SIZE = CDRMAtomicProperty::Instantiate( "GAMMA_LUT_SIZE", this, *rawProperties ); m_Props.DEGAMMA_LUT = CDRMAtomicProperty::Instantiate( "DEGAMMA_LUT", this, *rawProperties ); m_Props.CTM = CDRMAtomicProperty::Instantiate( "CTM", this, *rawProperties ); m_Props.VRR_ENABLED = CDRMAtomicProperty::Instantiate( "VRR_ENABLED", this, *rawProperties ); @@ -2784,6 +2801,7 @@ namespace gamescope { m_Props.TYPE = CDRMAtomicProperty::Instantiate( "TYPE", this, *rawProperties ); m_Props.NEXT = CDRMAtomicProperty::Instantiate( "NEXT", this, *rawProperties ); + m_Props.SIZE = CDRMAtomicProperty::Instantiate( "SIZE", this, *rawProperties ); m_Props.BYPASS = CDRMAtomicProperty::Instantiate( "BYPASS", this, *rawProperties ); m_Props.DATA = CDRMAtomicProperty::Instantiate( "DATA", this, *rawProperties ); m_Props.CURVE_1D_TYPE = CDRMAtomicProperty::Instantiate( "CURVE_1D_TYPE", this, *rawProperties ); @@ -2808,6 +2826,68 @@ namespace gamescope } } +// Build a nSize-entry 1D LUT blob (drm_color_lut32 layout: R,G,B,pad per +// entry in U0.32) from an arbitrary per-channel transfer function. The same +// function is applied to R, G and B (monotone/gamma-style curves only). +// nSize must match the colorop's SIZE property reported by the kernel driver. +template +static std::vector make_curve_lut_32bit( Fn fn, uint32_t nSize ) +{ + std::vector out( nSize * 4, 0 ); + for ( uint32_t i = 0; i < nSize; i++ ) + { + float x = ( nSize > 1 ) ? (float)i / (float)( nSize - 1 ) : 0.0f; + float y = std::clamp( fn( x ), 0.0f, 1.0f ); + // U0.32: 0xFFFFFFFF == 1.0 + uint32_t v = (uint32_t)( (double)y * 4294967295.0 + 0.5 ); + out[ i*4 + 0 ] = v; // R + out[ i*4 + 1 ] = v; // G + out[ i*4 + 2 ] = v; // B + out[ i*4 + 3 ] = 0; // padding + } + return out; +} + +// Helper to create a BackendBlob for a curve LUT sized to match the given colorop's SIZE property. +// Returns nullptr if the SIZE property is not available. +template +static std::shared_ptr make_colorop_curve_blob( const gamescope::CDRMColorOp *pOp, Fn fn ) +{ + if ( !pOp || !pOp->GetProperties().SIZE.has_value() ) + return nullptr; + uint32_t nSize = (uint32_t)pOp->GetProperties().SIZE->GetInitialValue(); + if ( nSize == 0 ) + return nullptr; + auto lut = make_curve_lut_32bit( fn, nSize ); + const uint8_t *pBegin = reinterpret_cast( lut.data() ); + const uint8_t *pEnd = pBegin + lut.size() * sizeof( uint32_t ); + return GetBackend()->CreateBackendBlob( typeid( lut ), std::span( pBegin, pEnd ) ); +} + +// Helper to create a drm_color_lut (16-bit/channel) blob for the CRTC GAMMA_LUT property. +// nSize is read from the GAMMA_LUT_SIZE immutable property. +template +static std::shared_ptr make_pipe_gamma_blob( uint32_t nSize, Fn fn ) +{ + if ( nSize == 0 ) + return nullptr; + std::vector lut( nSize ); + for ( uint32_t i = 0; i < nSize; i++ ) + { + float x = ( nSize > 1 ) ? (float)i / (float)( nSize - 1 ) : 0.0f; + float y = std::clamp( fn( x ), 0.0f, 1.0f ); + // U0.16: 0xFFFF == 1.0 + uint16_t v = (uint16_t)( y * 65535.0f + 0.5f ); + lut[i].red = v; + lut[i].green = v; + lut[i].blue = v; + lut[i].reserved = 0; + } + const uint8_t *pBegin = reinterpret_cast( lut.data() ); + const uint8_t *pEnd = pBegin + lut.size() * sizeof( drm_color_lut ); + return GetBackend()->CreateBackendBlob( typeid( lut ), std::span( pBegin, pEnd ) ); +} + static int drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, bool needs_modeset ) { @@ -3045,37 +3125,36 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo if ( pPlane->GetObjectId() != plane_id ) continue; + auto pColorPipelineProp = pPlane->GetProperties().COLOR_PIPELINE; + if ( !pColorPipelineProp ) + break; + if ( !frameInfo->layers[i].applyColorMgmt ) { - pPlane->GetProperties().COLOR_PIPELINE->SetPendingValue( drm->req, 0, true ); + pColorPipelineProp->SetPendingValue( drm->req, 0, true ); break; } bool bYCbCr = entry.layerState[i].ycbcr; std::optional p = get_plane_color_pipelines( drm, pPlane ); - if ( !p ) { - drm_log.debugf( "drm_prepare_liftoff: No color pipeline fits color mgmt needs of plane_id %d", plane_id ); + if ( !p ) break; - } pPlane->GetProperties().COLOR_PIPELINE->SetPendingValue( drm->req, p->id, true ); - GamescopeAppTextureColorspace colorspace = entry.layerState[i].colorspace; - std::optional degamma_tf = colorspace_to_drm_plane_degamma_curve( colorspace ); - std::optional shaper_tf = colorspace_to_drm_plane_shaper_curve( colorspace ); - - if ( bYCbCr ) - { - degamma_tf = DRM_COLOROP_1D_CURVE_BT2020_INV_OETF; - shaper_tf = DRM_COLOROP_1D_CURVE_BT2020_OETF; - } - // Generic color pipeline programming. // - // Locate roles by DRM colorop type + occurrence index. - // Works for any hw pipeline shape (AMD, Intel, …) without - // a vendor-specific enum. After programming all known roles, - // every op not explicitly handled is bypassed. + // Locate roles by DRM colorop type + occurrence index (same pattern as + // KWin / Weston). This works for any hw pipeline shape: + // AMD : 1D_CURVE→MULT→CTM→1D_CURVE→1D_LUT→3D_LUT→1D_CURVE→1D_LUT + // Intel: supports 1D_CURVE, 1D_LUT, CTM_3X4, MULTIPLIER, 3D_LUT + // (the actual chain depends on what the kernel exposes) + // + // After programming all known roles, every op not explicitly handled + // is bypassed so the hw state is always consistent. + + // Resolve role→op mappings. Roles that map to ops not present in this + // pipeline are simply nullptr and skipped below. gamescope::CDRMColorOp *pDegammaOp = p->FindByType( DRM_COLOROP_1D_CURVE ); // 1st 1D_CURVE gamescope::CDRMColorOp *pHDRMultOp = p->FindByType( DRM_COLOROP_MULTIPLIER ); // MULTIPLIER gamescope::CDRMColorOp *pCTMOp = p->FindByType( DRM_COLOROP_CTM_3X4 ); // CTM @@ -3085,13 +3164,27 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo gamescope::CDRMColorOp *pBlendOp = p->FindByType( DRM_COLOROP_1D_CURVE, 2 ); // 3rd 1D_CURVE gamescope::CDRMColorOp *pBlendLutOp = p->FindByType( DRM_COLOROP_1D_LUT, 1 ); // 2nd 1D_LUT + GamescopeAppTextureColorspace colorspace = entry.layerState[i].colorspace; + std::optional degamma_tf = colorspace_to_drm_plane_degamma_curve( colorspace ); + std::optional shaper_tf = colorspace_to_drm_plane_shaper_curve( colorspace ); + + if ( bYCbCr ) + { + degamma_tf = DRM_COLOROP_1D_CURVE_BT2020_INV_OETF; + shaper_tf = DRM_COLOROP_1D_CURVE_BT2020_OETF; + } + // Track which ops we explicitly handle so we can bypass the rest. std::unordered_set handledOps; + // True when the output requires HDR encoding (PQ / BT.2020). + const bool bHDROutput = g_bOutputHDREnabled; + // --- Degamma (1st 1D_CURVE) --- if ( pDegammaOp ) { - if ( !cv_drm_debug_disable_degamma_tf && degamma_tf.has_value() ) + bool bUseDegamma = !cv_drm_debug_disable_degamma_tf && degamma_tf.has_value(); + if ( bUseDegamma ) { pDegammaOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); pDegammaOp->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *degamma_tf, true ); @@ -3114,24 +3207,96 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo // --- Color transform matrix (CTM_3X4) --- if ( pCTMOp ) { + bool bSetCTM = false; if ( !cv_drm_debug_disable_ctm && frameInfo->layers[i].ctm != nullptr ) { + // Explicit layer CTM (e.g. scRGB BT.709→BT.2020 set by steamcompmgr). pCTMOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); pCTMOp->GetProperties().DATA->SetPendingValue( drm->req, frameInfo->layers[i].ctm->GetBlobValue(), true ); + bSetCTM = true; } - else + + if ( !bSetCTM && !cv_drm_debug_disable_ctm && bHDROutput + && pLut3DOp == nullptr + && ( colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_LINEAR || + colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SRGB || + colorspace == GAMESCOPE_APP_TEXTURE_COLORSPACE_SCRGB ) ) { - pCTMOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); + // For HDR output with BT.709 content on pipelines without a 3D LUT + // (e.g. Intel), apply a gamut-mapping matrix (BT.709 → BT.2020) so + // the blended result is in the display's native BT.2020 colour space + // before the OETF. On AMD the 3D LUT already encodes this transform. + if ( !drm->pending.gamut_ctm_colorop_id ) + drm->pending.gamut_ctm_colorop_id = GetBackend()->CreateBackendBlob( + glm::mat3x4( glm::transpose( k_2020_from_709 ) ) ); + if ( drm->pending.gamut_ctm_colorop_id ) + { + pCTMOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pCTMOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.gamut_ctm_colorop_id->GetBlobValue(), true ); + bSetCTM = true; + } } + + if ( !bSetCTM ) + pCTMOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); handledOps.insert( pCTMOp ); } // --- Shaper curve (2nd 1D_CURVE) + Shaper LUT (1st 1D_LUT) + 3D LUT --- + // Shaper+3DLUT only makes sense when ALL three ops are present in the pipeline + // and the LUT blobs have been computed. On pipelines without a 3D LUT (e.g. + // Intel's 1D_LUT → CTM → 1D_LUT) enabling this path would send an AMD-format + // blob to the wrong colorop type and cause EINVAL from drmModeAtomicCommit. + const uint32_t uEOTFIdx = ColorSpaceToEOTFIndex( colorspace ); bool bUseShaperAnd3DLUT = !cv_drm_debug_disable_shaper_and_3dlut && shaper_tf.has_value() && pLut3DOp != nullptr - && drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ] != nullptr - && drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ] != nullptr; + && drm->pending.shaperlut_colorop_id[ uEOTFIdx ] != nullptr + && drm->pending.lut3d_colorop_id[ uEOTFIdx ] != nullptr; + + // Fallback for pipelines without a 3D LUT (e.g. Intel 1D_LUT → CTM → 1D_LUT): + // use the first 1D_LUT as a degamma (EOTF), the CTM for gamut mapping, and the + // CRTC GAMMA_LUT (pipe gamma) for the OETF. The plane POST_CSC_LUT is bypassed. + // This is the preferred path: pipe gamma fires after all planes are blended, + // which is correct for multi-plane HDR composition. + // + // Degamma blob is sized to the colorop's SIZE property. + // Pipe gamma blob uses drm_color_lut (16-bit) at GAMMA_LUT_SIZE entries. + // Both are regenerated whenever bHDROutput may have changed. + if ( pLut3DOp == nullptr && !bUseShaperAnd3DLUT + && !cv_drm_debug_disable_degamma_tf && degamma_tf.has_value() ) + { + if ( !drm->pending.degamma_colorop_id[ uEOTFIdx ] && pShaperLutOp ) + { + if ( uEOTFIdx == EOTF_Gamma22 ) + drm->pending.degamma_colorop_id[ uEOTFIdx ] = make_colorop_curve_blob( pShaperLutOp, srgb_to_linear ); + else if ( uEOTFIdx == EOTF_PQ ) + drm->pending.degamma_colorop_id[ uEOTFIdx ] = make_colorop_curve_blob( pShaperLutOp, []( float x ) { + return pq_to_nits( x ) / 10000.0f; + } ); + } + // Pipe gamma blob for the CRTC GAMMA_LUT. Always regenerate so that HDR + // mode toggles (which don't bump g_ColorMgmt.serial) are reflected. + if ( drm->pCRTC && drm->pCRTC->GetProperties().GAMMA_LUT_SIZE ) + { + uint32_t nGammaSize = (uint32_t)drm->pCRTC->GetProperties().GAMMA_LUT_SIZE->GetInitialValue(); + if ( uEOTFIdx == EOTF_Gamma22 ) + drm->pending.pipe_gamma_id = make_pipe_gamma_blob( nGammaSize, + bHDROutput ? []( float x ) { return nits_to_pq( x * 10000.0f ); } + : static_cast(linear_to_srgb) ); + else if ( uEOTFIdx == EOTF_PQ ) + drm->pending.pipe_gamma_id = make_pipe_gamma_blob( nGammaSize, []( float x ) { + return nits_to_pq( x * 10000.0f ); + } ); + } + } + const bool bUseDegammaPipeGamma = !bUseShaperAnd3DLUT + && pLut3DOp == nullptr + && !cv_drm_debug_disable_degamma_tf + && degamma_tf.has_value() + && drm->pending.degamma_colorop_id[ uEOTFIdx ] != nullptr + && drm->pending.pipe_gamma_id != nullptr; + if ( pShaperOp ) { if ( bUseShaperAnd3DLUT ) @@ -3150,7 +3315,13 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo if ( bUseShaperAnd3DLUT ) { pShaperLutOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - pShaperLutOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.shaperlut_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + pShaperLutOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.shaperlut_colorop_id[ uEOTFIdx ]->GetBlobValue(), true ); + } + else if ( bUseDegammaPipeGamma ) + { + // Intel fallback: 1st 1D_LUT acts as degamma (EOTF). + pShaperLutOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); + pShaperLutOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.degamma_colorop_id[ uEOTFIdx ]->GetBlobValue(), true ); } else { @@ -3163,7 +3334,7 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo if ( bUseShaperAnd3DLUT ) { pLut3DOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); - pLut3DOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.lut3d_colorop_id[ ColorSpaceToEOTFIndex( colorspace ) ]->GetBlobValue(), true ); + pLut3DOp->GetProperties().DATA->SetPendingValue( drm->req, drm->pending.lut3d_colorop_id[ uEOTFIdx ]->GetBlobValue(), true ); } else { @@ -3172,11 +3343,13 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo handledOps.insert( pLut3DOp ); } - // --- Blend curve (3rd 1D_CURVE) --- + // --- Blend curve (3rd 1D_CURVE) + blend LUT (2nd 1D_LUT) --- + // When using the CRTC pipe gamma path (bUseDegammaPipeGamma) the OETF is applied + // by the CRTC GAMMA_LUT after blending, so the plane's POST_CSC_LUT is bypassed. if ( pBlendOp ) { std::optional blend_tf = amd_tf_to_drm_curve( drm->pending.output_tf ); - if ( !cv_drm_debug_disable_blend_tf && !bSinglePlane && blend_tf.has_value() ) + if ( !cv_drm_debug_disable_blend_tf && !bSinglePlane && blend_tf.has_value() && !bUseDegammaPipeGamma ) { pBlendOp->GetProperties().BYPASS->SetPendingValue( drm->req, 0, true ); pBlendOp->GetProperties().CURVE_1D_TYPE->SetPendingValue( drm->req, *blend_tf, true ); @@ -3189,11 +3362,14 @@ drm_prepare_liftoff( struct drm_t *drm, const struct FrameInfo_t *frameInfo, boo } if ( pBlendLutOp ) { + // Always bypass POST_CSC_LUT on the plane; the OETF is applied by + // the CRTC GAMMA_LUT (pipe gamma) after blending. pBlendLutOp->GetProperties().BYPASS->SetPendingValue( drm->req, 1, true ); handledOps.insert( pBlendLutOp ); } - // Bypass any op not covered by the roles above. + // Bypass any op in the pipeline not covered by the roles above + // (e.g. extra hw stages the current gamescope logic does not use). for ( const auto &pOp : p->ops ) { if ( handledOps.count( pOp.get() ) == 0 && pOp->GetProperties().BYPASS.has_value() ) @@ -3475,6 +3651,19 @@ int drm_prepare( struct drm_t *drm, bool async, const struct FrameInfo_t *frameI ret = 0; } + // Set CRTC GAMMA_LUT for the Intel fallback path (pipe gamma after blending). + // pipe_gamma_id is populated by drm_prepare_liftoff, so this must come after. + // For AMD the regamma TF is used instead; only set when color pipeline is supported + // but not the AMD-specific color management. + if ( ret == 0 && drm->pCRTC && !bSleep + && drm->pCRTC->GetProperties().GAMMA_LUT + && drm_supports_color_pipeline( drm ) + && !drm_supports_color_mgmt( drm ) ) + { + uint64_t uGammaBlob = drm->pending.pipe_gamma_id ? drm->pending.pipe_gamma_id->GetBlobValue() : 0; + drm->pCRTC->GetProperties().GAMMA_LUT->SetPendingValue( drm->req, uGammaBlob, bForceInRequest ); + } + if ( ret != 0 ) { drm_rollback( drm ); @@ -3617,7 +3806,10 @@ bool drm_update_color_mgmt(struct drm_t *drm) drm->pending.lut3d_id[ i ] = 0; drm->pending.shaperlut_colorop_id[ i ] = 0; drm->pending.lut3d_colorop_id[ i ] = 0; + drm->pending.degamma_colorop_id[ i ] = 0; } + drm->pending.gamut_ctm_colorop_id = nullptr; + drm->pending.pipe_gamma_id = nullptr; for ( uint32_t i = 0; i < EOTF_Count; i++ ) { @@ -3631,6 +3823,11 @@ bool drm_update_color_mgmt(struct drm_t *drm) drm->pending.lut3d_colorop_id[ i ] = GetBackend()->CreateBackendBlob( lut_convert_16bit_to_32bit( g_ColorMgmtLuts[i].lut3d ) ); } + // degamma_colorop_id for the Intel fallback path is generated on-demand + // in drm_prepare_liftoff using make_colorop_curve_blob() (size matches + // each colorop's SIZE property). pipe_gamma_id is also generated on-demand + // using make_pipe_gamma_blob() at GAMMA_LUT_SIZE entries. + return true; }