Unverified Commit 999be9b3 authored by K900's avatar K900 Committed by GitHub
Browse files

Merge pull request #279789 from nazarewk/hardware-firmware-edid-fix

nixos/hardware.display: init module
parents f8b69469 676a51c4
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -13,6 +13,9 @@
- `authelia` has been upgraded to version 4.38. This version brings several features and improvements which are detailed in the [release blog post](https://www.authelia.com/blog/4.38-release-notes/).
  This release also deprecates some configuration keys, which are likely to be removed in future version 5.0, but they are still supported and expected to be working in the current version.

- `hardware.display` is a new module implementing workarounds for misbehaving monitors
  through setting up custom EDID files and forcing kernel/framebuffer modes.

## New Services {#sec-release-24.11-new-services}

- [Open-WebUI](https://github.com/open-webui/open-webui), a user-friendly WebUI
+1 −0
Original line number Diff line number Diff line
@@ -567,6 +567,7 @@
  ./services/hardware/bolt.nix
  ./services/hardware/brltty.nix
  ./services/hardware/ddccontrol.nix
  ./services/hardware/display.nix
  ./services/hardware/fancontrol.nix
  ./services/hardware/freefall.nix
  ./services/hardware/fwupd.nix
+130 −0
Original line number Diff line number Diff line
# Customizing display configuration {#module-hardware-display}

This section describes how to customize display configuration using:
- kernel modes
- EDID files

Example situations it can help you with:
- display controllers (external hardware) not advertising EDID at all,
- misbehaving graphics drivers,
- loading custom display configuration before the Display Manager is running,

## Forcing display modes {#module-hardware-display-modes}

In case of very wrong monitor controller and/or video driver combination you can
[force the display to be enabled](https://mjmwired.net/kernel/Documentation/fb/modedb.txt#41)
and skip some driver-side checks by adding `video=<OUTPUT>:e` to `boot.kernelParams`.
This is exactly the case with [`amdgpu` drivers](https://gitlab.freedesktop.org/drm/amd/-/issues/615#note_1987392)

```nix
{
  # force enabled output to skip `amdgpu` checks
  hardware.display.outputs."DP-1".mode = "e";
  # completely disable output no matter what is connected to it
  hardware.display.outputs."VGA-2".mode = "d";

  /* equals
  boot.kernelParams = [ "video=DP-1:e" "video=VGA-2:d" ];
  */
}
```

## Crafting custom EDID files {#module-hardware-display-edid-custom}

To make custom EDID binaries discoverable you should first create a derivation storing them at
`$out/lib/firmware/edid/` and secondly add that derivation to `hardware.display.edid.packages` NixOS option:

```nix
{
  hardware.display.edid.packages = [
    (pkgs.runCommand "edid-custom" {} ''
       mkdir -p $out/lib/firmware/edid
       base64 -d > "$out/lib/firmware/edid/custom1.bin" <<'EOF'
       <insert your base64 encoded EDID file here `base64 < /sys/class/drm/card0-.../edid`>
       EOF
       base64 -d > "$out/lib/firmware/edid/custom2.bin" <<'EOF'
       <insert your base64 encoded EDID file here `base64 < /sys/class/drm/card1-.../edid`>
       EOF
    '')
  ];
}
```

There are 2 options significantly easing preparation of EDID files:
- `hardware.display.edid.linuxhw`
- `hardware.display.edid.modelines`

## Assigning EDID files to displays {#module-hardware-display-edid-assign}

To assign available custom EDID binaries to your monitor (video output) use `hardware.display.outputs."<NAME>".edid` option.
Under the hood it adds `drm.edid_firmware` entry to `boot.kernelParams` NixOS option for each configured output:

```nix
{
  hardware.display.outputs."VGA-1".edid = "custom1.bin";
  hardware.display.outputs."VGA-2".edid = "custom2.bin";
  /* equals:
  boot.kernelParams = [ "drm.edid_firmware=VGA-1:edid/custom1.bin,VGA-2:edid/custom2.bin" ];
  */
}
```

## Pulling files from linuxhw/EDID database {#module-hardware-display-edid-linuxhw}

`hardware.display.edid.linuxhw` utilizes `pkgs.linuxhw-edid-fetcher` to extract EDID files
from https://github.com/linuxhw/EDID based on simple string/regexp search identifying exact entries:

```nix
{
  hardware.display.edid.linuxhw."PG278Q_2014" = [ "PG278Q" "2014" ];

  /* equals:
  hardware.display.edid.packages = [
    (pkgs.linuxhw-edid-fetcher.override {
      displays = {
        "PG278Q_2014" = [ "PG278Q" "2014" ];
      };
    })
  ];
  */
}
```


## Using XFree86 Modeline definitions {#module-hardware-display-edid-modelines}

`hardware.display.edid.modelines` utilizes `pkgs.edid-generator` package allowing you to
conveniently use [`XFree86 Modeline`](https://en.wikipedia.org/wiki/XFree86_Modeline) entries as EDID binaries:

```nix
{
  hardware.display.edid.modelines."PG278Q_60" = "    241.50   2560 2608 2640 2720   1440 1443 1448 1481   -hsync +vsync";
  hardware.display.edid.modelines."PG278Q_120" = "   497.75   2560 2608 2640 2720   1440 1443 1448 1525   +hsync -vsync";

  /* equals:
  hardware.display.edid.packages = [
    (pkgs.edid-generator.overrideAttrs {
      clean = true;
      modelines = ''
        Modeline "PG278Q_60"      241.50   2560 2608 2640 2720   1440 1443 1448 1481   -hsync +vsync
        Modeline "PG278Q_120"     497.75   2560 2608 2640 2720   1440 1443 1448 1525   +hsync -vsync
      '';
    })
  ];
  */
}
```

## Complete example for Asus PG278Q {#module-hardware-display-pg278q}

And finally this is a complete working example for a 2014 (first) batch of [Asus PG278Q monitor with `amdgpu` drivers](https://gitlab.freedesktop.org/drm/amd/-/issues/615#note_1987392):

```nix
{
  hardware.display.edid.modelines."PG278Q_60" = "   241.50   2560 2608 2640 2720   1440 1443 1448 1481   -hsync +vsync";
  hardware.display.edid.modelines."PG278Q_120" = "  497.75   2560 2608 2640 2720   1440 1443 1448 1525   +hsync -vsync";

  hardware.display.outputs."DP-1".edid = "PG278Q_60.bin";
  hardware.display.outputs."DP-1".mode = "e";
}
```
+193 −0
Original line number Diff line number Diff line
{ config, lib, pkgs, ... }:
let
  cfg = config.hardware.display;
in
{
  meta.doc = ./display.md;
  meta.maintainers = with lib.maintainers; [
    nazarewk
  ];

  options = {
    hardware.display.edid.enable = lib.mkOption {
      type = with lib.types; bool;
      default = cfg.edid.packages != null;
      defaultText = lib.literalExpression "config.hardware.display.edid.packages != null";
      description = ''
        Enables handling of EDID files
      '';
    };

    hardware.display.edid.packages = lib.mkOption {
      type = with lib.types; listOf package;
      default = [ ];
      description = ''
        List of packages containing EDID binary files at `$out/lib/firmware/edid`.
        Such files will be available for use in `drm.edid_firmware` kernel
        parameter as `edid/<filename>`.

        You can craft one directly here or use sibling options `linuxhw` and `modelines`.
      '';
      example = lib.literalExpression ''
        [
          (pkgs.runCommand "edid-custom" {} '''
            mkdir -p "$out/lib/firmware/edid"
            base64 -d > "$out/lib/firmware/edid/custom1.bin" <<'EOF'
            <insert your base64 encoded EDID file here `base64 < /sys/class/drm/card0-.../edid`>
            EOF
          ''')
        ]
      '';
      apply = list:
        if list == [ ] then null else
        (pkgs.buildEnv {
          name = "firmware-edid";
          paths = list;
          pathsToLink = [ "/lib/firmware/edid" ];
          ignoreCollisions = true;
        }) // {
          compressFirmware = false;
        };
    };

    hardware.display.edid.linuxhw = lib.mkOption {
      type = with lib.types; attrsOf (listOf str);
      default = { };
      description = ''
        Exposes EDID files from users-sourced database at https://github.com/linuxhw/EDID

        Attribute names will be mapped to EDID filenames `<NAME>.bin`.

        Attribute values are lists of `awk` regexp patterns that (together) must match
        exactly one line in either of:
        - [AnalogDisplay.md](https://raw.githubusercontent.com/linuxhw/EDID/master/AnalogDisplay.md)
        - [DigitalDisplay.md](https://raw.githubusercontent.com/linuxhw/EDID/master/DigitalDisplay.md)

        There is no universal way of locating your device config, but here are some practical tips:
        1. locate your device:
          - find your model number (second column)
          - locate manufacturer (first column) and go through the list manually
        2. narrow down results using other columns until there is only one left:
          - `Name` column
          - production date (`Made` column)
          - resolution `Res`
          - screen diagonal (`Inch` column)
          - as a last resort use `ID` from the last column
      '';
      example = lib.literalExpression ''
        {
          PG278Q_2014 = [ "PG278Q" "2014" ];
        }
      '';
      apply = displays:
        if displays == { } then null else
        pkgs.linuxhw-edid-fetcher.override { inherit displays; };
    };

    hardware.display.edid.modelines = lib.mkOption {
      type = with lib.types; attrsOf str;
      default = { };
      description = ''
        Attribute set of XFree86 Modelines automatically converted
        and exposed as `edid/<name>.bin` files in initrd.
        See for more information:
        - https://en.wikipedia.org/wiki/XFree86_Modeline
      '';
      example = lib.literalExpression ''
        {
          "PG278Q_60" = "    241.50   2560 2608 2640 2720   1440 1443 1448 1481   -hsync +vsync";
          "PG278Q_120" = "   497.75   2560 2608 2640 2720   1440 1443 1448 1525   +hsync -vsync";
          "U2711_60" = "     241.50   2560 2600 2632 2720   1440 1443 1448 1481   -hsync +vsync";
        }
      '';
      apply = modelines:
        if modelines == { } then null else
        pkgs.edid-generator.overrideAttrs {
          clean = true;
          passthru.config = modelines;
          modelines = lib.trivial.pipe modelines [
            (lib.mapAttrsToList (name: value:
              lib.throwIfNot (builtins.stringLength name <= 12) "Modeline name must be 12 characters or less"
                ''Modeline "${name}" ${value}''
            ))
            (builtins.map (line: "${line}\n"))
            (lib.strings.concatStringsSep "")
          ];
        };
    };

    hardware.display.outputs = lib.mkOption {
      type = lib.types.attrsOf (lib.types.submodule ({
        options = {
          edid = lib.mkOption {
            type = with lib.types; nullOr str;
            default = null;
            description = ''
              An EDID filename to be used for configured display, as in `edid/<filename>`.
              See for more information:
              - `hardware.display.edid.packages`
              - https://wiki.archlinux.org/title/Kernel_mode_setting#Forcing_modes_and_EDID
            '';
          };
          mode = lib.mkOption {
            type = with lib.types; nullOr str;
            default = null;
            description = ''
              A `video` kernel parameter (framebuffer mode) configuration for the specific output:

                  <xres>x<yres>[M][R][-<bpp>][@<refresh>][i][m][eDd]

              See for more information:
              - https://docs.kernel.org/fb/modedb.html
              - https://wiki.archlinux.org/title/Kernel_mode_setting#Forcing_modes
            '';
            example = lib.literalExpression ''
              "e"
            '';
          };
        };
      }));
      description = ''
        Hardware/kernel-level configuration of specific outputs.
      '';
      default = { };

      example = lib.literalExpression ''
        {
          edid.modelines."PG278Q_60" = "241.50   2560 2608 2640 2720   1440 1443 1448 1481   -hsync +vsync";
          outputs."DP-1".edid = "PG278Q_60.bin";
          outputs."DP-1".mode = "e";
        }
      '';
    };
  };

  config = lib.mkMerge [
    {
      hardware.display.edid.packages =
        lib.optional (cfg.edid.modelines != null) cfg.edid.modelines
        ++ lib.optional (cfg.edid.linuxhw != null) cfg.edid.linuxhw;

      boot.kernelParams =
        # forcing video modes
        lib.trivial.pipe cfg.outputs [
          (lib.attrsets.filterAttrs (_: spec: spec.mode != null))
          (lib.mapAttrsToList (output: spec: "video=${output}:${spec.mode}"))
        ]
        ++
        # selecting EDID for displays
        lib.trivial.pipe cfg.outputs [
          (lib.attrsets.filterAttrs (_: spec: spec.edid != null))
          (lib.mapAttrsToList (output: spec: "${output}:edid/${spec.edid}"))
          (builtins.concatStringsSep ",")
          (p: lib.optional (p != "") "drm.edid_firmware=${p}")
        ]
      ;
    }
    (lib.mkIf (cfg.edid.packages != null) {
      # services.udev implements hardware.firmware option
      services.udev.enable = true;
      hardware.firmware = [ cfg.edid.packages ];
    })
  ];
}
+8 −0
Original line number Diff line number Diff line
@@ -89,6 +89,14 @@ for module in $(< ~-/closure); do
    done || :
done

if test -e lib/firmware/edid ; then
    echo "lib/firmware/edid found, copying."
    mkdir -p "$out/lib/firmware"
    cp -v --no-preserve=mode --recursive --dereference --no-target-directory lib/firmware/edid "$out/lib/firmware/edid"
else
    echo "lib/firmware/edid not found, skipping."
fi

# copy module ordering hints for depmod
cp $kernel/lib/modules/"$version"/modules.order $out/lib/modules/"$version"/.
cp $kernel/lib/modules/"$version"/modules.builtin $out/lib/modules/"$version"/.
Loading