Theldus's blog

Ricing my Tux boot logo

By Davidson Francis | Published: 2025-04-21 | Last Modified: 2025-04-23

Final

Image 1: Final result: one waifu/image for each CPU core

I've been using Slackware as my main OS for over 10 years, and one thing I have always liked is the 'Tuxes' at the top representing the number of cores I have. I know this isn't exclusive to Slackware, but anyone who's used it with LILO knows exactly what I'm talking about.

Anyway, I've always wanted to customize the Tux with something more, shall we say, interesting. However, I was never interested in putting a single image to replace the Tux, because why not several? For example, showing specifically the number of cores I have.

This post then aims to show what is needed to add as many images as we want and display them individually on the screen, just as shown in the first image of this post. Spoiler: we're going to have to patch the kernel!).

💡 Note: The entire procedure shown here is geared towards Slackware, but all the steps can be perfectly adapted according to the Linux distro you use.

How does it work?

There's no official way to, for example, via GRUB or LILO arguments, define images to be loaded during startup, but Linux being open-source, that won't stop us, right?

I believe the whole process can be divided into three steps:

1) Image preparation

The images must meet the following criteria:

All the steps above can be summarized with the following command:

$ pngtopnm img1.png | ppmquant -fs 223 | pnmtoplainpnm > logo_cpu0_clut224.ppm
$ pngtopnm img2.png | ppmquant -fs 223 | pnmtoplainpnm > logo_cpu1_clut224.ppm
$ pngtopnm img3.png | ppmquant -fs 223 | pnmtoplainpnm > logo_cpu2_clut224.ppm
$ pngtopnm img4.png | ppmquant -fs 223 | pnmtoplainpnm > logo_cpu3_clut224.ppm
...

2) Integrating images into the kernel build

Once you have the converted images, move them to: /usr/src/linux-XYZ/drivers/video/logo and apply the patch below to add them to the build system: Kconfig and Makefile, as in:

# 1) Move the images to the kernel logo folder
$ cp logo_cpu0_clut224.ppm /usr/src/linux-XYZ/drivers/video/logo
$ ...
$ cp logo_cpu4_clut224.ppm /usr/src/linux-XYZ/drivers/video/logo

# 2) Apply the logo.patch to the kernel
$ cd /usr/src/linux-XYZ/drivers/video/logo
$ patch -p1 < /path/to/logo.patch
logo.patch
diff -ruN --color logo-old/Kconfig logo/Kconfig
--- logo-old/Kconfig	2022-02-01 13:27:16.000000000 -0300
+++ logo/Kconfig	2025-04-21 02:23:23.000000000 -0300
@@ -28,6 +28,10 @@
 	bool "Standard 224-color Linux logo"
 	default y

+config LOGO_CPUSWAIFUS_CLUT224
+	bool "Show cute CPUs waifus for each core"
+	default y
+
 config LOGO_DEC_CLUT224
 	bool "224-color Digital Equipment Corporation Linux logo"
 	depends on MACH_DECSTATION || ALPHA
diff -ruN --color logo-old/Makefile logo/Makefile
--- logo-old/Makefile	2022-02-01 13:27:16.000000000 -0300
+++ logo/Makefile	2025-04-21 02:23:11.000000000 -0300
@@ -13,6 +13,7 @@
 obj-$(CONFIG_LOGO_SUPERH_MONO)		+= logo_superh_mono.o
 obj-$(CONFIG_LOGO_SUPERH_VGA16)		+= logo_superh_vga16.o
 obj-$(CONFIG_LOGO_SUPERH_CLUT224)	+= logo_superh_clut224.o
+obj-$(CONFIG_LOGO_CPUSWAIFUS_CLUT224)   += logo_cpu0_clut224.o logo_cpu1_clut224.o logo_cpu2_clut224.o logo_cpu3_clut224.o

 obj-$(CONFIG_SPU_BASE)			+= logo_spe_clut224.o

File: website/assets/src/boot-logo/logo.patch

The idea is simple: the config LOGO_CPUSWAIFUS_CLUT224 is added to the build system ('y' as default), and the object files corresponding to the images (to be generated automatically) are linked to this config, i.e., they are only added if the kernel is compiled with this config enabled.

3) Patching the drawing routines!

In short, the main logic for drawing the Tux resides in the files:

