This post is the fifth of a series of posts on tweaking Ubuntu 12.10 to exploit Optimus technology on my Lenovo W530 to the extent I need. Make sure you are familiar with the context and objectives.
As described in Part 1, plymouth issues when Optimus is enabled on the W530 in EFI mode. The issue around missing usplash/plymouth turned out to be connected to the random order in which the kernel initialized the graphics devices.
Part 3 captures the root cause of the symthom: Usplash is hardcoded to render to /dev/fb0
which is fine if the IGD is initialized first by the kernel, but not good if /dev/fb0
is associated with the DIS framebuffer.
One workaround already explained is blacklisting nouveau, and loading it later, after usplash is started on the IGD framebuffer. I seeked more elegant approaches and tinkered with initram to explore the alternatives:
- Forcing proper module load order in
initrd:/etc/modules
- I found that this file is only processed after the kernel finished autoloading, and cannot load blacklisted modules. - Crafting custom initram scripts to take care of module loading early in the book process - this approach did not prove effective either.
- Creating an initram script to disable DIS proved to be impossible as debugfs was not mounted at the time of initram script execution.
- Packaging custom udev rules into the initial ramdisk to ensure the desired framebuffer numbering. I settled with this option.
Ensuring consistent framebuffer numbering via udev rules
Traditionally, Unix systems exposed a static set of device files under /dev
but current versions of Linux ship with a device manager that can dynamically populate the /dev
directory with nodes for the devices present. It allows persistent naming insensitive to the order of hardware initialization or hotplug. This is achieved by udev rules, which match the devices based on their attributes and perform actions such as creating device nodes, symlinks, changing permissions and ownership, or if fact, running arbitrary logic.
This section provides a very brief walk-through on how the rules for the current use-case can be created. First, information about the framebuffer devices had to be gathered, and the properties and attributes for the matching part had to be identified.
$ udevadm info -a -n /dev/fb0 | head -n 24
Udevadm info starts with the device specified by the devpath and then
walks up the chain of parent devices. It prints for every device
found, all possible attributes in the udev rules key format.
A rule to match, can be composed by the attributes of the device
and the attributes from one single parent device.
looking at device '/devices/pci0000:00/0000:00:01.0/0000:01:00.0/graphics/fb0':
KERNEL=="fb0"
SUBSYSTEM=="graphics"
DRIVER==""
ATTR{pan}=="0,0"
ATTR{name}=="nouveaufb"
ATTR{mode}==""
ATTR{console}==""
ATTR{blank}==""
ATTR{modes}=="U:1024x768p-0"
ATTR{state}=="0"
ATTR{bits_per_pixel}=="32"
ATTR{cursor}==""
ATTR{rotate}=="0"
ATTR{stride}=="4096"
ATTR{virtual_size}=="1024,768"
$ udevadm info -a -n /dev/fb1 | sed -n '8,24p'
looking at device '/devices/pci0000:00/0000:00:02.0/graphics/fb1':
KERNEL=="fb1"
SUBSYSTEM=="graphics"
DRIVER==""
ATTR{pan}=="0,0"
ATTR{name}=="inteldrmfb"
ATTR{mode}==""
ATTR{console}==""
ATTR{blank}==""
ATTR{modes}=="U:1920x1080p-0"
ATTR{state}=="0"
ATTR{bits_per_pixel}=="32"
ATTR{cursor}==""
ATTR{rotate}=="0"
ATTR{stride}=="7680"
ATTR{virtual_size}=="1920,1080"
As it can be seen from the listing above, is is sufficient to match devices of the graphics subsystem named by the kernel as "fd0" and "fb1". The attribute "name" can be used to discriminate between the two framebuffers. The goal is create device node /dev/fb0
for the intel framebuffer /dev/fb1
for the nouveau framebuffer respectively. This can be achieved by the following udev rule:
KERNEL=="fb?", SUBSYSTEM=="graphics", ATTR{name}=="nouveaufb", NAME="fb1"
KERNEL=="fb?", SUBSYSTEM=="graphics", ATTR{name}=="inteldrmfb", NAME="fb0"
Normally, one would save these lines to a file in the appropriate folder, say /etc/udev/rules.d/82-explicit-fb-assignment.rules
. (The numbering is used to ensure the rule is not overridden, as rules are parsed in lexicographical order.)
In this very case, however, the rule will not yield the desired results. One would see usplash on shutdown, but not on boot. The rule file is located on disk, in the root partition to be precise. It gets mounted after usplash initializes during boot...
Packaging custom udev rules into initrd
As implied by the nature of the use case, it would not suffice to drop the custom udev rule into /etc/udev/rules.d/
, it needs to be included in the initial ramdisk. Customization of initial ramdisk content is best done via custom hook scripts - they provide a clean, modular way of achieving the current goal while being minimally intrusive to other parts of the system. The hook scripts will run whenever the a new initrd is created and can contribute files to the ramdisk. They do not become part of the ramdisk themselves.
$ cat <<EOX | sudo tee /etc/initramfs-tools/hooks/explicit-fb-assignment
#!/bin/sh -e
PREREQ="udev"
# Output pre-requisites
prereqs()
{
echo "$PREREQ"
}
case "$1" in
prereqs)
prereqs
exit 0
;;
esac
. /usr/share/initramfs-tools/hook-functions
# Create udev rules to control fb1/fb0 assignment
cat > ${DESTDIR}/lib/udev/rules.d/82-explicit-fb-assignment.rules <<EOF
KERNEL=="fb?", SUBSYSTEM=="graphics", ATTR{name}=="nouveaufb", NAME="fb1"
KERNEL=="fb?", SUBSYSTEM=="graphics", ATTR{name}=="inteldrmfb", NAME="fb0"
EOF
EOX
$ sudo chmod +x /etc/initramfs-tools/hooks/fb_order
# create new initrd for the current kernel
$ sudo update-initramfs -c -k $(uname -r)
# verify on next reboot
The listing above provides the commands for creating the hook and generating a new initrd. Having applied this tweak, usplash/plymouth is fully functional and reliable, as /dev/fb0
always refers to the intel framebuffer device.
Troubleshooting
One has to remember that there are 2 udev daemons launched at different parts of the boot process. One runs off initrd, while the other is spawned after the root filesystem has been mounted. The former uses the rules included in ramdisk, while the later one reads the rules from /{lib|etc}/udev/ruled.d
of the root partition, and does not use any rule from the initrd.