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

nixos/hardware.display: init module
This commit is contained in:
K900
2024-07-18 17:02:02 +03:00
committed by GitHub
11 changed files with 695 additions and 1 deletions
@@ -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
View File
@@ -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
View File
@@ -0,0 +1,130 @@
# 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
View File
@@ -0,0 +1,193 @@
{ 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 ];
})
];
}
@@ -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"/.
+176
View File
@@ -0,0 +1,176 @@
#!/usr/bin/env bash
set -eEuo pipefail
test -z "${DEBUG:-}" || set -x
set -eEuo pipefail
FIRMWARE_PATH="${EDID_PATH:-"/run/current-system/firmware"}"
mapfile -t edid_paths <<<"${FIRMWARE_PATH//":"/$'\n'}"
err() {
LOGGER="ERROR" log "$@"
return 1
}
log() {
# shellcheck disable=SC2059
printf "[${LOGGER:-"INFO"}] $1\n" "${@:2}" >&2
}
find_path() {
local filePath="$1"
mapfile -t candidates < <(
set -x
find -L "${@:2}" -path "*/${filePath}"
)
if test "${#candidates[@]}" -eq 0; then
log "'%s' path not found" "${filePath}"
return 1
fi
log "'%s' path found at %s" "${filePath}" "${candidates[0]}"
echo -n "${candidates[0]}"
}
wait_for_file() {
local filePath="$1"
until find_path "${filePath}" "${@:2}"; do
backoff "${filePath}"
done
}
backoff() {
local what="$1" sleepFor
backoff_start="${backoff_start:-"5"}"
backoff_current="${backoff_current:-"${backoff_start}"}"
backoff_jitter_multiplier="${backoff_jitter_multiplier:-"0.3"}"
backoff_multiplier="${backoff_multiplier:-1.5}"
sleepFor="$(bc <<<"${backoff_current} + ${RANDOM} % (${backoff_current} * ${backoff_jitter_multiplier})")"
log "still waiting for '%s', retry in %s sec..." "${what}" "${sleepFor}"
sleep "${sleepFor}"
backoff_current="$(bc <<<"scale=2; ${backoff_current} * ${backoff_multiplier}")"
}
force_mode() {
local connPath="$1" newMode="$2" currentMode
currentMode="$(cat "$connPath/force")"
if test "${currentMode}" == "${newMode}"; then
log "video mode is already '%s'" "${currentMode}"
return
fi
log "changing video mode from '%s' to '%s'" "${currentMode}" "${newMode}"
echo "${newMode}" >"$connPath/force"
CHANGED=1
}
force_edid() {
local connPath="$1" edidPath="$2"
}
apply_mode() {
local connPath="$1" mode="$2"
test -n "$mode" || return
log "setting up fb mode..."
# see https://github.com/torvalds/linux/blob/8cd26fd90c1ad7acdcfb9f69ca99d13aa7b24561/drivers/gpu/drm/drm_sysfs.c#L202-L207
# see https://docs.kernel.org/fb/modedb.html
case "${mode}" in
*d) force_mode "$connPath" off ;;
*e) force_mode "$connPath" on ;;
*D) force_mode "$connPath" on-digital ;;
esac
}
apply_edid() {
local connPath="$1" edidFilename="$2" edidPath
test -n "${edidFilename}" || return
log "loading EDID override..."
edidPath="$(find_path "${edidFilename}" "${edid_paths[@]/%/"/"}" -maxdepth 2)"
force_edid "${connPath}" "$edidPath"
cat "$edidPath" >"${connPath}/edid_override"
if cmp "${connPath}/edid_override" "${edidPath}" &>/dev/null; then
log "EDID is already up to date with '%s'" "${edidPath}"
else
log "applying EDID override from ${edidPath}"
cat "$edidPath" >"${connPath}/edid_override"
CHANGED=1
fi
}
load() {
local conn="$1" edidFilename="$2" mode="$3"
export LOGGER="$conn:${edidFilename}:$mode"
CHANGED="${CHANGED:-0}"
log "starting configuration"
local connPath
connPath="$(wait_for_file "$conn" /sys/kernel/debug/dri/ -maxdepth 2 -type d)"
apply_edid "${connPath}" "${edidFilename}"
apply_mode "${connPath}" "$mode"
if test "${CHANGED}" != 0; then
log "changes detected, triggering hotplug"
echo 1 >"${connPath}/trigger_hotplug"
else
log "no changes detected, skipping hotplug trigger"
fi
}
main() {
if [[ $EUID -ne 0 ]]; then
err "must be run as root"
fi
if test "$#" == 0; then
log "loading kernel parameters from /proc/cmdline"
# replace script arguments with kernel parameters
mapfile -t args < <(xargs -n1 </proc/cmdline)
else
log "loading kernel parameters compatible arguments from commandline"
args=("$@")
fi
local -A edids modes connectors
local -a entries
local key value
for arg in "${args[@]}"; do
key="${arg%%=*}"
value=""
test "${key}" == "${arg}" || value="${arg#*=}"
case "${key}" in
video)
# one argument per connector:
# video=DP-4:e video=DP-1:e
connector="${value%:*}"
mode="${value#*:}"
connectors["${connector}"]=""
modes["$connector"]="$mode"
;;
drm.edid_firmware)
# single argument for all connectors:
# drm.edid_firmware=DP-4:edid/one.bin,DP-1:edid/two.bin
mapfile -t entries <<<"${value//","/$'\n'}"
for entry in "${entries[@]}"; do
connector="${entry%:*}"
edidFilename="${entry#*:}"
connectors["${connector}"]=""
edids["${connector}"]="${edidFilename}"
done
;;
esac
done
for connector in "${!connectors[@]}"; do
# spawn in a subshell to easily adjust and runtime modify global variables
(load "${connector}" "${edids["${connector}"]:-""}" "${modes["${connector}"]:-""}") &
done
wait
}
main "$@"
+19
View File
@@ -0,0 +1,19 @@
{ lib
, writeShellApplication
, bc
, diffutils
, findutils
, coreutils
, firmwarePaths ? [
"/run/current-system/firmware"
]
}:
writeShellApplication {
name = "edido";
meta.description = "A tool to apply display configuration from `boot.kernelParams`.";
runtimeInputs = [ diffutils findutils coreutils bc ];
text = ''
FIRMWARE_PATH="''${FIRMWARE_PATH:-"${builtins.concatStringsSep ":" firmwarePaths}"}"
${builtins.readFile ./edido.sh}
'';
}
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -eEuo pipefail
test -z "${DEBUG:-}" || set -x
# based on instructions provided in https://github.com/linuxhw/EDID/blob/98bc7d6e2c0eaad61346a8bf877b562fee16efc3/README.md
usage() {
cat <<EOF >&2
Usage:
${BASH_SOURCE[0]} PG278Q 2014 >edid.bin
repo=/path/to/linuxhw/EDID ${BASH_SOURCE[0]} PG278Q 2014 >edid.bin
verify the generated file:
edid-decode <edid.bin
parse-edid <edid.bin
load the generated file:
cat edid.bin >/sys/kernel/debug/dri/0/DP-1/edid_override
EOF
}
log() {
# shellcheck disable=SC2059
printf "${1}\n" "${@:2}" >&2
}
find_displays() {
local script=("BEGIN { IGNORECASE=1 } /${1}/")
for pattern in "${@:2}"; do
script+=('&&' "/${pattern}/")
done
cat "${repo}"/{Analog,Digital}Display.md | awk "${script[*]}"
}
to_edid() {
if ! test -e "$1"; then
log "EDID specification file $1 does not exist,"
log "it is most likely an error with https://github.com/linuxhw/EDID"
return 1
fi
log "Extracting $1..."
# https://github.com/linuxhw/EDID/blob/228fea5d89782402dd7f84a459df7f5248573b10/README.md#L42-L42
grep -E '^([a-f0-9]{32}|[a-f0-9 ]{47})$' <"$1" | tr -d '[:space:]' | xxd -r -p
}
extract_link() {
awk '{ gsub(/^.+]\(</, ""); gsub(/>).+/, ""); print }'
}
check_repo() {
test -d "$1" && test -f "$1/AnalogDisplay.md" && test -f "$1/DigitalDisplay.md"
}
main() {
if [[ $# == 0 ]]; then
usage
exit 1
fi
: "${repo:="$PWD"}"
if ! check_repo "$repo"; then
repo="${TMPDIR:-/tmp}/edid"
log "Not running inside 'https://github.com/linuxhw/EDID', downloading content to ${repo}"
if ! check_repo "$repo"; then
curl -L https://github.com/linuxhw/EDID/tarball/HEAD | tar -zx -C "${repo}" --strip-components=1
fi
fi
log "Using repository at ${repo}"
readarray -t lines < <(find_displays "${@}")
case "${#lines[@]}" in
0)
log "No matches, try broader patterns?"
exit 1
;;
1)
log "Matched entries:"
log "> %s" "${lines[@]}"
log "Found exactly one pattern, continuing..."
;;
*)
log "Matched entries:"
log "> %s" "${lines[@]}"
log "More than one match, make patterns more specific until there is only one left"
exit 2
;;
esac
to_edid "${repo}/$(extract_link <<<"${lines[0]}")"
}
main "$@"
@@ -0,0 +1,66 @@
{ lib
, coreutils
, curl
, fetchFromGitHub
, gawk
, gnutar
, stdenv
, unixtools
, writeShellApplication
, nix-update-script
, displays ? { }
}:
# Usage:
# let
# edids = linuxhw-edid-fetcher.override {
# displays.PG278Q_2014 = [ "PG278Q" "2560x1440" "2014" ];
# };
# in
# "${edids}/lib/firmware/edid/PG278Q_2014.bin";
stdenv.mkDerivation rec {
pname = "linuxhw-edid-fetcher";
version = "unstable-2023-05-08";
src = fetchFromGitHub {
owner = "linuxhw";
repo = "EDID";
rev = "98bc7d6e2c0eaad61346a8bf877b562fee16efc3";
hash = "sha256-+Vz5GU2gGv4QlKO4A6BlKSETxE5GAcehKZL7SEbglGE=";
};
fetch = lib.getExe (writeShellApplication {
name = "linuxhw-edid-fetch";
runtimeInputs = [ gawk coreutils unixtools.xxd curl gnutar ];
text = ''
repo="''${repo:-"${src}"}"
${builtins.readFile ./linuxhw-edid-fetch.sh}
'';
});
configurePhase = lib.pipe displays [
(lib.mapAttrsToList (name: patterns: ''
"$fetch" ${lib.escapeShellArgs patterns} > "${name}.bin"
''))
(builtins.concatStringsSep "\n")
];
installPhase = ''
mkdir -p "$out/bin"
ln -s "$fetch" "$out/bin/"
${lib.optionalString (displays != { }) ''
install -D --mode=444 --target-directory="$out/lib/firmware/edid" *.bin
''}
'';
passthru.updateScript = nix-update-script { extraArgs = [ "--version=branch=master" ]; };
meta = {
description = "Fetcher for EDID binaries from Linux Hardware Project's EDID repository";
homepage = "https://github.com/linuxhw/EDID";
license = lib.licenses.cc-by-40;
maintainers = with lib.maintainers; [ nazarewk ];
platforms = lib.platforms.all;
mainProgram = "linuxhw-edid-fetch";
};
}
@@ -1147,6 +1147,8 @@ let
# For systemd-binfmt
BINFMT_MISC = option yes;
# Required for EDID overriding
FW_LOADER = yes;
# Disable the firmware helper fallback, udev doesn't implement it any more
FW_LOADER_USER_HELPER_FALLBACK = option no;
+1 -1
View File
@@ -219,7 +219,7 @@ let
config = {
CONFIG_MODULES = "y";
CONFIG_FW_LOADER = "m";
CONFIG_FW_LOADER = "y";
CONFIG_RUST = if withRust then "y" else "n";
};
});