And it works as follows: the function fb_show_logo() is invoked with the image rotation parameter and the framebuffer structure. It, in turn, obtains the number of online CPU cores and invokes fb_show_logo_line(), which, in addition to other parameters, receives the logo to be drawn.

This second function performs additional checks and prepares an additional structure, dynamically allocated for drawing the logo, which is finally done by the third function, fb_do_show_logo(). This last function, which receives this newly allocated structure, draws the same image num times.

In general terms:
Function signatures:
--------------------
int fb_show_logo(struct fb_info *info, int rotate);

Params:
  fb_info: Holds many framebuffer important properties
           (include/linux/fb.h)
  rotate:  Whether the logo should be rotated or not
           0 = no rotation

===

static int fb_show_logo_line(
  struct fb_info *info,
  int rotate,
  const struct linux_logo *logo,
  int y,
  unsigned int n);

Params:
  logo: actual linux logo (include/linux/linux_logo.h)
      struct linux_logo {
        int type;
        unsigned int width;
        unsigned int height;
        unsigned int clutsize;
        const unsigned char *clut;
        const unsigned char *data;
      };
  y: Y-coordinate to draw, starts at 0
  n: Number of times the logo should repeat

===

void fb_do_show_logo(
  struct fb_info  *info,
  struct fb_image *image,
  int rotate,
  unsigned int num)

 Params:
   fb_image: A dynamically allocated copy of linux_logo with
             additional data. (include/uapi/linux/fb.h)
      struct fb_image {
        __u32 dx;            /* Where to place image         */
        __u32 dy;
        __u32 width;         /* Size of image                */
        __u32 height;
        __u32 fg_color;      /* Only used when a mono bitmap */
        __u32 bg_color;
        __u8  depth;         /* Depth of the image           */
        const char *data;    /* Pointer to image data        */
        struct fb_cmap cmap; /* color map info               */
      };

A typical execution for 224-color image, no rotation, 4 cores:
--------------------------------------------------------------
fb_show_logo (info, 0)
  | ncpus = 4
  |
   \->  fb_show_logo_line(info, 0, actual_logo, 0, ncpus)
          | struct fb_image = <copy> actual_logo
          |
           \-> fb_do_show_logo(info, fb_image, 0, ncpus)

Can you see where this is going? The whole mechanism is basically ready, and we do not need significant changes for this to work. Note, for example, that the fb_do_show_logo() function already considers the starting X-axis for drawing the next Tux, but this is defined as 0 in the previous function (in image.dx = 0).

The following patch (core-fbmem.patch) then does three main things:

  1. Modifies the function signatures so that the X-axis can be considered when drawing the other images.
  2. Adds the list of images defined earlier.
  3. Defines a new function, which then iterates over the list of images, recalculating the X-axis, and passing only '1' as the number of redraws, since fb_do_show_logo() should draw a different image each time.

Apply it with:

$ cd /usr/src/linux-XYZ/drivers/video/fbdev/core
$ patch -p1 < /path/to/core-fbmem.patch
core-fbmem.patch (kernel < v6.7)
diff -ruN --color core-old/fbmem.c core/fbmem.c
--- core-old/fbmem.c	2022-02-01 13:27:16.000000000 -0300
+++ core/fbmem.c	2025-04-21 20:53:49.000000000 -0300
@@ -186,6 +186,18 @@

 #ifdef CONFIG_LOGO

+extern const struct linux_logo logo_cpu0_clut224;
+extern const struct linux_logo logo_cpu1_clut224;
+extern const struct linux_logo logo_cpu2_clut224;
+extern const struct linux_logo logo_cpu3_clut224;
+
+static const struct linux_logo *logo_cpuswaifus[] = {
+	&logo_cpu0_clut224,
+	&logo_cpu1_clut224,
+	&logo_cpu2_clut224,
+	&logo_cpu3_clut224
+};
+
 static inline unsigned safe_shift(unsigned d, int n)
 {
 	return n < 0 ? d >> -n : d << n;
@@ -456,7 +468,7 @@
 }

 static int fb_show_logo_line(struct fb_info *info, int rotate,
-			     const struct linux_logo *logo, int y,
+			     const struct linux_logo *logo, int x, int y,
 			     unsigned int n)
 {
 	u32 *palette = NULL, *saved_pseudo_palette = NULL;
@@ -516,7 +528,7 @@
 		image.dx = (xres - n * (logo->width + 8) - 8) / 2;
 		image.dy = y ?: (yres - logo->height) / 2;
 	} else {
-		image.dx = 0;
+		image.dx = x;
 		image.dy = y;
 	}

