Disable GPU‑side output dithering — truncation, spatial dither, and
especially temporal dithering (FRC, Frame Rate Control) — in the AMD
amdgpu display driver on Linux, by rebuilding amdgpu with a tiny patch.
Why anyone would want this: temporal dithering rapidly toggles a pixel
between two colours every frame to fake an intermediate shade. Many people
never notice it, but a minority are physically sensitive to the resulting
flicker and get eye strain, headaches, fatigue or nausea. amdgpu enables
this dithering by default and offers no runtime switch to turn it off, so
the only reliable way to disable it is to patch and rebuild the module.
This procedure was developed and verified on a single hardware/software configuration:
| GPU | AMD Radeon RX 6700 (Navi 22, RDNA2), driver amdgpu |
| Distro | Ubuntu 24.04.4 LTS (noble), later upgraded to Ubuntu 26.04.1 LTS (resolute) |
| Kernel | 7.0.0-28-generic (HWE stack; upstream base 7.0.12) — also in use on 7.0.0-29 … 7.0.0-38 |
| Display | HDMI to a TV, X11 (XFCE) |
| Secure Boot | Enabled (module must be MOK‑signed) |
I have not tested this on any other hardware, GPU generation, kernel, distro,
or display setup, and I can't say how universal it is within amdgpu + Linux.
The approach — patch resource_build_bit_depth_reduction_params() so the
driver programs no dithering, then rebuild amdgpu — should generalise, because
that function is the single place AMD's Display Core decides on bit‑depth
reduction. But every concrete detail will differ on other systems: kernel
version and source, whether Secure Boot / MOK applies, build flags, config
option names, GPU generation quirks. Treat the exact commands here as a worked
example, not a guaranteed recipe. Read each step, understand it, and adapt.
No warranty. You can end up with a black screen if a step is wrong (been there — see Recovery). Have a way to get a terminal (SSH from another machine, or a TTY) before you start.
AMD's Display Core (DC) applies bit‑depth reduction in the OPP → FMT block of the pipeline. The decision is made in one function:
drivers/gpu/drm/amd/display/dc/core/dc_resource.c
void resource_build_bit_depth_reduction_params(struct dc_stream_state *stream,
struct bit_depth_reduction_params *fmt_bit_depth)
It fills fmt_bit_depth with truncation / spatial‑dither / temporal‑dither
(frame‑random, frame‑modulation) flags depending on the stream's output colour
depth. Notably:
- at 8‑bit output it uses static spatial dithering (
FRAME_RANDOM = 0), but - at 10‑bit output it falls into
DITHER_OPTION_TRUN10withFRAME_RANDOM = 1— i.e. temporal dithering is on. With a deep‑colour display andmax bpc = 16, the driver may well pick 10‑bit, so FRC is active.
The driver already has a "do nothing" path: DITHER_OPTION_DISABLE returns
early with the struct all‑zero. The patch simply forces that path, so no
truncation, no spatial dither and no temporal dither (FRC) is ever programmed —
regardless of the output bit depth:
memset(fmt_bit_depth, 0, sizeof(*fmt_bit_depth));
+ /* disable all output bit-depth reduction (truncation + spatial +
+ * temporal/FRC dithering); identical to DITHER_OPTION_DISABLE */
+ return;There is no
amdgpumodule parameter for this.amdgpu.dcdebugmask(the driver's only debug knob) has no dithering bit as of kernel 7.0 — it was verified againstenum DC_DEBUG_MASKinamd_shared.h. Hence the rebuild.
The build also appends a one‑line marker, MODULE_INFO(frc_dither, "disabled"),
so you can confirm at any time that the running driver is the patched one:
$ modinfo -F frc_dither amdgpu
disabledA second marker records which source the module was built from, e.g.
modinfo -F nodither_src amdgpu → ubuntu-7.0.0-38.38 (see
Use the distro's kernel source).
Modern distro kernels are compiled with CPU‑mitigation features that change the machine code of every function — on the tested kernel:
CONFIG_CALL_PADDING=y
CONFIG_FUNCTION_PADDING_BYTES=16
CONFIG_MITIGATION_CALL_DEPTH_TRACKING=y
CONFIG_MITIGATION_RETHUNK=y CONFIG_MITIGATION_RETPOLINE=y
If you build amdgpu from a standalone kernel tree, the module's function
padding / objtool‑generated call sites won't match what the kernel's module
loader expects, and loading fails at runtime with:
modprobe: ERROR: could not insert 'amdgpu': Exec format error
module: x86/modules: Invalid relocation target, existing value is nonzero ...
(The module signature and vermagic can be perfectly correct and it still
won't load.)
The fix: build amdgpu as an external module against the installed kernel
build tree /lib/modules/$(uname -r)/build. That tree carries the distro's
exact .config, mitigation flags, objtool binary, Module.symvers and
kernel.release, so the resulting module's relocations and vermagic match
the running kernel automatically. This is the same mechanism DKMS uses.
Two gotchas when building the in‑tree amdgpu driver out‑of‑tree:
TRACE_INCLUDE_PATHinamdgpu_trace.his relative to the kernel srctree and won't resolve for an external build — rewrite it to an absolute path.- That absolute path must not contain the token
linux:linuxis a predefined GCC macro (= 1), so a path like.../linux-7.0.12/...gets mangled to.../1-7.0.12/.... Extract the source into a directory named e.g.ksrcinstead of the defaultlinux-<ver>.
Both are handled automatically by rebuild.sh.
The module you build replaces the distro's amdgpu wholesale, so its source
must be the distro's source for that exact kernel package — not the upstream
stable tarball whose version the headers' Makefile names.
Ubuntu stops bumping SUBLEVEL at some point and keeps backporting fixes on
top (on 7.0 the Makefile said 7.0.14 from 7.0.0-31 through 7.0.0-38,
while Ubuntu kept pulling fixes from 6.18.y / 7.1.y). A vanilla 7.0.14 build
compiles and loads perfectly, yet silently drops every one of those fixes —
about 90 drm/amd changes by 7.0.0-38, including one whose absence froze the
machine (see Post-mortem).
To see what your distro adds:
zcat /usr/share/doc/linux-modules-$(uname -r)/changelog.Debian.gz | grep -E '^\s+- drm/amd'rebuild.sh therefore fetches Ubuntu's linux-source-X.Y.Z
package in the exact version of the installed kernel package with
apt-get download (download only, no install — so it also works inside the
apt kernel hook) and builds from the tarball inside it.
git clone https://github.com/minavr/amdgpu-disable-dithering
cd amdgpu-disable-dithering
sudo bash setup.sh
# -> installs build deps, creates & imports a local MOK signing key,
# installs the automation, and (once the MOK is enrolled) builds+installs
# the patched module.
# Reboot. In the blue "MOK Manager" screen: Enroll MOK -> enter the one-time
# password you set -> Continue.
sudo amdgpu-nodither-rebuild.sh # build + sign + install for this kernel
sudo reboot
# verify:
modinfo -F frc_dither amdgpu # must print: disabledThen judge the real result: does your eye strain / nausea go away after a while of normal use? That's the only test that ultimately matters.
lspci -nnk | grep -iA3 -E 'vga|display' # confirm amdgpu
uname -r # your kernel release
echo "$XDG_SESSION_TYPE" # x11 / waylandsudo apt-get install -y build-essential flex bison libssl-dev libelf-dev \
libdw-dev bc zstd curl xz-utils bzip2 mokutil libnotify-binlibdw-devis required — without it the kernel'sgendwarfksymstool (used forMODVERSIONS) fails withfatal error: dwarf.h.- You also need the matching kernel headers:
linux-headers-$(uname -r)(the tested machine had them vialinux-headers-generic-hwe-24.04).
Use the source of the very kernel package you are running — not the
kernel.org tarball named by the headers' Makefile (that one lacks the
distro's backported fixes; see
Use the distro's kernel source).
On Ubuntu it ships as the linux-source-X.Y.Z package:
KREL=$(uname -r) # e.g. 7.0.0-38-generic
PKGVER=$(dpkg-query -W -f='${Version}' linux-modules-$KREL) # e.g. 7.0.0-38.38
SRCPKG=linux-source-${KREL%%-*} # e.g. linux-source-7.0.0
mkdir ksrc-build && cd ksrc-build
apt-get download "$SRCPKG=$PKGVER" # download only, ~200 MB
dpkg-deb --fsys-tarfile ${SRCPKG}_*.deb \
| tar -xO ./usr/src/$SRCPKG/$SRCPKG.tar.bz2 > src.tar.bz2
tar -xf src.tar.bz2
mv $SRCPKG ksrc # dir name WITHOUT the token "linux"
cd ksrcOther distros: use their equivalent (e.g.
apt-get source, a source RPM), always in the version that exactly matches the installed kernel.
patch -p1 < /path/to/amdgpu-nodither.patch # or insert the early return by hand
printf '\n#include <linux/module.h>\nMODULE_INFO(frc_dither, "disabled");\n' \
>> drivers/gpu/drm/amd/display/dc/core/dc_resource.cAMD=$PWD/drivers/gpu/drm/amd/amdgpu
sed -i "s|^#define TRACE_INCLUDE_PATH .*|#define TRACE_INCLUDE_PATH $AMD|" \
drivers/gpu/drm/amd/amdgpu/amdgpu_trace.hmake -C /lib/modules/$(uname -r)/build M="$AMD" modules -j"$(nproc)"
strip --strip-debug "$AMD/amdgpu.ko" # ~740 MB -> ~40 MBSanity‑check before trusting it:
modinfo -F vermagic "$AMD/amdgpu.ko" # must equal the stock module's
modinfo -F frc_dither "$AMD/amdgpu.ko" # must print: disabledCreate a Machine Owner Key once, sign the module, and enroll the key:
sudo mkdir -p /var/lib/amdgpu-nodither
sudo openssl req -new -x509 -newkey rsa:2048 -nodes -days 36500 \
-keyout /var/lib/amdgpu-nodither/MOK.priv -outform DER \
-out /var/lib/amdgpu-nodither/MOK.der -subj "/CN=local amdgpu module signing/"
sudo /lib/modules/$(uname -r)/build/scripts/sign-file sha256 \
/var/lib/amdgpu-nodither/MOK.priv /var/lib/amdgpu-nodither/MOK.der "$AMD/amdgpu.ko"
sudo mokutil --import /var/lib/amdgpu-nodither/MOK.der # set a one-time passwordKREL=/lib/modules/$(uname -r)/kernel/drivers/gpu/drm/amd/amdgpu/amdgpu.ko.zst
sudo cp -a "$KREL" "$KREL.stock-orig" # backup!
sudo zstd -q -f -19 "$AMD/amdgpu.ko" -o /tmp/amdgpu.ko.zst
sudo install -m0644 /tmp/amdgpu.ko.zst "$KREL"
sudo depmod "$(uname -r)"
sudo update-initramfs -u -k "$(uname -r)"
sudo rebootOn the reboot, enroll the MOK in the blue MOK Manager screen (only the first time). Then verify:
modinfo -F frc_dither amdgpu # disabledSecure Boot ordering: don't overwrite the stock module before the MOK is enrolled — until the key is trusted, lockdown will refuse to load your signed module and you'll boot without a GPU driver. Enroll first, then install.
These automate everything above for the tested Ubuntu config:
| file | purpose |
|---|---|
amdgpu-nodither.patch |
the source patch |
rebuild.sh |
fetch Ubuntu's source for the exact kernel package, patch, build against KDIR, verify vermagic + markers + struct module relocation layout, sign, install. --build-only compiles without installing; --force rebuilds even when the installed module already looks patched; --vanilla is a last-resort kernel.org build (not recommended) |
setup.sh |
one‑time installer: build deps, MOK, automation, first build |
files/zz-amdgpu-nodither |
/etc/kernel/postinst.d/ hook — rebuilds the patched module for every newly installed kernel |
files/amdgpu-nodither-rebuild.service |
boot‑time fallback that rebuilds if the running kernel is unpatched |
files/amdgpu-nodither-check.* |
desktop notification if amdgpu fails to load at all, if the running driver is not the patched build, or if it was built from vanilla instead of distro source |
A plain module replacement does not survive kernel updates — the next kernel
ships the stock (dithering) module and silently overrides yours. The
postinst.d hook rebuilds the patched module automatically whenever a new
kernel is installed (log: /var/log/amdgpu-nodither.log). The boot service and
the desktop check are a safety net: if a rebuild ever fails, you get a visible
warning instead of a silent return of the flicker.
The scripts run from /usr/local/sbin/, not from your clone — the kernel
hook calls /usr/local/sbin/amdgpu-nodither-rebuild.sh. Pulling a new version
of this repo therefore changes nothing until you redeploy it:
git pull
sudo bash setup.sh --no-build # redeploys the scripts to /usr/local/sbin
sudo amdgpu-nodither-rebuild.sh --force
sudo rebootSkipping the redeploy is exactly how the failure described in
Post-mortem happened:
a fixed rebuild.sh sat in the clone for weeks while the kernel hook kept
running the old, broken one.
# the driver is actually driving a display (not just present as a file):
readlink -f /sys/class/drm/card*/device/driver # .../drivers/amdgpu
modinfo -F frc_dither amdgpu # disabled
xrandr | head -3 # your real resolution
modinforeads the module file on disk, so it printsdisabledeven when no driver is loaded at all. Always check the DRM binding too — that is the one test that distinguishes "patched and running" from "patched and dead".
The technical checks only prove the patched code is running. The decisive test is subjective: after normal use, is the eye strain / fatigue / nausea gone?
If a boot ends in a black screen, get a terminal (SSH or a TTY) and restore the stock module:
KV=$(uname -r)
M=/lib/modules/$KV/kernel/drivers/gpu/drm/amd/amdgpu/amdgpu.ko.zst
sudo cp -a "$M.stock-orig" "$M"
sudo depmod "$KV" && sudo update-initramfs -u -k "$KV"
sudo rebootTo also disable the automation:
sudo rm -f /etc/kernel/postinst.d/zz-amdgpu-nodither \
/etc/xdg/autostart/amdgpu-nodither-check.desktop
sudo systemctl disable --now amdgpu-nodither-rebuild.serviceExec format error/Invalid relocation targeton load → you built against a standalone tree; build against/lib/modules/$(uname -r)/build. This is the nastiest one: the build, the signature andvermagicall look fine, and the module only dies at load time — see Post-mortem.fatal error: dwarf.hduringmodules_prepare/build → installlibdw-dev.- Trace include not found, or path mangled to
1-<ver>→ fixTRACE_INCLUDE_PATHand use a source dir without the tokenlinux. - Signed module still rejected under Secure Boot → the MOK isn't enrolled yet; enroll it (MOK Manager) before relying on the module.
modprobesucceeds at boot but/sys/module/amdgpuis missing → the module loaded then failed init; almost always the relocation issue above.- Kernel oopses inside
amdgpu(e.g.amdgpu_hmm_invalidate_gfx), a slowly freezing machine → the module was built from kernel.org source and lacks the distro's fixes; rebuild from the distro source — see Post-mortem.
A real failure from this setup, worth reading before you trust any automation of your own.
Symptom. After a routine apt upgrade (kernel 7.0.0-28 → 7.0.0-29) the
desktop came up at 1024×768 and refused every other mode — and stayed that
way across reboots for three days. The GPU was fine; amdgpu simply never
loaded, so the display fell back to simpledrm on the EFI framebuffer:
$ journalctl -k -b | grep -iE 'amdgpu|relocation'
module: x86/modules: Invalid relocation target, existing value is nonzeroCause. A fixed rebuild.sh (the KDIR build described above) existed only in
the working copy and had never been copied to /usr/local/sbin/. So when
the kernel hook fired for the new kernel, it rebuilt the module with the old
standalone‑tree method → the binary carried a different struct module layout
than the running kernel expects → the loader rejected it.
The layout difference is directly visible in the module's relocations:
readelf -r -W amdgpu.ko | grep -A5 gnu.linkonce.this_module # broken vs. .stock-orig
# broken build: cleanup_module @ 0x490
# stock module: cleanup_module @ 0x4a8Why nothing warned. All three safety nets were blind in the same way — they tested the module file, never the running driver:
rebuild.shsawfrc_dither=disabledon disk → "already patched, nothing to do" → the boot service's retry did nothing either, forever.- The kernel hook's rebuild reported success — the module built and signed cleanly; the defect only appears at load time.
amdgpu-nodither-check.shtestedmodinfo -F frc_dither amdgpu, which reads that same file on disk, so it stayed quiet while there was no driver at all.
Fixes now in this repo.
rebuild.shcompares the.gnu.linkonce.this_modulerelocation offsets of the module it just built against the stock module, and refuses to install on a mismatch — a missing driver is far worse than a dithering one.- The same comparison runs in the "already patched" shortcut, so a module that cannot load triggers a rebuild instead of "nothing to do".
rebuild.sh --forceskips that shortcut entirely.amdgpu-nodither-check.shfirst checks whetheramdgpuis bound to a DRM card, and shouts if the driver never loaded.
The general lesson: a check that reads an artefact on disk proves only that the artefact exists. If what you care about is a running component, test the running component.
Symptom. After about three weeks of uptime on Ubuntu kernel 7.0.0-38 the
machine froze progressively (container health checks timing out, logind
timeouts, unresponsive desktop) and needed a hard reset. The journal held four
identical oopses over two days, the first one in kcompactd0 and the later
ones in game threads:
BUG: kernel NULL pointer dereference, address: 0000000000000000
RIP: 0010:amdgpu_hmm_invalidate_gfx+0x42/0xe0 [amdgpu]
Call Trace:
__mmu_notifier_invalidate_range_start
try_to_migrate_one … migrate_pages … compact_zone
__alloc_pages_direct_compact … do_huge_pmd_anonymous_page
Memory compaction migrates a page that belongs to a GPU userptr buffer → the
MMU notifier calls into amdgpu → NULL dereference. The oops kills the task
while it holds memory-management locks, so every other task that needs them
blocks too, and the machine grinds to a halt.
Cause. rebuild.sh took the source from the kernel.org tarball named by
the headers' Makefile (7.0.14). Vanilla 7.0.14 dereferences
bo->vm_bo->vm->root.bo in amdgpu_hmm_invalidate_gfx() without checking
vm_bo (introduced in 7.0.12 by "drm/amdgpu: fix calling VM invalidation in
amdgpu_hmm_invalidate_gfx"). Ubuntu's 7.0.0-38 carries the follow-up
"drm/amdgpu: fix check in amdgpu_hmm_invalidate_gfx", and the disassembly of
the stock module confirms it never touches vm_bo there. The patched module
had silently thrown that fix away, along with about 90 other drm/amd fixes
Ubuntu had added since 7.0.14, because Ubuntu never bumped SUBLEVEL while
backporting them.
Why nothing warned. Every check passed: vermagic, the patch marker, the
struct module relocation layout. They all prove that the module fits the
kernel, not that its source matches the module it replaced.
Fixes now in this repo.
rebuild.shbuilds from Ubuntu'slinux-source-X.Y.Zin the exact version of the installed kernel package (apt-get download; no install, so it is safe inside the apt hook). If that source can't be fetched it soft-fails and leaves the stock module in place; the boot service retries later.- A
MODULE_INFO(nodither_src, "ubuntu-<pkgver>")marker. The "already patched" shortcut rebuilds whenever it doesn't match, so an existing vanilla build is replaced automatically on the next run (boot service, kernel hook, orsudo amdgpu-nodither-rebuild.sh). amdgpu-nodither-check.shwarns when the module wasn't built from the distro source.- The kernel.org path survives only as an explicit
--vanillalast resort.
The general lesson: when you replace a distro binary, rebuild it from the distro's source for that exact package version. A matching version number doesn't mean matching code.
Developed on real hardware with the help of Claude Code. Contributions/reports from other GPUs, kernels and distros are very welcome — especially data points on how far this generalises.