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 |
|---|---|
![]() |
![]() |
| Brightness forwarding: live status and the level being sent to gamescope. | EDID fix, the peak luminance games now read, and update checks. |
| 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 |
| 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.
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>.zipThen install the resulting zip through Decky's Install Plugin from ZIP, which is the supported path and avoids permission problems.
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.
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.
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.
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.
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.
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.
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 -vThe 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.
- libdisplay-info - the parser DXVK vendors, whose stale copy this works around; the proper fix belongs upstream
BSD 3-Clause - see LICENSE. Third-party components are listed in NOTICE.
Vibe coded with the help of Claude 🤖



