Skip to content

Instantly share code, notes, and snippets.

@kj800x
Last active September 12, 2024 23:46
Show Gist options
  • Save kj800x/be3001c07c49fdb36970633b0bc6defb to your computer and use it in GitHub Desktop.
Save kj800x/be3001c07c49fdb36970633b0bc6defb to your computer and use it in GitHub Desktop.
Hacking the LG Monitor's EDID

preface: Posting these online since it sounds like these notes are somewhat interesting based on a few folks I've shared with. These are semi-rough notes that I basically wrote for myself in case I ever needed to revisit this fix, so keep that in mind.

I recently bought an LG ULTRAGEAR monitor secondhand off of a coworker. I really love it and it's been great so far, but I ran into some minor issues with it in Linux. It works great on both Mac and Windows, but on Linux it displays just a black panel until I use the second monitor to go in and reduce the refresh rate down to 60 Hz.

This has worked decent so far but there's some issues:

  • It doesn't work while linux is booting up. The motherboards boot sequence is visible just fine, but as soon as control is handed over to Linux and I'd normally see a splash screen while I'm waiting for my login window, I see nothing.
  • It doesn't work on the login screen. This would be fine if login consistently worked on my second screen, but I need to manually switch the cables between my work computer and the desktop for the second screen and sometimes I don't feel like doing that. Even when I switch the cables, the second screen seems to be moody and doesn't always show the login screen either.
  • Once I've logged in and fixed the settings on my second screen it seems to go fine, unless I actually unplug the second screen. If I do, it looks like the graphics settings go reset back to default (settings that don't work) and I lose the main monitor too.

Debugging

Since I was able to fix the issue by manually reducing the refresh rate, my guess is that the issue is really about Linux insisting on defaulting the monitor to a mode that it doesn't support: 3440 x 1440 143.923 Hz. I started looking online to do some research and I found a lot of articles and forum posts about how to sync up your logged-in display preferences with your boot sequence preferences & your greeter preferences, but that was mostly around fixing resolution, position, and orientation of monitors and it only worked for specific monitor connections.

In my case, it wasn't so much about the positioning of the monitors, the default mode that my monitor seemed to want to be in just didn't seem to work at all. Regardless, I tried the suggestions around creating an xrandr script and having it run when the greeter starts, but it seems that my monitor didn't care and the xrandr script wasn't doing anything at all. I also tried updating my version of Linux Mint and my Linux kernel to the latest versions, neither of those solved the issue.

Since this monitor did work out of the box when hooked up to the Macbook or when I boot this computer into Windows, I knew it was possible for this monitor to work properly.

EDID

I landed on the arch wiki for xrandr, xorg, and kernel mode setting and started to get the idea that my monitor was possibly reporting the wrong mode to the OS. This eventually lead me to this mailing list post which had me start to look closer at the EDID. Running edid-decode /sys/devices/pci0000:00/0000:00:03.1/0000:08:00.0/drm/card0/card0-DP-2/edid I got this output:

edid-decode (hex):

00 ff ff ff ff ff ff 00 1e 6d 4b 77 b5 42 02 00
09 1e 01 04 b5 50 21 78 9f f6 75 af 4e 42 ab 26
0e 50 54 21 09 00 71 40 81 80 81 c0 a9 c0 b3 00
d1 c0 81 00 d1 cf da a7 70 50 d5 a0 34 50 90 20
3a 30 20 4f 31 00 00 1a 00 00 00 fd 00 30 90 e1
e1 50 01 0a 20 20 20 20 20 20 00 00 00 fc 00 4c
47 20 55 4c 54 52 41 47 45 41 52 0a 00 00 00 ff
00 30 30 39 4e 54 5a 4e 34 43 31 34 39 0a 02 34

02 03 30 71 23 09 07 07 47 10 04 03 01 1f 13 12
83 01 00 00 e3 05 c0 00 e2 00 6a e6 06 05 01 61
61 3d 6d 1a 00 00 02 05 30 90 00 04 61 3d 61 3d
4e d4 70 d0 d0 a0 32 50 30 20 3a 00 20 4f 31 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 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ee

