#!/bin/bash
#
# radeonbuild.sh
#
# Author: htischer@weihenstephan.org
# Version: 07-Aug-2020
#
# Integrate the patch by elws@elstel.org into a .deb Linux distribution
# See https://www.elstel.org/software/hunt-for-4K-UHD-2160p.html.en
#
# Allow screen resolutions bigger as 1920x1200 or 2048x1080 for some AMD/ATI Radeon graphics cards
# Support e.g. 2560x1440 @ 60 Hz, 3840x2160 @ 30 Hz, 3840x2160 @ 60 Hz
# Disclaimer: Higher resolutions may reduce life time of graphics card or not work at all
#
# Support additional grub.cfg linux kernel load parameters:
# radeon.hdmimhz : Force a maximum HDMI pixel clock (in MHz); try 165/225/297/330 to overclock your TMDS for gaining a higher resolution.
# radeon.duallink : Allow for the dual link feature when overclocking with radeon.hdmimhz.
#
# Successfully tested graphics cards and resolutions:
# ATI/AMD Radeon 
# 2560x1440@60Hz R5 230 (Caicos, Nothern Islands)
#    Almost identical, not tested: HD 8450, HD 7450, HD 6450)
# 2560x1440@60Hz HD 4550 (RV710)
# 2560x1440@60Hz HD 2400 Pro/XT (RV610) (at this resolution only one monitor output working)
#    Almot identical, not tested: HD 3410 
# Not working with 2560x1440@60Hz: X600 (RV380)
# 
# Not supported:
# - dkm build # auto patch/rebuild driver on kernel update
# - grub.cfg template to avoid patch on boot menu update/recreation
# - secure boot - need to turn off in BIOS, system tagged as "tainted" in boot log
#
# Todo: support secure boot [create private/public certificate pair,
#   add public certificate to user certificates in BIOS,
#   build module with option to signing with private certificate]

# Max allowed TDMS pixel clock rate for HDMI/DVI in MHz
# Find out the needed clock rate, e.g. for 2560x1440 resolution with 60 Hz:
# Shows the clock rate and other data
# cvt -r 2560 1440 60
# Note: The following variants are producing clock rates which are too high
# cvt 2560 1440 60
# gtf 2560 1440 60

radeon_hdmimhz_val=250

# catch/log execution errors

function failcommand() {
	echo >&2 echo $(date +%Y-%m-%d.%H:%M:%S) ERROR: $BASH_COMMAND
	exit 1
}

# install build support, needed only once

function prepare() { trap failcommand ERR
    sudo apt-get install build-essential linux-headers-generic
}

# select kernel to patch
# alternative 1) kernel which is currently running #kernel_target=$(uname-r)
# alternative 2) default kernel for next reboot
# if update and rebuild done at same time

