plymouth: add boot splash for KMS devices

Plymouth draws a boot splash straight onto a DRM device and holds the
display until whatever follows it takes over, so nothing of the boot is
visible between reset and the user interface. On a board with no
framebuffer console that is the only way to put anything on screen
early.

Built without the framebuffer backend, without systemd and upstart
integration, and without gtk, pango and freetype, so the runtime needs
only libpng and libdrm. msgfmt is needed at build time, for the themes
that are built and then not installed. Neither binary answers to
--version, so the CI version probe is answered by test-version.sh
instead. No theme is installed and none is named in
the daemon configuration: a theme is a package of its own and selects
itself when it is installed.

Four patches are carried. Two are build fixes for musl, which has
neither rpmatch() nor execinfo.h. The other two are bugs, both
submitted upstream. The drm renderer drops master immediately after
opening the device, so every connector probe runs as a client that is
not drm master; the kernel declines it a forced probe and returns a
cached status of "unknown", and the renderer gives up with "Could not
initialize heads", which leaves the splash able to light only a display
something else has already probed. The escape key tears the splash down
for a text view, guarded only on a VT existing, so on a kernel with
CONFIG_VT but no framebuffer console the dummy driver is bound, the
text goes nowhere and the screen turns black.

Signed-off-by: Daniel Golle <daniel@makrotopia.org>
This commit is contained in:
Daniel Golle committed 2026-09-26 10:21:06 +01:00
1 parent 6968dd1804
commit 1a39b47dbd
8 files changed
+483

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=plymouth
PKG_VERSION:=26.134.222
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://gitlab.freedesktop.org/plymouth/plymouth/-/archive/$(PKG_VERSION)
PKG_HASH:=053687d4e8bb1320c14dafc014ebf4e45427f1cf417ce76b51102fbd61a3350a
PKG_MAINTAINER:=Daniel Golle <daniel@makrotopia.org>
PKG_LICENSE:=GPL-2.0-or-later
PKG_LICENSE_FILES:=COPYING
PKG_BUILD_DEPENDS:=gettext-full/host
PKG_INSTALL:=1
PKG_BUILD_PARALLEL:=1
include $(INCLUDE_DIR)/package.mk
include $(INCLUDE_DIR)/meson.mk
MESON_ARGS += \
-Ddrm=true \
-Dudev=enabled \
-Dgtk=disabled \
-Dpango=disabled \
-Dfreetype=disabled \
-Ddocs=false \
-Dsystemd-integration=false \
-Dupstart-monitoring=false \
-Dtracing=$(if $(CONFIG_DEBUG),true,false) \
-Drelease-file=/etc/openwrt_release \
-Drunstatedir=/var/run
define Package/plymouth/Default
SECTION:=utils
CATEGORY:=Utilities
URL:=https://www.freedesktop.org/wiki/Software/Plymouth/
endef
define Package/plymouth
$(call Package/plymouth/Default)
TITLE:=Graphical boot splash on a KMS device
DEPENDS:=+libpng +libdrm +libudev +libevdev +libxkbcommon +xkeyboard-config
endef
define Package/plymouth/description
Draws a boot splash directly on a DRM/KMS device and owns the display
until the application that follows it takes over, so the console stays
silent from reset to user interface.
endef
define Package/plymouth/conffiles
/etc/plymouth/plymouthd.conf
endef
define Build/InstallDev
$(INSTALL_DIR) $(1)/usr/include
$(CP) $(PKG_INSTALL_DIR)/usr/include/plymouth-1 $(1)/usr/include/
$(INSTALL_DIR) $(1)/usr/lib
$(CP) $(PKG_INSTALL_DIR)/usr/lib/libply*.so* $(1)/usr/lib/
$(INSTALL_DIR) $(1)/usr/lib/pkgconfig
$(CP) $(PKG_INSTALL_DIR)/usr/lib/pkgconfig/*.pc $(1)/usr/lib/pkgconfig/
endef
define Package/plymouth/install
$(INSTALL_DIR) $(1)/usr/lib
$(CP) $(PKG_INSTALL_DIR)/usr/lib/libply*.so* $(1)/usr/lib/
$(INSTALL_DIR) $(1)/usr/lib/plymouth
$(CP) $(PKG_INSTALL_DIR)/usr/lib/plymouth/* $(1)/usr/lib/plymouth/
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/plymouth $(1)/usr/bin/
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/plymouthd $(1)/usr/sbin/
$(INSTALL_DIR) $(1)/usr/share/plymouth
$(CP) $(PKG_INSTALL_DIR)/usr/share/plymouth/plymouthd.defaults \
$(1)/usr/share/plymouth/
$(INSTALL_DIR) $(1)/etc/plymouth
$(INSTALL_DATA) ./files/plymouthd.conf $(1)/etc/plymouth/plymouthd.conf
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/plymouth.init $(1)/etc/init.d/plymouth
endef
$(eval $(call BuildPackage,plymouth))
+72
View File
@@ -0,0 +1,72 @@
#!/bin/sh /etc/rc.common
START=11
STOP=06
PIDFILE=/var/run/plymouth.pid
running() {
/usr/bin/plymouth --ping 2>/dev/null
}
boot() {
start "$@"
}
start() {
# The KMS device is not always card0; a render-only node can
# enumerate first.
ls /dev/dri/card* >/dev/null 2>&1 || return 0
running && return 0
# plymouth only draws a graphical splash when the kernel was asked
# for one, and requiring that of every board's boot script would put
# a plymouth-ism in front of everyone. Ask for it here instead.
# Where the console is a serial port plymouth drops to text on it
# and never opens a renderer, so leave those alone.
/usr/sbin/plymouthd --mode=boot --pid-file="$PIDFILE" \
--ignore-serial-consoles --attach-to-session \
--kernel-command-line="$(cat /proc/cmdline) splash" || return 1
/usr/bin/plymouth show-splash
}
# Give the display up without going away. The daemon keeps the device
# open and the mode configured, so whatever draws next inherits a live
# crtc, and the splash can return later without a second modeset.
release() {
running || return 0
/usr/bin/plymouth deactivate
}
stop() {
running || return 0
/usr/bin/plymouth quit --retain-splash
rm -f "$PIDFILE"
}
# procd runs the K scripts as "shutdown", so this is the start of that
# sequence rather than the end. Take the display back from whoever has
# it, reusing the daemon that has been there since boot.
shutdown() {
ls /dev/dri/card* >/dev/null 2>&1 || return 0
[ -x /usr/sbin/plymouthd ] || return 0
if running; then
/usr/bin/plymouth reactivate
/usr/bin/plymouth change-mode --shutdown
/usr/bin/plymouth show-splash
return 0
fi
/usr/sbin/plymouthd --mode=shutdown --pid-file="$PIDFILE" \
--ignore-serial-consoles \
--kernel-command-line="$(cat /proc/cmdline) splash" || return 0
/usr/bin/plymouth show-splash
}
reload() {
/usr/bin/plymouth update --status=reload
}
EXTRA_COMMANDS="release"
EXTRA_HELP=" release Drop the display but keep the daemon running"
+3
View File
@@ -0,0 +1,3 @@
[Daemon]
ShowDelay=0
DeviceTimeout=8
@@ -0,0 +1,42 @@
From dd1ec1448548beee9b9c6a6b1a627580a087b179 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Thu, 24 Sep 2026 21:07:31 +0100
Subject: [PATCH] libply: provide rpmatch where the C library lacks it
rpmatch() is a GNU extension. Building against musl fails with an
implicit declaration, and the call is only used to read a yes or no
answer for a boolean command line option, so a plain first-letter test
covers what the parser needs.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
src/libply/ply-command-parser.c | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
--- a/src/libply/ply-command-parser.c
+++ b/src/libply/ply-command-parser.c
@@ -32,6 +32,24 @@
#include "ply-list.h"
#include "ply-utils.h"
+#ifndef __GLIBC__
+/* rpmatch() is a GNU extension; the C library need not provide it. */
+static int
+rpmatch (const char *response)
+{
+ if (response == NULL)
+ return -1;
+
+ if (response[0] == 'y' || response[0] == 'Y')
+ return 1;
+
+ if (response[0] == 'n' || response[0] == 'N')
+ return 0;
+
+ return -1;
+}
+#endif
+
typedef union
{
bool as_boolean;
@@ -0,0 +1,46 @@
From 29124ae4d8307bc8332b4678eb8cd4434b4bb872 Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Thu, 24 Sep 2026 21:08:57 +0100
Subject: [PATCH] main: keep the crash handler building without execinfo
backtrace() and execinfo.h are glibc extensions, so the crash handler
fails to build against musl. Guard both the include and the body of
write_backtrace, and record that the backtrace is unavailable, leaving
the maps dump and the rest of the crash report intact.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
src/main.c | 7 +++++++
1 file changed, 7 insertions(+)
--- a/src/main.c
+++ b/src/main.c
@@ -2202,7 +2202,9 @@ dump_debug_buffer_to_file (void)
#include <termios.h>
#include <unistd.h>
+#ifdef __GLIBC__
#include <execinfo.h>
+#endif
#define BACKTRACE_SIZE 1024
#define MAPS_SIZE 8192
@@ -2252,6 +2254,7 @@ write_maps (int output_fd)
static void
write_backtrace (int output_fd)
{
+#ifdef __GLIBC__
void *addresses[BACKTRACE_SIZE];
int number_of_addresses;
@@ -2264,6 +2267,10 @@ write_backtrace (int output_fd)
backtrace_symbols_fd (addresses + BACKTRACE_FRAMES_TO_SKIP,
number_of_addresses - BACKTRACE_FRAMES_TO_SKIP,
output_fd);
+#else
+ write (output_fd, "backtrace: unavailable\n",
+ strlen ("backtrace: unavailable\n"));
+#endif
}
static void
@@ -0,0 +1,105 @@
From 0cdc68b17ed3290a233e699dc31e973650b216ee Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Fri, 25 Sep 2026 11:22:40 +0100
Subject: [PATCH] drm: Probe connectors while holding DRM master
The DRM renderer drops master immediately after opening the device and
takes it back only when the splash is activated, so the connector
enumeration in query_device () runs as a client that is not DRM master.
The kernel refuses a forced probe from such a client and returns the
cached connection status instead:
User-space requested a forced probe on [CONNECTOR:84:HDMI-A-1] but is
not the DRM master, demoting to read-only probe
On a display nothing has driven yet that status is unknown, so no output
counts as connected, no controller is assigned, and the renderer gives
up with "Could not initialize heads". The splash therefore appears only
on a screen that some other client has already lit, which on a system
without fbdev emulation never happens.
This commit holds master for the duration of the connector enumeration,
dropping it again unless the backend was already active. The probe then
detects for real, reads the EDID and brings up a sink that had gone to
low power. On an rk3588 board with no other DRM client running, this
takes the HDMI connector from unknown to connected at 1920x1080.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
src/plugins/renderers/drm/plugin.c | 57 ++++++++++++++++++++++--------
1 file changed, 43 insertions(+), 14 deletions(-)
--- a/src/plugins/renderers/drm/plugin.c
+++ b/src/plugins/renderers/drm/plugin.c
@@ -1421,6 +1421,46 @@ check_simpledrm_resolution (ply_renderer
return true;
}
+/* Probe every connector, holding DRM master so the kernel performs a real
+ * detect rather than handing back the cached connection status.
+ */
+static bool
+probe_outputs (ply_renderer_backend_t *backend,
+ ply_output_t *outputs,
+ int outputs_len,
+ bool force,
+ bool *changed)
+{
+ bool was_master;
+ bool ret = true;
+ int i;
+
+ was_master = backend->is_active;
+ if (!was_master)
+ drmSetMaster (backend->device_fd);
+
+ backend->connected_count = 0;
+ for (i = 0; i < outputs_len; i++) {
+ get_output_info (backend, backend->resources->connectors[i], &outputs[i]);
+
+ if (!force && !check_simpledrm_resolution (backend, &outputs[i])) {
+ ret = false;
+ break;
+ }
+
+ if (check_if_output_has_changed (backend, &outputs[i]))
+ *changed = true;
+
+ if (outputs[i].connected)
+ backend->connected_count++;
+ }
+
+ if (!was_master)
+ drmDropMaster (backend->device_fd);
+
+ return ret;
+}
+
/* Update our outputs array to match the hardware state and
* create and/or remove heads as necessary.
* Returns true if any heads were modified.
@@ -1447,20 +1487,9 @@ create_heads_for_active_connectors (ply_
outputs = calloc (backend->resources->count_connectors, sizeof(*outputs));
outputs_len = backend->resources->count_connectors;
- backend->connected_count = 0;
- for (i = 0; i < outputs_len; i++) {
- get_output_info (backend, backend->resources->connectors[i], &outputs[i]);
-
- if (!force && !check_simpledrm_resolution (backend, &outputs[i])) {
- free (outputs);
- return false;
- }
-
- if (check_if_output_has_changed (backend, &outputs[i]))
- changed = true;
-
- if (outputs[i].connected)
- backend->connected_count++;
+ if (!probe_outputs (backend, outputs, outputs_len, force, &changed)) {
+ free (outputs);
+ return false;
}
/* Step 2:
@@ -0,0 +1,122 @@
From 7d73b0fd2746cd24c758dd75fc73ac92b5162d0b Mon Sep 17 00:00:00 2001
From: Daniel Golle <daniel@makrotopia.org>
Date: Fri, 25 Sep 2026 13:20:37 +0100
Subject: [PATCH] Ignore the escape key when the VT shows nothing
Escape toggles between the splash and the details view, guarded on
there being a VT console to show the details on. The guard asks only
whether the local console terminal is a VT, which it is whenever the
kernel has CONFIG_VT and a /dev/tty1 node exists, even where no console
driver draws that VT.
On a system built without a framebuffer console that leaves only the
dummy driver bound, so escape blanks the screen: the splash is torn
down, the details view writes to a VT nobody renders, and the display
stays dark until escape is pressed again.
Require a bound console driver other than the dummy one before treating
a VT as somewhere the details view can be read.
Signed-off-by: Daniel Golle <daniel@makrotopia.org>
---
src/main.c | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
--- a/src/main.c
+++ b/src/main.c
@@ -36,6 +36,7 @@
#include <assert.h>
#include <values.h>
#include <locale.h>
+#include <string.h>
#include <linux/kd.h>
#include <linux/vt.h>
@@ -56,6 +57,7 @@
#include "ply-progress.h"
#include "ply-kmsg-reader.h"
+#define VT_CONSOLE_DIRECTORY "/sys/class/vtconsole"
#define BOOT_DURATION_FILE PLYMOUTH_TIME_DIRECTORY "/boot-duration"
#define SHUTDOWN_DURATION_FILE PLYMOUTH_TIME_DIRECTORY "/shutdown-duration"
@@ -1638,6 +1640,70 @@ toggle_between_splash_and_details (state
}
}
+static bool
+vt_console_driver_draws (const char *directory)
+{
+ char path[PATH_MAX];
+ char name[64];
+ FILE *file;
+ bool draws;
+
+ snprintf (path, sizeof(path), "%s/bind", directory);
+ file = fopen (path, "r");
+
+ if (file == NULL)
+ return false;
+
+ draws = (fgetc (file) == '1');
+ fclose (file);
+
+ if (!draws)
+ return false;
+
+ snprintf (path, sizeof(path), "%s/name", directory);
+ file = fopen (path, "r");
+
+ if (file == NULL)
+ return false;
+
+ if (fgets (name, sizeof(name), file) == NULL)
+ name[0] = '\0';
+
+ fclose (file);
+
+ return strstr (name, "dummy") == NULL;
+}
+
+/* The dummy console driver puts nothing on screen, so a VT it is the only
+ * driver for cannot show the details view.
+ */
+static bool
+vt_console_is_visible (void)
+{
+ char directory[PATH_MAX];
+ struct dirent *entry;
+ bool visible = false;
+ DIR *consoles;
+
+ consoles = opendir (VT_CONSOLE_DIRECTORY);
+
+ if (consoles == NULL)
+ return true;
+
+ while (!visible && (entry = readdir (consoles)) != NULL) {
+ if (strncmp (entry->d_name, "vtcon", strlen ("vtcon")) != 0)
+ continue;
+
+ snprintf (directory, sizeof(directory), "%s/%s",
+ VT_CONSOLE_DIRECTORY, entry->d_name);
+ visible = vt_console_driver_draws (directory);
+ }
+
+ closedir (consoles);
+
+ return visible;
+}
+
static void
on_escape_pressed (state_t *state)
{
@@ -1647,6 +1713,8 @@ on_escape_pressed (state_t *state)
if (state->local_console_terminal != NULL) {
if (!ply_terminal_is_vt (state->local_console_terminal))
has_vt_consoles = false;
+ else if (!vt_console_is_visible ())
+ has_vt_consoles = false;
} else {
has_vt_consoles = false;
}
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
# Neither binary reports a version: upstream's command parser has no
# --version and prints only a usage summary. Check that both still
# identify themselves and exit cleanly.
/usr/bin/plymouth --help 2>&1 | grep -q '^Splash control client$' || exit 1
/usr/sbin/plymouthd --help 2>&1 | grep -q '^Splash server$' || exit 1
exit 0