Solved using Claude. 1920x1080 @ 180 Hz now runs cleanly over USB-C DisplayPort on the NVIDIA GPU.
The problem
My monitor’s DisplayPort EDID advertises 10 bits per colour. The panel cannot sustain 10-bit
above 60 Hz. Linux believes the EDID and drives 10 bpc, so every mode above 60 Hz blanks and
sparkles. Windows sends 8 bpc for an SDR desktop by default and never triggers it.
The same monitor’s HDMI EDID declares no bit depth at all, so that path defaults to 8 bpc —
which is why the identical monitor on the identical machine was rock solid at 180 Hz over HDMI the
whole time.
Fix: override the EDID on that one connector with a copy declaring 8 bpc. One byte.
System
| Laptop | ASUS TUF Gaming FA506IH |
| iGPU | AMD Ryzen 5 4600H / Renoir — PCI 0000:05:00.0 (1002:1636) |
| dGPU | NVIDIA GTX 1650 Mobile (TU117M) — PCI 0000:01:00.0 (10de:1f99) |
| OS | Ubuntu 26.04.1, kernel 7.0.0-31, GNOME/mutter 50.1, Wayland only |
| Driver | NVIDIA 610.43.02, nvidia-drm.modeset=1, PRIME on-demand |
| Monitor | Xiaomi “Mi Monitor” 24", 1920x1080 up to 180 Hz |
The ports are not equivalent. HDMI is wired to the AMD iGPU (HDMI-A-1). The USB-C port
carries DisplayPort alt-mode hardwired to the NVIDIA dGPU (DP-1). No MUX, no switching. Two
different GPUs, two different drivers — and, as it turns out, two different EDIDs.
DRM card numbers are not stable across reboots here; nvidia and amdgpu swap between card1 and
card2. Always resolve by PCI address.
Symptom
Over DisplayPort at 100, 120, 144 or 180 Hz: the screen blanks briefly, recovers, repeats, with
random sparkling speckles. 60 Hz is stable. Nothing is logged — no Xid, no kernel error, no
compositor warning.
Over HDMI: 180 Hz, indefinitely, no issues. Under Windows through the same USB-C cable: fine.
Root cause
The monitor exposes a different EDID per input:
DisplayPort: byte 0x14 = 0xB5 -> 10 bpc, interface DisplayPort
HDMI: byte 0x14 = 0x80 -> bit depth UNDEFINED -> driver falls back to 8 bpc
Both carry the same timings: 60 Hz @ 148.50 MHz, 120 Hz @ 288.79 MHz, 144 Hz @ 346.50 MHz,
180 Hz @ 421.20 MHz.
Proven with a single-variable test — three EDIDs, otherwise byte-identical:
| EDID | bit depth | horiz range | 180 Hz |
|---|---|---|---|
| stock | 10 bpc | 255–255 kHz (malformed) | flickers |
| patched: range only | 10 bpc | 30–230 kHz | flickers |
| patched: range + depth | 8 bpc | 30–230 kHz | works |
Rows 2 and 3 differ by one byte.
It is not bandwidth. HBR2 over 4 lanes carries 17.28 Gbps. 180 Hz needs 10.11 Gbps at 8 bpc and
12.64 Gbps at 10 bpc — both fit easily. And the link must be 4-lane, since 8 bpc alone already
exceeds what a 2-lane link could carry, yet it works. The panel simply mishandles 10-bit input.
The malformed range descriptor is a red herring. The DP EDID claims a horizontal range of
255–255 kHz while its own modes need 67.5–202.5 kHz. It looks damning. Fixing it alone changed
nothing (row 2).
Terms used above
| Term | Meaning |
|---|---|
| EDID | 128/256-byte blob in an EEPROM inside the monitor, read over DDC/I²C (HDMI) or AUX (DP). The monitor describing itself. Written at the factory, not by the OS. A monitor with two inputs often has a separate EDID per input |
| bpc | Bits per colour channel. 8 bpc = 24-bit pixel, 10 bpc = 30-bit. 10 bpc is 25 % more data for the same mode. Encoded in EDID byte 0x14, bits 6:4 |
| Pixel clock | Pixels per second including blanking. 1920x1080 @ 180 Hz here is 421.20 MHz. × bits-per-pixel = required link bandwidth |
| DTD | Detailed Timing Descriptor — an 18-byte block holding one exact mode. The monitor’s real timings |
| Range limits descriptor | EDID block declaring acceptable refresh range, horizontal scan range, max pixel clock. Drivers use it to validate modes. This monitor’s DP copy is nonsense |
| HBR2 / lanes | DP link rate: 5.4 Gbps/lane raw, 4.32 Gbps after 8b/10b. Links use 1, 2 or 4 lanes |
| DP alt-mode | DisplayPort over USB-C. On this laptop, hardwired to the dGPU |
output_bpc |
DRM debugfs file. Prints Maximum: — the connector’s max_bpc from EDID, not the bit depth actually being driven. Useful to confirm an override landed, not a measurement |
What I changed
Four bytes of the DisplayPort EDID. Nothing else — no kernel parameters, no driver patches.
| Offset | Stock | Patched | Meaning |
|---|---|---|---|
0x14 |
0xB5 |
0xA5 |
Bits 6:4 011 (10 bpc) → 010 (8 bpc). Bit 7 (digital) and bits 3:0 (interface) untouched |
0x61 |
0xFF |
0x1E |
Min horizontal scan rate, kHz (255 → 30) |
0x62 |
0xFF |
0xE6 |
Max horizontal scan rate, kHz (255 → 230) |
0x7F |
0xEE |
0xF8 |
Base block checksum, recomputed |
The CEA extension block — all three high-refresh timings, VIC list, audio — is untouched.
Only 0x14 is proven necessary. The range fix rides along; changing 0x14 alone is very likely
sufficient.
Why Windows works and Linux doesn’t
Both read the same lying EEPROM. They differ in how much they trust it.
Windows barely trusts EDID — it keeps its own monitor database and the WDDM stacks do their own
validation, because broken EDIDs are common in cheap monitors. More importantly, Windows drives
an SDR desktop at 8 bpc by default; 10-bit is opt-in behind a control-panel toggle. So it never
sends 10-bit here.
Linux takes the EDID literally. DRM core hands drivers max_bpc straight from byte 0x14. There
is a quirk table in drm_edid.c for known-bad displays, but it is per-model and this monitor isn’t
in it. The driver did exactly what the monitor told it to.
This is not an NVIDIA driver bug
- The driver honours a capability the monitor declares. That is correct behaviour.
- The AMD path only looks better by accident — the HDMI EDID declares no bit depth, so amdgpu falls
back to 8 bpc. Handed the same data, I’d expect the same failure. - The hardware is fine: Windows drives 180 Hz through the same cable, port and GPU.
- The defect is in the monitor firmware. Xiaomi’s DP EDID advertises a capability the panel
doesn’t have.
The fair criticism is that there’s no easy supported way to tell Linux “this sink is lying, cap it
at 8 bpc”. amdgpu at least exposes a writable max bpc connector property; the NVIDIA driver
exposes nothing equivalent and its EDID override is undocumented. That’s a usability gap, not a
correctness bug.
The fix
The NVIDIA driver ignores the usual override routes: drm.edid_firmware= is never consulted by
nvidia-drm; debugfs edid_override accepts the write and never uses it; CustomEDID needs an
Xorg session, and GNOME no longer ships one. The only path that works under Wayland is the
nvidia_modeset module’s config_file parameter.
1. Find your connector and dump its EDID
DRM card numbers are not stable, and your connector may not be called DP-1. Find it first — it
must say connected:
for f in /sys/class/drm/card*-*/status; do echo "$(basename $(dirname $f)): $(cat $f)"; done
Then dump it, substituting the name you just found:
cat /sys/class/drm/card2-DP-1/edid > stock.edid
wc -c < stock.edid # must be 128 or 256
If that prints 0, the connector is disconnected — plug the monitor into that port and retry.
Two things that look like failure but aren’t: ls -l and stat report 0 bytes for the file
in sysfs (that is normal for sysfs, check the size of your output file instead), and cat stock.edid prints binary garbage or apparently nothing. Verify it properly:
sudo apt install -y edid-decode
edid-decode stock.edid | grep -E "Display Product Name|Bits per primary"
Bits per primary color channel: 10
Display Product Name: 'Mi Monitor'
That 10 is the bug.
2. Patch byte 0x14 and fix the checksum
#!/usr/bin/env python3
b = bytearray(open('stock.edid','rb').read())
b[0x14] = 0x80 | (0b010 << 4) | (b[0x14] & 0x0F) # 8 bpc, keep digital bit + interface
b[0x61], b[0x62] = 30, 230 # optional: repair range limits
b[0x7F] = (-sum(b[0:127])) & 0xFF # base block checksum
open('mi-monitor-8bpc.bin','wb').write(bytes(b))
Check the result — it must now report 8, and edid-decode must not complain about the checksum:
edid-decode mi-monitor-8bpc.bin | grep -E "Bits per primary"
Bits per primary color channel: 8
3. Install and configure
sudo mkdir -p /lib/firmware/edid /etc/nvidia
sudo cp mi-monitor-8bpc.bin /lib/firmware/edid/
/etc/nvidia/modeset.conf:
override.[0000:01:00.0].DP-0 = /lib/firmware/edid/mi-monitor-8bpc.bin
override.[0000:01:00.0].DP-1 = /lib/firmware/edid/mi-monitor-8bpc.bin
override.[0000:01:00.0].DP-2 = /lib/firmware/edid/mi-monitor-8bpc.bin
override.[0000:01:00.0].DP-3 = /lib/firmware/edid/mi-monitor-8bpc.bin
Replace 0000:01:00.0 with your own GPU’s PCI address from lspci -nn.
Syntax — this is undocumented, recovered from the module binary:
override.[<designator>].<display name> = <value>
- Designator must be in square brackets, otherwise it is truncated at the first dot and you get
Unknown GPU designator: 0000:01:00. - Designator is a bare PCI address (
%04x:%02hhx:%02hhx.%hhx) — noPCI:prefix, that is
rejected.tegraandrm-soc-dispare also accepted. - A value starting with
/is loaded as an EDID file; otherwise decoded as ASCII hex.connected/
disconnectedforce the connection state instead. - I list
DP-0…DP-3because an entry naming a non-existent display is silently ignored — no
error, no effect — and I never determined which name the driver uses internally.
/etc/modprobe.d/99-nvidia-modeset-config.conf:
options nvidia_modeset config_file=/etc/nvidia/modeset.conf
/etc/dracut.conf.d/61-nvidia-modeset-config.conf (so both reach the initramfs):
install_items+=" /etc/nvidia/modeset.conf /lib/firmware/edid/mi-monitor-8bpc.bin "
sudo update-initramfs -u && sudo reboot
A reboot is required per change: the parameter is mode 0400 at runtime and the module can’t be
unloaded while a display server holds it.
4. Verify
sudo journalctl -k -b | grep nvidia-modeset
# expect: Successfully read /etc/nvidia/modeset.conf
sudo cat /sys/kernel/debug/dri/0000:01:00.0/DP-1/output_bpc
# expect: Maximum: 8 (was 10)
Substitute your own PCI address and connector name in that path. Note this file needs root — as a
normal user you get Permission denied, and with Secure Boot enabled you may not be able to read
it at all.
Then set the mode and look at the screen — the only verification that matters.
Pitfalls
- Don’t force a DisplayPort EDID onto an HDMI connector. I tried; it capped HDMI at 60 Hz. The
DP blob has no HDMI VSDB (OUI00-0C-03), so the kernel stopped treating the sink as HDMI and
fell back to the 165 MHz DVI limit — which admits 1080p60 and rejects everything above. Dump
each input’s EDID separately. - Secure Boot blocks the debugfs route. Lockdown
integrityrefuses debugfs writes even as
root. The fix above doesn’t need debugfs, so leave Secure Boot on. output_bpcprintsMaximum:, notCurrent:. Don’t read it as the driven bit depth.- Ruled out and not worth repeating: VRR/FreeSync, dual-display scheduling, kernel version,
cross-GPU copy, NVIDIA pstate and clock locking, PCIe bandwidth, runtime PM,
amdgpu.dcfeaturemask, mode timing variants, and the cable.
Files you create
these are the four files you create on your own machine.
| File | Purpose |
|---|---|
/lib/firmware/edid/mi-monitor-8bpc.bin |
Your patched EDID, 256 bytes, generated by the script in step 2 |
/etc/nvidia/modeset.conf |
Override entries |
/etc/modprobe.d/99-nvidia-modeset-config.conf |
Points nvidia_modeset at the config |
/etc/dracut.conf.d/61-nvidia-modeset-config.conf |
Ships both into the initramfs |
Generate the .bin from your own monitor rather than copying someone else’s. An EDID is
specific to the unit — it contains that monitor’s serial number, and firmware revisions differ
between production batches.
Appendix: the stock DisplayPort EDID
Reproduced so anyone can verify the claim independently — feed it to edid-decode yourself.
00 ff ff ff ff ff ff 00 61 a9 b1 24 00 00 00 00
19 22 01 04 b5 35 1d 78 3b fe b1 a7 53 4f a3 26
10 50 54 a5 cb 00 81 80 95 00 a9 c0 b3 00 01 01
01 01 01 01 01 01 02 3a 80 18 71 38 2d 40 58 2c
45 00 0f 28 21 00 00 1a 00 00 00 ff 00 35 37 39
30 30 32 30 30 30 34 30 31 35 00 00 00 fd 00 30
b4 ff ff 3c 01 0a 20 20 20 20 20 20 00 00 00 fc
00 4d 69 20 4d 6f 6e 69 74 6f 72 0a 20 20 01 ee
02 03 21 f1 4b 01 03 12 13 04 14 05 1f 90 40 3f
23 09 07 07 83 01 00 00 68 1a 00 00 01 01 30 b4
00 cf 70 80 a0 70 38 4d 40 30 20 35 00 0f 28 21
00 00 1e 5a 87 80 a0 70 38 4c 40 30 20 35 00 0f
28 21 00 00 1a 88 a4 80 a0 70 38 2d 40 30 20 35
00 0f 28 21 00 00 1a 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1
Key fields:
byte 0x14 = 0xb5 -> digital, bit depth = 10 bpc, interface = DisplayPort
range limits -> vertical 48-180 Hz, horizontal 255-255 kHz, max pixel clock 600 MHz
DTD (base) -> 1920x1080 @ 60.000 Hz, pixel clock 148.50 MHz, hfreq 67.5 kHz
CEA EXT-DTD0 -> 1920x1080 @ 120.001 Hz, pixel clock 288.79 MHz, hfreq 138.8 kHz
CEA EXT-DTD1 -> 1920x1080 @ 144.106 Hz, pixel clock 346.50 MHz, hfreq 166.6 kHz
CEA EXT-DTD2 -> 1920x1080 @ 180.000 Hz, pixel clock 421.20 MHz, hfreq 202.5 kHz
The same monitor’s HDMI EDID has byte 0x14 = 0x80 — bit depth undefined — which is why that path
defaults to 8 bpc and never failed. Note every mode above falls outside the 255-255 kHz horizontal
range the EDID claims for itself; that descriptor is broken too, it just isn’t what causes the
flicker.
Summary
A cheap monitor’s DisplayPort EDID advertises a colour depth its panel can’t deliver. Linux
believes it, Windows doesn’t, and the result looks like a driver, cable, bandwidth or compositor
problem while being none of those. Its HDMI EDID happens to be silent about bit depth, which is why
one port on the same machine worked flawlessly and made the fault look far stranger than it was.
If you have a monitor that is stable at 60 Hz and falls apart above it — check byte 0x14 of its
EDID before you buy another cable.