function kernel_target_sel() { trap failcommand ERR
    echo $(find /boot/vmlinuz -printf %l | sed -e s/vmlinuz-//)
}

# get location where kernel got extracted to
# name mismatch underscore/dash, there is no clear relationship even not
# with downloaded tar file name or top folder in tar content.

function kernel_src_sel() { trap failcommand ERR
    echo $(ls -1tr linux*.diff.gz | tail -n 1 | sed -e 's/-.*//' | sed -e s/_/-/)
}

# download and extract current kernel

function download() { trap failcommand ERR

    # Driver patches for better modes
    
    [ -f 0001-radeon.hdmimhz-parameter-introduced-linux-4.20.patch ] || wget -nv https://www.elstel.org/software/xorg.conf/0001-radeon.hdmimhz-parameter-introduced-linux-4.20.patch
    [ -f 0002-radeon.duallink-parameter-introduced-linux-4.20.patch ] || wget -nv https://www.elstel.org/software/xorg.conf/0002-radeon.duallink-parameter-introduced-linux-4.20.patch
    
    # move extracted kernel away, to ensure extract of freshly downloaded/current kernel
    # alternatively delete rm -rf

    if [ -n "$(find -mindepth 1 -maxdepth 1 -iname "linux*.diff.gz")" ]; then
        kernel_src=$(kernel_src_sel)
        [ -d ${kernel_src} ] && mv ${kernel_src} $(mktemp -u -p . -d ${kernel_src}_XXX)
    fi
    
    apt-get source linux
}

# build and install patched radeon.ko driver, backup original versions

function build() { trap failcommand ERR
    
    # temp location for radeon driver build. must not contain linux as ancestor folder name

    driver_build=$(mktemp -p /tmp -d radeon_XXX)

    kernel_src=$(kernel_src_sel)

    kernel_target=$(kernel_target_sel)

    # custom trace implementation on external module build must have an absolute path,
    # see instructions in /usr/src/linux-headers-$kernel_target/include/trace/define_trace.h
    # Also the  path is not a string and therefore is vulnerable to macro expansions.
    # Particularly "linux" will be expanded to "1".
    # Therefore need to copy driver to path without linux as ancestor folder name


    cp -Rp $kernel_src/drivers/gpu/drm/radeon/* $driver_build

    # integrate the TDMS patch
    
    patch -d $driver_build -p5 <0001-radeon.hdmimhz-parameter-introduced-linux-4.20.patch
    patch -d $driver_build -p5 <0002-radeon.duallink-parameter-introduced-linux-4.20.patch

    mv ${driver_build}/radeon_trace.h ${driver_build}/radeon_trace.h_orig

    sed -e "s@#define TRACE_INCLUDE_PATH.*@#define TRACE_INCLUDE_PATH ${driver_build}@" ${driver_build}/radeon_trace.h_orig >${driver_build}/radeon_trace.h

    rm ${driver_build}/radeon_trace.h_orig

    # parallel build? not really needed for radeon only
    # if yes: limit number of processes, otherwise system immediately out of memory,
    # e.g number of cores+1: ... $(($(nproc)+1)) ...

    make -j $(($(nproc)+1)) -C /lib/modules/$kernel_target/build M=${driver_build} clean

    make -j $(($(nproc)+1)) -C /lib/modules/$kernel_target/build M=${driver_build} modules

    # backup orig driver for the case of trouble
    
    backup=$(mktemp -p . -d radeon_linux-${kernel_target}_XXX)

    # Don't add to intalled folders - will not be removed on later updates

    zip -o -j $backup/radeon_orig.zip /lib/modules/$kernel_target/kernel/drivers/gpu/drm/radeon/radeon.ko

    sudo cp -p $driver_build/radeon.ko /lib/modules/$kernel_target/kernel/drivers/gpu/drm/radeon/radeon.ko
    
    sudo chown root:root /lib/modules/$kernel_target/kernel/drivers/gpu/drm/radeon/radeon.ko
    
    # chmod not necessary: *.ko don't need to be executable, many *.ko have write permissions for group

    # depmod needed  ?- no serious change
    # Must add all modules to /lib/modules/${kernel_target}/nodules.deb, must not specify install path of radeon.ko
    
    sudo depmod $kernel_target 

    sudo zip -o -j $backup/radeon_hmdimhz.zip /lib/modules/$kernel_target/kernel/drivers/gpu/drm/radeon/radeon.ko

    # create new ramdisk not needed - does no contain module
    # sudo update-initramfs -c -k $kernel_target
}

function enable() { trap failcommand ERR

    # to be sure (not really needed) again create new grub menu

    sudo update-grub
    
    kernel_target=$(kernel_target_sel)
     
    # manually add hires to default menu record
    # Todo: better would be modifying templates, but how?

    driver_grub_temp=/tmp/grub.cfg

    sed -e "s@\(/boot/vmlinuz-$kernel_target\)@\1 radeon.hdmimhz=$radeon_hdmimhz_val@" /boot/grub/grub.cfg >$driver_grub_temp

    sudo cp $driver_grub_temp /boot/grub/grub.cfg

    rm $driver_grub_temp
}

function clean() { trap failcommand ERR
    rm -rf linux-*/ /tmp/radeon_*/
}

function check() { trap failcommand ERR
    kernel_target=$(kernel_target_sel)
    
    if grep -q hdmimhz /boot/grub/grub.cfg
    then echo grub.cfg OK $(grep hdmimhz /boot/grub/grub.cfg | head -n 1 | sed -e 's/.*\(radeon.hdmimhz=[1-9][0-9]*\).*/\1/')
    else echo grub.cfg FAILED
    fi
    if strings -a /lib/modules/$kernel_target/kernel/drivers/gpu/drm/radeon/radeon.ko | grep -q hdmimhz
    then echo radeon.ko OK hdmimz supported
    else echo radeon.ko FAILED
    fi
}

function usage() { trap failcommand ERR
    echo Patch radeon.ko graphics driver to accepting TDMS pixel clock rates bigger as 165 MHz
    echo on HDMI,DVI,DisplayPort output for resolutions bigger as 1920x1200 or 2048x1080
    echo on HDMI monitor input.
    echo
    echo Works with .deb based Linux distributions, e.g. Ubuntu, state Aug-2020 .
    echo
    echo For resolutions bigger as 2560x1440 change variable radeon_hdmimhz_val in $0
    echo
    echo Not supported: Secure boot
    echo
    echo Requires: sudo permissions and user password
    echo
    echo Usage:
    echo "    "$(basename $0) prepare" " \# Download tools, needed only once
    echo "    "$(basename $0) download"" \# Download newest kernel version,
    echo "                           "" "\# need to repeat after each update which involves a kernel change
    echo "    "$(basename $0) build"   " \# build new radeon.ko driver,
    echo "                           "" "\# need to repeat after each download of a new kernel
    echo "    "$(basename $0) enable"  " \# change grub boot menu to enable new resolutions,
    echo "                           "" "\# need to repeat after each update, which touches the grub boot menu
    echo "    "$(basename $0) clean"   " \# remove temporary files
    echo "    "$(basename $0) check"   " \# verify whether patch is active
}

case $1 in
    prepare) prepare;;
    download) download;;
    build) build;;
    enable) enable;;
    clean) clean;;
    check) check;;
    *) echo argument error; echo; usage; exit 1;;
esac

# End
