Skip to content

Repository files navigation

LeGo2BrightnessFix

Release Downloads Device Requires License

Two display fixes for the Lenovo Legion Go 2 OLED under gamescope. The Steam brightness slider, and the EDID that games are given. Both are gaps rather than misbehaviour: something upstream simply does not write where the display is listening.

Features · Requirements · Installation · Usage · How it works · Troubleshooting

Top Bottom
Settings, top Settings, bottom
Brightness forwarding: live status and the level being sent to gamescope. EDID fix, the peak luminance games now read, and update checks.

Features

Brightness slider that works While the panel runs in PQ with no HDR content on screen, the backlight level is forwarded to gamescope, which is the only thing still listening
Correct HDR metadata for games Drops the DisplayID block DXVK's stale parser chokes on, so games read the panel's real peak luminance instead of a 1499 nit placeholder
Both halves are automatic Neither adds a control of its own. The plugin makes the ones you already have reach the panel
Knows when to stand down It releases the moment HDR content appears, because Steam already has a working path there, and whenever the panel leaves PQ, because the backlight then works by itself
Restores what it found The previous gamescope value and the untouched EDID bytes are captured before the first write and put back on unload or removal
Costs almost nothing Roughly 0.1% of one core, and nothing at all while the brightness slider is still
No root It reads the backlight and talks to gamescope as the normal user

Requirements

Requirement Details
Device Lenovo Legion Go 2 with the Samsung AMS881KB01-0 panel (EDID SDC 0x4301)
OS SteamOS in Game Mode, panel configured for HDR/PQ output
Plugin loader Decky Loader
Privileges none, runs as the normal user

Note

The device row applies to the brightness half only, which is matched on EDID manufacturer and product code so it cannot dim a display that does honour its backlight. The EDID half is not gated that way: the DXVK defect fires on any display carrying a DisplayID 2.0 block, and the block it removes is one DXVK never reads. It does refuse to touch an EDID whose CTA block carries no luminance, so a display is never left with no usable metadata at all.


Installation

1. Install Decky Loader if you haven't already. 2. Download LeGo2BrightnessFix-x.x.x.zip from the Releases page. 3. In Gaming Mode, open the Quick Access Menu (the button). 4. Open the Decky menu, scroll to the bottom, then Developer → Install Plugin from ZIP. 5. Select the downloaded zip.

Building from source

Requires Node.js 18+.

git clone https://github.com/Rayekkk/LeGo2BrightnessFix
cd LeGo2BrightnessFix

npm install
npm run build      # bundles src/index.tsx into dist/
npm run package    # produces LeGo2BrightnessFix-<version>.zip

Then install the resulting zip through Decky's Install Plugin from ZIP, which is the supported path and avoids permission problems.


Usage

Everything is on by default and reports what it is doing, so a glance tells you whether either half is engaged and why. There is nothing to configure.

First run needs one step. The plugin needs a gamescope display script that puts the panel in HDR/PQ, so until that is in place it shows nothing but the button that installs it, and the background work stays off with it.

1. Install the script 2. Restart Game Mode
Setup Restart needed
The status line says whether there is a script to replace. gamescope only reads display scripts at startup, so the change needs a restart. The button does it for you.

After the restart the full panel appears and the plugin starts working. Any script you already had for this panel is kept alongside as .backup, and uninstalling the plugin puts it back.


How it works

Fix 1: the brightness slider

Steam writes the slider position somewhere the panel is not listening. The panel is driven over eDP AUX luminance control, so /sys/class/backlight/amdgpu_bl0 is expressed in millinits and tops out at 471000, which is 471 nits of full-field white. While gamescope drives the panel in PQ, the panel ignores that control completely, and the slider moves with nothing happening on screen.

Steam does have a working path, but only for HDR content. With an HDR game on screen it drives gamescope's GAMESCOPE_HDR_INPUT_GAIN and GAMESCOPE_SDR_INPUT_GAIN instead of the backlight. That is the two-tone slider the Quick Access Menu shows in HDR, and it works. With no HDR content it falls back to the backlight alone, and nothing downstream is listening.

So the gap is narrow and precise: PQ output, no HDR content.

if the panel is in HDR/PQ  and  no HDR content is on screen:
        gamescope SDR level (nits) = backlight value / 1000
otherwise:
        stay out of the way

GAMESCOPE_SDR_ON_HDR_CONTENT_BRIGHTNESS takes nits, the same unit the backlight node already uses, which is why the mapping is a plain divide. There is no curve of our own: Steam has already spaced its 101 slider steps perceptually, and that spacing carries over untouched.

