diff --git a/scripts/00-gamescope/displays/lenovo.legiongo2.oled.lua b/scripts/00-gamescope/displays/lenovo.legiongo2.oled.lua new file mode 100644 index 0000000000..805b833ad2 --- /dev/null +++ b/scripts/00-gamescope/displays/lenovo.legiongo2.oled.lua @@ -0,0 +1,69 @@ +local legiongo2_oled_refresh_rates = { + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, + 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, + 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, + 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, + 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, + 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, + 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, + 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, + 138, 139, 140, 141, 142, 143, 144 +} + +gamescope.config.known_displays.lenovo_legiongo2_oled = { + pretty_name = "Lenovo Legion Go 2 OLED", + hdr = { + supported = true, + force_enabled = false, + eotf = gamescope.eotf.pq, + content_driven = true, + }, + dynamic_refresh_rates = legiongo2_oled_refresh_rates, + dynamic_modegen = function(base_mode, refresh) + debug("Generating mode "..refresh.."Hz for Lenovo Legion Go 2 OLED with fixed pixel clock") + local vfps = { + 2696, 2615, 2538, 2463, 2391, + 2322, 2256, 2192, 2130, 2071, + 2013, 1958, 1904, 1852, 1802, + 1753, 1706, 1660, 1616, 1572, + 1531, 1491, 1451, 1413, 1376, + 1340, 1305, 1270, 1237, 1205, + 1173, 1142, 1112, 1083, 1054, + 1026, 999, 972, 946, 921, + 896, 872, 848, 825, 802, + 780, 758, 737, 716, 696, + 676, 656, 637, 618, 600, + 581, 564, 546, 529, 512, + 496, 480, 464, 448, 433, + 418, 403, 389, 375, 361, + 347, 333, 320, 307, 294, + 281, 269, 257, 245, 233, + 221, 209, 198, 187, 176, + 165, 155, 144, 134, 123, + 113, 103, 94, 84, 75, + 65, 56 + } + local vfp = vfps[zero_index(refresh - 48)] + if vfp == nil then + warn("Couldn't do refresh "..refresh.." on Lenovo Legion Go 2 OLED") + return base_mode + end + + local mode = base_mode + + gamescope.modegen.adjust_front_porch(mode, vfp) + mode.vrefresh = gamescope.modegen.calc_vrefresh(mode) + + return mode + end, + matches = function(display) + if display.vendor == "SDC" and display.product == 17153 then + debug("[lenovo_legiongo2_oled] Matched vendor: "..display.vendor.." product: "..display.product) + return 5000 + end + return -1 + end, +} +debug("Registered Lenovo Legion Go 2 OLED as a known display") +--debug(inspect(gamescope.config.known_displays.lenovo_legiongo2_oled)) diff --git a/src/Backends/DRMBackend.cpp b/src/Backends/DRMBackend.cpp index 1d34eba030..560a78c134 100644 --- a/src/Backends/DRMBackend.cpp +++ b/src/Backends/DRMBackend.cpp @@ -2297,6 +2297,9 @@ namespace gamescope bool bHasKnownColorimetry = false; bool bHasKnownHDRInfo = false; + bool bHasKnownMaxCLL = false; + bool bHasKnownMaxFALL = false; + bool bHasKnownMinCLL = false; m_Mutable.ValidDynamicRefreshRates.clear(); m_Mutable.fnDynamicModeGenerator = nullptr; @@ -2392,9 +2395,23 @@ namespace gamescope { m_Mutable.HDR.bExposeHDRSupport = otHDRInfo->get_or( "supported", false ); m_Mutable.HDR.eOutputEncodingEOTF = otHDRInfo->get_or( "eotf", EOTF_Gamma22 ); - m_Mutable.HDR.uMaxContentLightLevel = nits_to_u16( otHDRInfo->get_or( "max_content_light_level", 400.0f ) ); - m_Mutable.HDR.uMaxFrameAverageLuminance = nits_to_u16( otHDRInfo->get_or( "max_frame_average_luminance", 400.0f ) ); - m_Mutable.HDR.uMinContentLightLevel = nits_to_u16_dark( otHDRInfo->get_or( "min_content_light_level", 0.1f ) ); + m_Mutable.HDR.bContentDrivenHDR = otHDRInfo->get_or( "content_driven", false ); + + if ( sol::optional ofMaxCLL = (*otHDRInfo)["max_content_light_level"] ) + { + m_Mutable.HDR.uMaxContentLightLevel = nits_to_u16( *ofMaxCLL ); + bHasKnownMaxCLL = true; + } + if ( sol::optional ofMaxFALL = (*otHDRInfo)["max_frame_average_luminance"] ) + { + m_Mutable.HDR.uMaxFrameAverageLuminance = nits_to_u16( *ofMaxFALL ); + bHasKnownMaxFALL = true; + } + if ( sol::optional ofMinCLL = (*otHDRInfo)["min_content_light_level"] ) + { + m_Mutable.HDR.uMinContentLightLevel = nits_to_u16_dark( *ofMinCLL ); + bHasKnownMinCLL = true; + } bHasKnownHDRInfo = true; } @@ -2460,7 +2477,10 @@ namespace gamescope ///////////////////// // Parse HDR stuff. ///////////////////// - if ( !bHasKnownHDRInfo ) + if ( !bHasKnownHDRInfo + || !bHasKnownMaxCLL + || !bHasKnownMaxFALL + || !bHasKnownMinCLL ) { const di_cta_hdr_static_metadata_block *pHDRStaticMetadata = nullptr; const di_cta_colorimetry_block *pColorimetry = nullptr; @@ -2495,22 +2515,28 @@ namespace gamescope if ( pColorimetry && pColorimetry->bt2020_rgb && pHDRStaticMetadata && pHDRStaticMetadata->eotfs && pHDRStaticMetadata->eotfs->pq ) { - m_Mutable.HDR.bExposeHDRSupport = true; - m_Mutable.HDR.eOutputEncodingEOTF = EOTF_PQ; - m_Mutable.HDR.uMaxContentLightLevel = - pHDRStaticMetadata->desired_content_max_luminance - ? nits_to_u16( pHDRStaticMetadata->desired_content_max_luminance ) - : nits_to_u16( 1499.0f ); - m_Mutable.HDR.uMaxFrameAverageLuminance = - pHDRStaticMetadata->desired_content_max_frame_avg_luminance - ? nits_to_u16( pHDRStaticMetadata->desired_content_max_frame_avg_luminance ) - : nits_to_u16( std::min( 799.f, nits_from_u16( m_Mutable.HDR.uMaxContentLightLevel ) ) ); - m_Mutable.HDR.uMinContentLightLevel = - pHDRStaticMetadata->desired_content_min_luminance - ? nits_to_u16_dark( pHDRStaticMetadata->desired_content_min_luminance ) - : nits_to_u16_dark( 0.0f ); + if ( !bHasKnownHDRInfo ) + { + m_Mutable.HDR.bExposeHDRSupport = true; + m_Mutable.HDR.eOutputEncodingEOTF = EOTF_PQ; + } + if ( !bHasKnownMaxCLL ) + m_Mutable.HDR.uMaxContentLightLevel = + pHDRStaticMetadata->desired_content_max_luminance + ? nits_to_u16( pHDRStaticMetadata->desired_content_max_luminance ) + : nits_to_u16( 1499.0f ); + if ( !bHasKnownMaxFALL ) + m_Mutable.HDR.uMaxFrameAverageLuminance = + pHDRStaticMetadata->desired_content_max_frame_avg_luminance + ? nits_to_u16( pHDRStaticMetadata->desired_content_max_frame_avg_luminance ) + : nits_to_u16( std::min( 799.f, nits_from_u16( m_Mutable.HDR.uMaxContentLightLevel ) ) ); + if ( !bHasKnownMinCLL ) + m_Mutable.HDR.uMinContentLightLevel = + pHDRStaticMetadata->desired_content_min_luminance + ? nits_to_u16_dark( pHDRStaticMetadata->desired_content_min_luminance ) + : nits_to_u16_dark( 0.0f ); } - else + else if ( !bHasKnownHDRInfo ) { m_Mutable.HDR.bExposeHDRSupport = false; } diff --git a/src/backend.h b/src/backend.h index 607c25d9a0..d739a0dd12 100644 --- a/src/backend.h +++ b/src/backend.h @@ -123,6 +123,9 @@ namespace gamescope // For displays doing "traditional HDR" such as Steam Deck OLED, this is Gamma 2.2. EOTF eOutputEncodingEOTF = EOTF_Gamma22; + // Only drive a panel in HDR while an HDR app is running. + bool bContentDrivenHDR = false; + uint16_t uMaxContentLightLevel = 500; // Nits uint16_t uMaxFrameAverageLuminance = 500; // Nits uint16_t uMinContentLightLevel = 0; // Nits / 10000 diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index 7250cc50f8..dc8bac7491 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -455,6 +455,7 @@ bool g_bVRRInUse_CachedValue = false; bool g_bSupportsHDR_CachedValue = false; bool g_bForceHDR10OutputDebug = false; gamescope::ConVar cv_hdr_enabled{ "hdr_enabled", false, "Whether or not HDR is enabled if it is available." }; +gamescope::ConVar cv_hdr_content_driven{ "hdr_content_driven", false, "Only drive a panel in HDR while an HDR app is running." }; bool g_bHDRItmEnable = false; int g_nCurrentRefreshRate_CachedValue = 0; @@ -884,6 +885,7 @@ global_focus_t *GetCurrentFocus() uint32_t currentOutputWidth, currentOutputHeight; int currentOutputRefresh; bool currentHDROutput = false; +bool currentHDRCapable = false; bool currentHDRForce = false; std::vector< uint32_t > vecFocuscontrolAppIDs; @@ -8541,7 +8543,36 @@ steamcompmgr_main(int argc, char **argv) g_uCompositeDebug = cv_composite_debug; - g_bOutputHDREnabled = (g_bSupportsHDR_CachedValue || g_bForceHDR10OutputDebug) && cv_hdr_enabled; + // Capability is advertised to clients regardless of the live "active" + // state below, so games can request HDR while the panel is idling in SDR. + const bool bOutputHDRCapable = (g_bSupportsHDR_CachedValue || g_bForceHDR10OutputDebug) && cv_hdr_enabled; + + { + gamescope::IBackendConnector *pConn = GetBackend()->GetCurrentConnector(); + const bool bDynamic = bOutputHDRCapable && + ( cv_hdr_content_driven || + ( pConn && pConn->GetHDRInfo().bContentDrivenHDR ) ); + + bool bActive = bOutputHDRCapable; + if ( bDynamic ) + { + // Track any live window, not just the focused one, so switching + // to the Steam UI over a running HDR game keeps the panel in HDR. + bActive = false; + for ( steamcompmgr_win_t *pWin : GetGlobalPossibleFocusWindows() ) + { + commit_t *pCommit = get_window_last_done_commit_peek( pWin ); + if ( pCommit && ColorspaceIsHDR( pCommit->colorspace() ) ) + { + bActive = true; + break; + } + } + } + if ( bActive != g_bOutputHDREnabled ) + xwm_log.infof( "HDR output %s%s", bActive ? "enabled" : "disabled", bDynamic ? " (hdr_content_driven)" : "" ); + g_bOutputHDREnabled = bActive; + } // Pick our width/height for this potential frame, regardless of how it might change later // At some point we might even add proper locking so we get real updates atomically instead @@ -8550,6 +8581,7 @@ steamcompmgr_main(int argc, char **argv) currentOutputHeight != g_nOutputHeight || currentOutputRefresh != g_nOutputRefresh || currentHDROutput != g_bOutputHDREnabled || + currentHDRCapable != bOutputHDRCapable || currentHDRForce != g_bForceHDRSupportDebug ) { if ( g_nXWaylandCount > 1 ) @@ -8579,7 +8611,7 @@ steamcompmgr_main(int argc, char **argv) gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { - uint32_t hdr_value = ( g_bOutputHDREnabled || g_bForceHDRSupportDebug ) ? 1 : 0; + uint32_t hdr_value = ( bOutputHDRCapable || g_bForceHDRSupportDebug ) ? 1 : 0; XChangeProperty(server->ctx->dpy, server->ctx->root, server->ctx->atoms.gamescopeHDROutputFeedback, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&hdr_value, 1 ); @@ -8600,6 +8632,7 @@ steamcompmgr_main(int argc, char **argv) currentOutputHeight = g_nOutputHeight; currentOutputRefresh = g_nOutputRefresh; currentHDROutput = g_bOutputHDREnabled; + currentHDRCapable = bOutputHDRCapable; currentHDRForce = g_bForceHDRSupportDebug; #if HAVE_PIPEWIRE