70 12 79 00 00 03 01 14 66 38 01 86 6f 0d ef 00
2f 80 1f 00 9f 05 45 00 02 00 09 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 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 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 0b 90

----------------

Block 0, Base EDID:
  EDID Structure Version & Revision: 1.4
  Vendor & Product Identification:
    Manufacturer: GSM
    Model: 30539
    Serial Number: 148149
    Made in: week 9 of 2020
  Basic Display Parameters & Features:
    Digital display
    Bits per primary color channel: 10
    DisplayPort interface
    Maximum image size: 80 cm x 33 cm
    Gamma: 2.20
    DPMS levels: Standby
    Supported color formats: RGB 4:4:4, YCrCb 4:4:4, YCrCb 4:2:2
    Default (sRGB) color space is primary color space
    First detailed timing includes the native pixel format and preferred refresh rate
    Display is continuous frequency
  Color Characteristics:
    Red  : 0.6865, 0.3076
    Green: 0.2587, 0.6699
    Blue : 0.1494, 0.0576
    White: 0.3134, 0.3291
  Established Timings I & II:
    DMT 0x04:   640x480    59.940 Hz   4:3    31.469 kHz  25.175 MHz
    DMT 0x09:   800x600    60.317 Hz   4:3    37.879 kHz  40.000 MHz
    DMT 0x10:  1024x768    60.004 Hz   4:3    48.363 kHz  65.000 MHz
    DMT 0x24:  1280x1024   75.025 Hz   5:4    79.976 kHz 135.000 MHz
  Standard Timings:
    CVT     :  1152x864    59.959 Hz   4:3    53.783 kHz  81.750 MHz (EDID 1.4 source)
    GTF     :  1152x864    60.000 Hz   4:3    53.700 kHz  81.624 MHz (EDID 1.3 source)
    DMT 0x23:  1280x1024   60.020 Hz   5:4    63.981 kHz 108.000 MHz
    DMT 0x55:  1280x720    60.000 Hz  16:9    45.000 kHz  74.250 MHz
    DMT 0x53:  1600x900    60.000 Hz  16:9    60.000 kHz 108.000 MHz (RB)
    DMT 0x3a:  1680x1050   59.954 Hz  16:10   65.290 kHz 146.250 MHz
    DMT 0x52:  1920x1080   60.000 Hz  16:9    67.500 kHz 148.500 MHz
    DMT 0x1c:  1280x800    59.810 Hz  16:10   49.702 kHz  83.500 MHz
    CVT     :  1920x1080   74.906 Hz  16:9    84.643 kHz 220.750 MHz (EDID 1.4 source)
    GTF     :  1920x1080   75.000 Hz  16:9    84.600 kHz 220.637 MHz (EDID 1.3 source)
  Detailed Timing Descriptors:
    DTD 1:  3440x1440   60.001 Hz  43:18   89.521 kHz 429.700 MHz (800 mm x 335 mm)
                 Hfront  144 Hsync 800 Hback 416 Hpol P
                 Vfront    3 Vsync  10 Vback  39 Vpol N
  Display Range Limits:
    Monitor ranges (Bare Limits): 48-144 Hz V, 225-225 kHz H, max dotclock 800 MHz
    Display Product Name: 'LG ULTRAGEAR'
    Display Product Serial Number: '009NTZN4C149'
  Extension blocks: 2
Checksum: 0x34

----------------

