Saturation control for both SDR and HDR content under gamescope, in one plugin.
SDR
GAMESCOPE_COLOR_SDR_GAMUT_WIDENESS moves the source colorimetry away from
Rec.709. buildSDRColorimetry() has two branches and they disagree about where
neutral sits, so the slider is mapped onto whichever one the panel takes instead
of assuming: 100% is neutral either way, and the range above it follows the
display. Where the atom stops responding, GAMESCOPE_COLOR_LOOK_G22 carries the
rest with a generated .cube.
HDR
There is no dial at all - buildPQColorimetry() zeroes the gamut remap on
purpose. What the PQ path does accept is a look, so the plugin generates that
too and hands it to GAMESCOPE_COLOR_LOOK_PQ.
Why the maths is not trivial
The look is applied to values that are still transfer-encoded, before
linearisation and before any tonemapping. Scaling distance from grey in place
would drag lightness and hue around, worst in exactly the bright saturated areas
the control exists for. Both LUTs therefore decode, convert to ICtCp (ITU-R
BT.2100), scale the two chroma axes, and convert back. Black maps to black by
construction.
What it does about the display
The EDID gamescope publishes decides the SDR mapping, whether the HDR section is
drawn at all - the panel has to advertise SMPTE ST 2084, in either a CTA-861
extension or a DisplayID one - and whether anything is applied. An external
display is left alone, followed live rather than only at startup.
Neither slider goes below 100%. Settings survive a suspend and a session
restart, and are restored when a toggle goes off, the plugin unloads, or it is
uninstalled.
Install
Download the zip, then in Decky: gear icon, Developer, Install Plugin from ZIP
File.
Requires gamescope in Game Mode driving the built-in panel, and DeckyLoader.
Runs as the normal user; no root.
Tested on a Legion Go 2 (OLED, HDR) and a Legion Go S (LCD, no HDR).