Note

The plugin never touches anything while HDR content is up. Adding to Steam's own gain there would dim the image twice.

Fix 2: the EDID games read

gamescope writes a copy of the panel EDID and publishes its path in GAMESCOPE_DISPLAY_EDID_PATH. Proton loads that into the Wine registry, and DXVK parses it to fill DXGI_OUTPUT_DESC1, the numbers every Direct3D game bases its HDR tone mapping on.

DXVK vendors libdisplay-info pinned at a commit from March 2023. In that revision parse_ext() ignores errno, so when _di_displayid_parse() refuses a DisplayID 2.0 block with ENOTSUP, the whole EDID is discarded rather than that one block. DXVK then substitutes the placeholders in NormalizeDisplayMetadata().

Measured on the device, with DXVK_HDR=1 so DXVK is in the same HDR10 colour space a game would be:

What a game is told Without the fix With the fix The panel's actual EDID
Peak luminance 1499.000 nits 1107.128 nits 1107.128
Full-frame luminance 799.000 nits 475.683 nits 475.683
Black level 0.0100 nits 0.0007 nits 0.0007
Red primary 0.6800, 0.3200 0.6836, 0.3154 0.6836, 0.3154
Green primary 0.2650, 0.6900 0.2402, 0.7139 0.2402, 0.7139
Blue primary 0.1500, 0.0600 0.1396, 0.0439 0.1396, 0.0439
White point 0.3127, 0.3290 0.3135, 0.3291 0.3134, 0.3291

Every figure in the "without" column is a hardcoded constant inside DXVK. None of them has anything to do with this panel. Peak luminance overstated by 35%, full-field by 68%, black level by 14x, and a gamut that is simply a different one, DCI-P3, where this panel is noticeably wider in green and blue.

Cross-check: with the fix on, Marvel's Spider-Man Remastered reports 1107 nits in its own HDR calibration screen after a reset to defaults.

The plugin removes the DisplayID extension from gamescope's copy, nothing else, and fixes the extension count and base checksum so the result is still a valid EDID. DXVK never reads DisplayID at all, only base-block chromaticity and the CTA HDR static metadata, so dropping it cannot cost a game anything.

Note

The proper fix belongs in DXVK. This is a stopgap for as long as that copy is stale.

What it costs

Brightness is not polled at all. The backlight class calls sysfs_notify() on every level change, so the plugin blocks on that and the kernel wakes it exactly when the slider moves. The only recurring work is one xprop call every two seconds to notice HDR content, which re-checks the EDID at the same time.

Measured on the device: 224 loop passes over 300 seconds where polling at 20 Hz would have been 6000, and roughly 0.1% of one core on a 16-thread CPU.


Troubleshooting

The brightness half never engages

There are three reasons it stands down, and the panel says which one applies: HDR content is on screen and Steam is already driving gamescope's gain, the display has left PQ so the backlight works by itself, or this is not a panel it recognises.

The last one is deliberate. On a display that does honour its backlight, forwarding the level a second time would dim it twice, so the half only runs where that cannot happen. See the note under Requirements.

Games still read 1499 nits

That figure is DXVK's placeholder, so it means the EDID handed to the game is still the original one. Check that the EDID half reports as engaged in the panel, and note that Proton reads the EDID into the Wine registry when the game starts, so a game already running keeps the values it was given.


Development

npm install
npm run build        # bundles src/index.tsx into dist/
npm run typecheck    # TypeScript check with no emit
npm run package      # builds the installable zip

python -m unittest discover -s tests -v

The suite is in two halves. test_logic.py runs anywhere, including CI. test_device.py skips itself unless it is running on an affected panel.

tests/live_run.py runs the plugin's decision loop outside DeckyLoader and prints every state change, the quickest way to check the bridge on the device without installing anything.

lego_updater.py is shared verbatim with all my other plugins, change it in one repo and copy it to the others.


Credits

  • libdisplay-info - the parser DXVK vendors, whose stale copy this works around; the proper fix belongs upstream

License

BSD 3-Clause - see LICENSE. Third-party components are listed in NOTICE.


Vibe coded with the help of Claude 🤖

About

Decky plugin for the Lenovo Legion Go 2 OLED: makes the Steam brightness slider work while the panel is in HDR, and gives games the panel's real HDR metadata instead of DXVK's placeholder.

Topics

Resources

Stars

Watchers

Forks

Releases

Contributors

Languages