Block 1, CTA-861 Extension Block:
  Revision: 3
  Basic audio support
  Supports YCbCr 4:4:4
  Supports YCbCr 4:2:2
  Native detailed modes: 1
  Audio Data Block:
    Linear PCM:
      Max channels: 2
      Supported sample rates (kHz): 48 44.1 32
      Supported sample sizes (bits): 24 20 16
  Video Data Block:
    VIC  16:  1920x1080   60.000 Hz  16:9    67.500 kHz 148.500 MHz
    VIC   4:  1280x720    60.000 Hz  16:9    45.000 kHz  74.250 MHz
    VIC   3:   720x480    59.940 Hz  16:9    31.469 kHz  27.000 MHz
    VIC   1:   640x480    59.940 Hz   4:3    31.469 kHz  25.175 MHz
    VIC  31:  1920x1080   50.000 Hz  16:9    56.250 kHz 148.500 MHz
    VIC  19:  1280x720    50.000 Hz  16:9    37.500 kHz  74.250 MHz
    VIC  18:   720x576    50.000 Hz  16:9    31.250 kHz  27.000 MHz
  Speaker Allocation Data Block:
    FL/FR - Front Left/Right
  Colorimetry Data Block:
    BT2020YCC
    BT2020RGB
  Video Capability Data Block:
    YCbCr quantization: No Data
    RGB quantization: Selectable (via AVI Q)
    PT scan behavior: Always Underscanned
    IT scan behavior: Always Underscanned
    CE scan behavior: Always Underscanned
  HDR Static Metadata Data Block:
    Electro optical transfer functions:
      Traditional gamma - SDR luminance range
      SMPTE ST2084
    Supported static metadata descriptors:
      Static metadata type 1
    Desired content max luminance: 97 (408.759 cd/m^2)
    Desired content max frame-average luminance: 97 (408.759 cd/m^2)
    Desired content min luminance: 61 (0.234 cd/m^2)
  Vendor-Specific Data Block (AMD), OUI 00-00-1A:
    02 05 30 90 00 04 61 3d 61 3d                   '..0...a=a='
  Detailed Timing Descriptors:
    DTD 2:  3440x1440   99.990 Hz  43:18  148.986 kHz 543.500 MHz (800 mm x 335 mm)
                 Hfront   48 Hsync  32 Hback 128 Hpol P
                 Vfront    3 Vsync  10 Vback  37 Vpol N
Checksum: 0xee

----------------

Block 2, DisplayID Extension Block:
  Version: 1.2
  Extension Count: 0
  Display Product Type: Extension Section
  Video Timing Modes Type 1 - Detailed Timings Data Block:
    DTD:  3440x1440  143.923 Hz  64:27  217.323 kHz 799.750 MHz (aspect 64:27, no 3D stereo, preferred)
               Hfront   48 Hsync  32 Hback 160 Hpol P
               Vfront    3 Vsync  10 Vback  57 Vpol N
  Checksum: 0x0b
Checksum: 0x90