@@ -590,7 +602,7 @@

 	for (i = 0; i < fb_logo_ex_num; i++)
 		y = fb_show_logo_line(info, rotate,
-				      fb_logo_ex[i].logo, y, fb_logo_ex[i].n);
+				      fb_logo_ex[i].logo, 0, y, fb_logo_ex[i].n);

 	return y;
 }
@@ -685,6 +697,23 @@
 	return fb_prepare_extra_logos(info, height, yres);
 }

+static int fb_show_logo_cpus_waifus(struct fb_info *info, int x, int y,
+				    int rotate, unsigned int cpus)
+{
+	unsigned int i;
+	unsigned int amnt;
+	unsigned int nlogos = ARRAY_SIZE(logo_cpuswaifus);
+
+	amnt = (cpus <= nlogos) ? cpus : nlogos;
+
+	for (i = 0; i < amnt; i++) {
+		y = fb_show_logo_line(info, rotate, logo_cpuswaifus[i], x, 0, 1);
+		x += logo_cpuswaifus[i]->width + 8;
+	}
+
+	return y;
+}
+
 int fb_show_logo(struct fb_info *info, int rotate)
 {
 	unsigned int count;
@@ -694,8 +723,13 @@
 		return 0;

 	count = fb_logo_count < 0 ? num_online_cpus() : fb_logo_count;
-	y = fb_show_logo_line(info, rotate, fb_logo.logo, 0, count);
+
+#ifdef CONFIG_LOGO_CPUSWAIFUS_CLUT224
+	y = fb_show_logo_cpus_waifus(info, 0, 0, rotate, count);
+#else
+	y = fb_show_logo_line(info, rotate, fb_logo.logo, 0, 0, count);
 	y = fb_show_extra_logos(info, y, rotate);
+#endif

 	return y;
 }

File: website/assets/src/boot-logo/core-fbmem.patch

⚠️ Warning: The patch above was made and tested only on kernel v5.15.19, but should work without issues on kernels prior to v6.7 (i.e., kernels before 2024-01-07). The reason for this is that from v6.7 onwards, a slight refactoring was done, and pieces of code were moved from fbmem.c to fb-logo.c (in the same folder) for better code organization.

However, the idea remains exactly the same, and therefore, I leave the possible adaptations, which are trivial, as an exercise for the reader 😁.

I should also point out that there may be small differences even in v5.X kernels. On another machine of mine, which uses v5.4.186, there is a tiny difference in the fb_show_logo() function, which means the patch does not apply completely. However, as mentioned, these are small enough differences to be corrected manually.

Kernel build!

Once patched, just compile the kernel and use it. The exact procedure may vary depending on your Linux distribution, but the procedures below for Slackware should serve as general guidelines for any environment:

$ cd /usr/src/new-linux-XYZ

# Get your current .config and generate a new one based on yours
$ zcat /proc/config.gz > .config
$ make olddefconfig

# Build kernel
$ make -j$(nproc) bzImage

# Build and install modules
$ make -j4 modules && make modules_install

# Copy kernel to /boot and adjust symlinks
$ cp arch/x86/boot/bzImage /boot/vmlinuz-waifu-XYZ
$ cp System.map /boot/System.map-XYZ
$ cp .config /boot/config-XYZ
$ cd /boot
$ rm System.map
$ rm config
$ ln -s System.map-XYZ System.map
$ ln -s config-XYZ config

# Make a initrd with:
$ mkinitrd -c -k XYZ -f ext4 -r /dev/sdx1 -m ext4 -u -o /boot/initrd-waifu.gz

# Update your LILO
$ vim /etc/lilo.conf
...
image = /boot/vmlinuz-waifu-XYZ
  root = /dev/sdx1
  label = XYZ-waifu
  initrd = /boot/initrd-waifu.gz
  read-only
...

$ lilo
$ sudo reboot

Final thoughts

I'm quite happy with the result, and the idea can be expanded to other things, such as displaying a different logo depending on the CPU model or anything not defined at compile time.

Realistically, I don't expect others to actually use this, so let it at least serve as curiosity, that kernel-dev can be fun too, and of course, not everything goes well the first time:

not-so-scary

Image 2: not-so-scary kernel panic I had during code study 😂 (click to enlarge)

but it's also part of the fun =)

This post is licensed under CC BY 4.0 by the author.