I am not an EDID expert by any means (although I'm certainly more than a novice after this whole saga), but most of that looks fine to me. Actually the only place where I saw the mode that Linux Mint was defaulting to was in that last block:

Block 2, DisplayID Extension Block:
    ...
    DTD:  3440x1440  143.923 Hz  64:27  217.323 kHz 799.750 MHz (aspect 64:27, no 3D stereo, preferred)

My hypothesis now was that if there was some way for me to change the EDID so that that last block had 60 Hz instead of 143.923 Hz, that could fix everything.

I started by trying to read the specs for EDID and DisplayID. Ultimately these were helpful to have on the side, but actually reading and understanding the details of every byte was beginning to confuse me and was starting to take me too long. I started thinking about a faster way to grok this bytestring when I realized there was a program I just used that was really great at parsing edids: edid-decode.

Edid-Decode

I found the git repo and cloned it locally. Thankfully, the build instructions were super easy, just one make later and I had a development build of parse-edid on disk.

git clone git://linuxtv.org/edid-decode.git
cd edid-decode
make
./edid-decode /sys/devices/pci0000:00/0000:00:03.1/0000:08:00.0/drm/card0/card0-DP-2/edid

Looking at the output, I wanted to hone in on that DisplayID Extension Block section. I popped the source open in VSCode and started searching around. DisplayID Extension Block shows up as a constant in the block_name function, so looking at usages of that brought me to edid_state::parse_extension. This clearly was a DisplayID block, so I clicked into parse_displayid_block.

At this point I wanted to orient myself a bit. The function was using things like x[1] and x[2] but I knew we weren't thinking about the second and third bytes of the whole EDID. I added a print statement to print out the first 5 bytes of x, just so that I could compare with my hex editor and see where we were.

printf("-- %x %x %x %x %x", x[0], x[1], x[2], x[3], x[4]);

This gave us 70 12 79 00 00 and I could find where that sequence was in my hex editor. In fact, I could also see that the variable assignments lined up with the structure table in the DisplayID Wikipedia page (version, length, prod_type, ext_count).

Continuing to trace the code, we moved the pointer forward by 5 and then jumped into edid_state::displayid_block. We know that the next byte tag is 0x03 which matches the fact that Video Timing Modes Type 1 - Detailed Timings Data Block is the next section that's printed out. Later down the function, there's a function call to edid_state::parse_displayid_type_1_7_timing that we enter and this is where the code starts to get exciting.

Actually, the start of the function isn't that bad, but scroll down halfway and you'll find this:

t.hact = 1 + (x[4] | (x[5] << 8));
hbl = 1 + (x[6] | (x[7] << 8));
t.hfp = 1 + (x[8] | ((x[9] & 0x7f) << 8));
t.hsync = 1 + (x[10] | (x[11] << 8));
t.hbp = hbl - t.hfp - t.hsync;
if ((x[9] >> 7) & 0x1)
  t.pos_pol_hsync = true;
t.vact = 1 + (x[12] | (x[13] << 8));
vbl = 1 + (x[14] | (x[15] << 8));
t.vfp = 1 + (x[16] | ((x[17] & 0x7f) << 8));
t.vsync = 1 + (x[18] | (x[19] << 8));
t.vbp = vbl - t.vfp - t.vsync;
if ((x[17] >> 7) & 0x1)
  t.pos_pol_vsync = true;
if (x[3] & 0x10) {
  t.interlaced = true;
  t.vfp /= 2;
  t.vsync /= 2;
  t.vbp /= 2;
}

Oh no, there's no way I'm going to be able to figure out all this byte manipulation in my head. I probably don't need to care about most of this just to figure out how to fix the refresh rate, and I wish there was a way I could just see what all these values get set to in the end.

Actually debugging now

I don't do much C or C++ development, but I've used gdb in the past for schoolwork and a really great capture the flag I did once. I probably can remember enough about it to get some useful info out.

First I worked to try and get debug symbols enabled. Turns out this Makefile already has debug symbols turned on, so that wasn't actually necessary. If you ever do need to turn on debug symbols, the flag to remember is -g for the compiler although in this Makefile I just tossed it onto the WARN_FLAGS variable since I didn't see an obvious other place for it.

I fired up gdb ./edid-decode did some quick googling to figure out that setting a breakpoint is break filename.cpp:line-number and running with arguments is run arg1 arg2 and I landed in the right place.

Breakpoint 1, edid_state::parse_displayid_type_1_7_timing (this=0x5555555b3940 <state>, x=0x5555555b4408 <edid+264> "f8\001\206o", <incomplete sequence \357>, type7=false, block_rev=1, is_cta=false) at parse-displayid-block.cpp:368
368		print_timings("    ", &t, name.c_str(), s.c_str(), true);
(gdb) 

Ok, we just got through that wild bit where we set up t. I'm not entirely sure what I'm expecting since a C struct is just a bunch of bytes, but lets try to print it out:

(gdb) p t
$1 = {hact = 3440, vact = 1440, hratio = 64, vratio = 27, pixclk_khz = 799750, rb = 0, interlaced = false, hfp = 48, hsync = 32, 
  hbp = 160, pos_pol_hsync = true, vfp = 3, vsync = 10, vbp = 57, pos_pol_vsync = false, hborder = 0, vborder = 0, even_vtotal = false, 
  no_pol_vsync = false, hsize_mm = 0, vsize_mm = 0, ycbcr420 = false}

Oh wow, that's a lot more readable than I expected. I guess debug symbols go a long way! Some of these values I recognize (3440 and 1440 are the resolution), but I'm not seeing the one most important value I need to fix: the refresh rate of 143.923. Let's step forward and see if we can figure out how that's calculated:

(gdb) n
    DTD:  3440x1440  143.922761 Hz  64:27   217.323 kHz    799.750000 MHz (aspect 64:27, no 3D stereo, preferred)
               Hfront   48 Hsync  32 Hback  160 Hpol P
               Vfront    3 Vsync  10 Vback   57 Vpol N
369		if (is_cta) {
(gdb) 

Oops, we went way too far, all the calculations for the refresh rate just got done without us. I had to go look up the difference between s and n and realized that I stepped over all the interesting stuff (n = next line, s = step into). Well at least we know the interesting stuff is inside the print_timings function.

Back in my editor, we go looking and we find another relatively dense function. At this point, I'm looking for other strategies to take shortcuts and I realize that there's a Hz right after the refresh rate. I do a quick search and I find this printf:

printf("%s%s: %5ux%-5s %10.6f Hz %3u:%-3u %8.3f kHz %13.6f MHz%s\n",
	   prefix, type,
	   t->hact, buf,
	   refresh,
	   t->hratio, t->vratio,
	   out_hor_freq_khz,
	   pixclk / 1000000.0,
	   s.c_str());

That actually makes things a lot clearer. We're really looking at where the refresh variable is set. The definition for that is a bit like this:

double refresh = t->pixclk_khz * 1000.0 / (htotal * vtotal);
if (options[OptNTSC] && fmod(refresh, 6.0) == 0) {
  refresh *= ntsc_fact;
}

Ah, so that pixclk_khz from the struct earlier is actually pretty key. Let me try to see if I do that math, do I get the same number?

$$ \frac{799750 \cdot 1000}{3440 \cdot 1440} = 161.44\dots $$

What if we add in ntsc_fact which is 1000.0 / 1001.0

$$ \frac{799750 \cdot 1000}{3440 \cdot 1440} \cdot \frac{1000}{1001}= 161.28\dots $$

Huh, 161 is close to 143, but neither of these are quite right. I must be missing something.

After a lot of searching, it turned out that htotal and vtotal aren't just the resolution as I had first assumed. Here's the relevant bits, with comments added based on what stuff ended up evaluating out to:

if (t->interlaced) // false
  vact /= 2;

unsigned hbl = t->hfp + t->hsync + t->hbp + 2 * t->hborder; // 240
unsigned vbl = t->vfp + t->vsync + t->vbp + 2 * t->vborder; // 70
unsigned htotal = t->hact + hbl; // 3680
double vtotal = vact + vbl; // 1510

if (t->even_vtotal) // false
  vtotal = vact + t->vfp + t->vsync + t->vbp;
else if (t->interlaced) // false
  vtotal = vact + t->vfp + t->vsync + t->vbp + 0.5;

Ok, trying our math again:

$$ \frac{799750 \cdot 1000}{3680 \cdot 1510} = 143.922\dots $$

There we go! Finally, we understand roughly how that refresh rate that Linux Mint was erroneously using was being derived. Now let's see what we can do to fix things.

Working backwards

At this point, we're trying to work backwards from the end result we want to figure out what bytes need to be updated in the EDID. First, we want that fraction to evaluate out to 60 instead, and we don't want to muck with either of the resolution values. That basically means we should change the pixclk_khz value:

$$ \frac{x \cdot 1000}{3680 \cdot 1510} = 60 $$

This is really easy math, but I already have the whole thing plugged into WolframAlpha so it's easy to just change that to an x and set the whole thing equal to 60:

$$ x = 333408 $$

Computers are great, aren't they?

Ok, so we need t->pixclk_khz to be 333408. Where does that get set?

// edid_state::parse_displayid_type_1_7_timing
// type7 = false
t.pixclk_khz = (type7 ? 1 : 10) * (1 + (x[0] + (x[1] << 8) + (x[2] << 16))); 

It took me a bit longer than I'd like to admit to realize that this is really just 10 times 1 plus the first 3 bytes of the DisplayID section payload in little endian (look, I don't work with bit manipulation that often). Those bytes in decimal are 102, 56, and 1 so let's double check our understanding

$$ 10 \cdot (1 + 102 + 56\cdot 2^8 + 1 \cdot 2^{16}) = 799750 $$

Great, that was the value for pixclk_khz that our debugger showed us earlier. So if we want that to instead be 333408, what are the values we need for those three bytes?

$$ 10 \cdot (1 + x_0 + x_1 \cdot 2^8 + x_2 \cdot 2^{16}) = 333408 $$

Rearranging a bit:

$$ x_0 + x_1 \cdot 2^8 + x_2 \cdot 2^{16} = 33339.8 $$

It looks like we're gonna have to add a bit of imprecision since we're working with whole numbers here. I'm not entirely sure how this is typically handled, but lets just round up to 33340 and hope that nothing blows up.

There's lots of ways to solve this, but at the core it's a base change to base 8. I like to do this with division and remainders. To get the most significant digit $x_2$ we divide our number by the value of that place and look at the whole number.

$$ \frac{33340}{2^{16}} = 0.5 $$

It turns out $33340 &lt; 2^{16}$ so $x_2$ is 0. Since it's zero, we use the same number with the next place value:

$$ \frac{33340}{2^{8}} = 130.234375 $$

So we know that $x_1$ needs to be 130. We can look at the remainder there by multiplying just the decimals by the place value: $0.234375 * 2^8 = 60$ and we've already made it to our 1s place so $x_0$ just needs to be 60.

$$ 60 + 130 \cdot 2^8 + 0 \cdot 2^{16} = 33340 $$

Making the change

We've got our new 3 bytes, so let's use a hex editor to fix the file. 60 130 0 is 0x3c 0x82 0x00 in hex, so we go in and update the first line of our 3rd EDID block:

-70 12 79 00 00 03 01 14 **66 38 01** 86 6f 0d ef 00
+70 12 79 00 00 03 01 14 **3c 82 00** 86 6f 0d ef 00

Let's save and run edid-decode on the new file. Hopefully it agrees with the change that we made.

Block 2, DisplayID Extension Block:
  Version: 1.2
  Extension Count: 0
  Display Product Type: Extension Section
  Video Timing Modes Type 1 - Detailed Timings Data Block:
    DTD:  3440x1440   60.000 Hz  64:27   90.601 kHz 333.410 MHz (aspect 64:27, no 3D stereo, preferred)
               Hfront   48 Hsync  32 Hback 160 Hpol P
               Vfront    3 Vsync  10 Vback  57 Vpol N
  Checksum: 0x0b (should be 0xec)
Checksum: 0x90 (should be 0x71)

I do not have the language to explain how excited I was when I saw that 60.000 Hz in the output! You will have to trust me when I tell you I shouted in excitement!

One slight issue which probably isn't a big deal, but I'm going to fix it anyways: those checksums are off since we modified the file. I think fixing this with math wouldn't be that difficult (based on the spec, the checksums should be chosen such that the sum of all the bytes in the section modulo 256 is 0), but edid-decode was so kind as to just tell us what they should be, so we can just go and update those lines in the file directly:

-00 00 00 00 00 00 00 00 00 00 00 00 00 00 0B 90
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 EC 71

Just a quick check to make sure:

Block 2, DisplayID Extension Block:
  Version: 1.2
  Extension Count: 0
  Display Product Type: Extension Section
  Video Timing Modes Type 1 - Detailed Timings Data Block:
    DTD:  3440x1440   60.000 Hz  64:27   90.601 kHz 333.410 MHz (aspect 64:27, no 3D stereo, preferred)
               Hfront   48 Hsync  32 Hback 160 Hpol P
               Vfront    3 Vsync  10 Vback  57 Vpol N
  Checksum: 0xec
Checksum: 0x71 (should be 0x90)

Doh! The checksum for Block 2 is part of the bytes considered for the overall checksum, so by fixing it we've caused issues for the final checksum. No matter, we can fix that real quick:

-00 00 00 00 00 00 00 00 00 00 00 00 00 00 EC 71
+00 00 00 00 00 00 00 00 00 00 00 00 00 00 EC 90

Astute readers might notice that the final checksum changed back to 90 just now. This is an interesting artifact of the way that the checksum for EDID works, where all the bytes of a section must be congruent to 0 mod 256. When we fixed the checksum for Block 2, we made the sum of that whole section (including our only changes) cancel out to 0, just like it must have done before for the checksums to be valid.

The moment of truth

At this point, we've got a valid updated EDID which seems to pass the parse test for edid-decode. The real question is if this EDID would work for the monitor or not. Fortunately, it seems that there's a way to force the Linux kernel to ignore the EDID that the monitor self reports and override it with a custom firmware (very convenient for this very situation).

Very briefly, the steps seem to be:

  1. Copy the new firmware to /usr/lib/firmware (call it desktop_edid.bin for compatibility with the scripts below)
  2. Create this file: /etc/initramfs-tools/hooks/desktop_edid.sh
#!/bin/sh
# Copy local EDID monitor description data

PREREQ=""
prereqs()
{
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

. /usr/share/initramfs-tools/hook-functions

EDID_DATA="/usr/lib/firmware/desktop_edid.bin"

if [ ! -f "$EDID_DATA" ]; then
    exit 0
fi

add_firmware "$(basename $EDID_DATA)"

exit 0
  1. Make it executable: sudo chmod +x /etc/initramfs-tools/hooks/desktop_edid.sh
  2. Rebuild your initramfs: sudo update-initramfs -u -v

If all goes well, you should see a log line about copying firmware desktop_edid.bin. This means that when you next reboot, you can force the kernel to use that firmware.

Let's do a quick test first before making any permanent changes:

  1. Reboot
  2. When you get to grub, use e to edit the boot configuration for the current boot
  3. Find the linux line which should have something like quiet splash at the end of it. Update that line to instead end in quiet splash drm.edid_firmware=desktop_edid.bin
  4. Press F10 and boot!

After all this I saw a sight I've never seen before. The loading spinner for Linux Mint was showing up on my new monitor. A few seconds later, I was greeted with an even more impressive sight: the login screen popped open on the new monitor. To be honest, I wasn't sure that I believed it would work from the start, but I'm amazed that it actually worked in the end.

Follow ups:

  • One issue is that now my second monitor isn't displaying anything. When I go into Display Settings, it seems like my computer thinks that both monitors are sending this EDID so it's put the second monitor into the wrong mode. There is a way to override the firmware for only one connector according to the Arch wiki: drm.edid_firmware=VGA-1:edid/your_edid.bin so I'm going to need to explore that a bit.
  • Once I get it working, I'll update the grub files for real and make it permanent.
  • One last thing I might consider doing at some point would be to try to overwrite the EDID on the monitor itself. I'd probably want to confirm that my changes are 100% safe and correct before doing this, but that would mean that my system doesn't need any special configuration for this monitor. Any Linux laptop that I plug it into or any port that I plug it into should just work. I've found docs on this online, but it's pretty risky because if I write a bad EDID I can brick the monitor, and if I do the process wrong it sounds like I could actually brick any of the components in my computer... not good!

Used resources

@dreamlayers
Copy link

In the past I've used https://github.com/akatrevorjay/edid-generator to create EDID with a custom mode based on an X modeline. This would force Linux to use that mode. Though that program can create invalid EDID in case of overflow, and probably couldn't handle such a high resolution.

The EDID only needs to be in initramfs if you use early KMS, Typically, early KMS is not done, so this is not necessary.

@repentsinner
Copy link

@plaes
Copy link

plaes commented Aug 31, 2023

Now that you got it working, it should be possible to write EDID back via i2c-tools. (Or just patch these bytes)

@schildbach
Copy link

Can EDIDs be written into a monitor?

@dreamlayers
Copy link

The chip that contains the EDID is usually a serial EEPROM. It can be rewritten using the same interface that is used to read it. However, they're often write protected via another pin of the chip that isn't externally accessible. Sometimes a service menu allows you temporarily remove the write protection. Other times one has to open up the device to physically do something to temporarily remove write protection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment