* Re: [PATCH v3] printk: fix zero-valued printk timestamps in early boot
From: Petr Mladek @ 2026-03-04 11:23 UTC (permalink / raw)
To: Tim Bird
Cc: rostedt, john.ogness, senozhatsky, francesco, geert,
linux-embedded, linux-kernel
In-Reply-To: <20260210234741.3262320-1-tim.bird@sony.com>
On Tue 2026-02-10 16:47:41, Tim Bird wrote:
> During early boot, printk timestamps are reported as zero before
> kernel timekeeping starts (e.g. before time_init()). This
> hinders boot-time optimization efforts. This period is about 400
> milliseconds for many current desktop and embedded machines
> running Linux.
>
> Add support to save cycles during early boot, and output correct
> timestamp values after timekeeping is initialized. get_cycles()
> is operational on arm64 and x86_64 from kernel start. Add code
> and variables to save calibration values used to later convert
> cycle counts to time values in the early printks. Add a config
> to control the feature.
>
> This yields non-zero timestamps for printks from the very start
> of kernel execution. The timestamps are relative to the start of
> the architecture-specified counter used in get_cycles
> (e.g. the TSC on x86_64 and cntvct_el0 on arm64).
>
> All timestamps reflect time from processor power-on instead of
> time from the kernel's timekeeping initialization.
>
> Signed-off-by: Tim Bird <tim.bird@sony.com>
It looks good to me and seems to work fine. Feel free to use:
Reviewed-by: Petr Mladek <pmladek@suse.com>
Tested-by: Petr Mladek <pmladek@suse.com>
See a note below.
> --- a/init/main.c
> +++ b/init/main.c
> @@ -104,6 +104,7 @@
> #include <linux/pidfs.h>
> #include <linux/ptdump.h>
> #include <linux/time_namespace.h>
> +#include <linux/early_times.h>
JFYI, I have tried this patch on top of the current Linus' tree (v7.0-rc2+)
and it conflicted with the commit 499f86de4f8c34e19 ("init/main: read
bootconfig header with get_unaligned_le32()") which added here:
#include <linux/unaligned.h>
> #include <net/net_namespace.h>
>
> #include <asm/io.h>
Best Regards,
Petr
^ permalink raw reply
* Reminder of Boot-Time SIG meeting (Feb 24)
From: Bird, Tim @ 2026-02-23 23:01 UTC (permalink / raw)
To: Linux Embedded
Hey Linux Boot-Time SIG interested parties (and other interested Linux kernel developers),
Sorry for the late notice. I intended to send this last week.
Here is the information for the next Linux Boot-Time SIG conference call.
The meeting will be held via the Jitsi online meeting platform.
To Join the meeting via web, click on:
https://meet.jit.si/LinuxBootTimeSIG
----
Our next meeting is Tuesday, January 24, at 9:00 am Mountain Standard Time.
See this link for other time zones:
https://www.timeanddate.com/worldclock/meetingdetails.html?year=2026&month=02&day=24&hour=16&min=0&sec=0&p1=220&p2=137&p3=195&p4=771
(That makes it 8:00 am Pacific, 15:00 UTC, 16:00 CET, and 20:30 IST)
I'm planning on 1 hour for this meeting.
The agenda for the meeting (and where we'll keep the minutes) is here:
https://docs.google.com/document/d/1XAufoTT6VVJOTMzKMoz8SyOss-JA9H4J1_yVXQq5mN0/edit?usp=sharing
The agenda for the February 24 meeting will be available in the above document before the call.
To be honest, I haven't gotten a ton of things done on boot time in the last month. But others
may have. I'm shifting gears now to look at two areas of focus 1) boot-time reduction through
automated feedback (tuning wizard), and 2) examining delays in user space.
If you have items you'd like to add to the agenda, please let me know.
(Please e-mail me and/or put them in the document.)
I look forward to talking to you then.
Thanks,
-- Tim
^ permalink raw reply
* [PATCH v3] printk: fix zero-valued printk timestamps in early boot
From: Tim Bird @ 2026-02-10 23:47 UTC (permalink / raw)
To: pmladek, rostedt, john.ogness, senozhatsky
Cc: francesco, geert, linux-embedded, linux-kernel, Tim Bird
In-Reply-To: <39b09edb-8998-4ebd-a564-7d594434a981@bird.org>
During early boot, printk timestamps are reported as zero before
kernel timekeeping starts (e.g. before time_init()). This
hinders boot-time optimization efforts. This period is about 400
milliseconds for many current desktop and embedded machines
running Linux.
Add support to save cycles during early boot, and output correct
timestamp values after timekeeping is initialized. get_cycles()
is operational on arm64 and x86_64 from kernel start. Add code
and variables to save calibration values used to later convert
cycle counts to time values in the early printks. Add a config
to control the feature.
This yields non-zero timestamps for printks from the very start
of kernel execution. The timestamps are relative to the start of
the architecture-specified counter used in get_cycles
(e.g. the TSC on x86_64 and cntvct_el0 on arm64).
All timestamps reflect time from processor power-on instead of
time from the kernel's timekeeping initialization.
Signed-off-by: Tim Bird <tim.bird@sony.com>
---
V2->V3
Default CONFIG option to 'n'
Move more code from into early_times.h
(reducing ifdefs in init/main.c)
Use math64 helper routines
Use cycles_t instead of u64 type
Add #defines for EARLY_CYCLES_BIT and MASK
Invert if logic in adjust_early_ts()
(note: no change to 'depends on' in Kconfig entry)
V1->V2
Remove calibration CONFIG vars
Add 'depends on' to restrict arches (to handle ppc bug)
Add early_ts_offset to avoid discontinuity
Save cycles in ts_nsec, and convert on output
Move conditional code to include file (early_times.h)
include/linux/early_times.h | 85 +++++++++++++++++++++++++++++++++++++
init/Kconfig | 14 ++++++
init/main.c | 6 +++
kernel/printk/printk.c | 18 +++++++-
4 files changed, 121 insertions(+), 2 deletions(-)
create mode 100644 include/linux/early_times.h
diff --git a/include/linux/early_times.h b/include/linux/early_times.h
new file mode 100644
index 000000000000..05388dcb8573
--- /dev/null
+++ b/include/linux/early_times.h
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
+#define _KERNEL_PRINTK_EARLY_TIMES_H
+
+#include <linux/timex.h>
+#include <linux/clocksource.h>
+
+/* use high bit of a u64 to indicate cycles instead of a timestamp */
+#define EARLY_CYCLES_BIT BIT_ULL(63)
+#define EARLY_CYCLES_MASK ~(BIT_ULL(63))
+
+#if defined(CONFIG_EARLY_PRINTK_TIMES)
+extern cycles_t start_cycles;
+extern u64 start_ns;
+extern u32 early_mult, early_shift;
+extern u64 early_ts_offset;
+
+static inline void early_times_start_calibration(void)
+{
+ start_cycles = get_cycles();
+ start_ns = local_clock();
+}
+
+static inline void early_times_finish_calibration(void)
+{
+ cycles_t end_cycles;
+ u64 end_ns;
+
+ /* set calibration data for early_printk_times */
+ end_cycles = get_cycles();
+ end_ns = local_clock();
+ clocks_calc_mult_shift(&early_mult, &early_shift,
+ mul_u64_u64_div_u64(end_cycles - start_cycles,
+ NSEC_PER_SEC, end_ns - start_ns),
+ NSEC_PER_SEC, 100);
+ early_ts_offset = mul_u64_u32_shr(start_cycles, early_mult, early_shift) - start_ns;
+
+ pr_debug("Early printk times: mult=%u, shift=%u, offset=%llu ns\n",
+ early_mult, early_shift, early_ts_offset);
+}
+
+static inline u64 early_cycles(void)
+{
+ return (get_cycles() | EARLY_CYCLES_BIT);
+}
+
+/*
+ * adjust_early_ts detects whether ts in is cycles or nanoseconds
+ * and converts it or adjusts it, taking into account the offset
+ * from cycle-counter start.
+ *
+ * Note that early_mult may be 0, but that's OK because
+ * we'll just multiply by 0 and return 0. This will
+ * only occur if we're outputting a printk message
+ * before the calibration of the early timestamp.
+ * Any output after user space start (eg. from dmesg or
+ * journalctl) will show correct values.
+ */
+static inline u64 adjust_early_ts(u64 ts)
+{
+ if (likely(!(ts & EARLY_CYCLES_BIT)))
+ /* if timestamp is not in cycles, just add offset */
+ return ts + early_ts_offset;
+
+ /* mask high bit and convert to nanoseconds */
+ return mul_u64_u32_shr(ts & EARLY_CYCLES_MASK, early_mult, early_shift);
+}
+
+#else
+# define early_times_start_calibration() do { } while (0)
+# define early_times_finish_calibration() do { } while (0)
+
+static inline u64 early_cycles(void)
+{
+ return 0;
+}
+
+static inline u64 adjust_early_ts(u64 ts)
+{
+ return ts;
+}
+#endif /* CONFIG_EARLY_PRINTK_TIMES */
+
+#endif /* _KERNEL_PRINTK_EARLY_TIMES_H */
diff --git a/init/Kconfig b/init/Kconfig
index fa79feb8fe57..a928c1efb09d 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -777,6 +777,20 @@ config IKHEADERS
or similar programs. If you build the headers as a module, a module called
kheaders.ko is built which can be loaded on-demand to get access to headers.
+config EARLY_PRINTK_TIMES
+ bool "Show non-zero printk timestamps early in boot"
+ default n
+ depends on PRINTK
+ depends on ARM64 || X86_64
+ help
+ Use a cycle-counter to provide printk timestamps during
+ early boot. This allows seeing timestamps for printks that
+ would otherwise show as 0. Note that this will shift the
+ printk timestamps to be relative to processor power on, instead
+ of relative to the start of kernel timekeeping. This should be
+ closer to machine power on, giving a better indication of
+ overall boot time.
+
config LOG_BUF_SHIFT
int "Kernel log buffer size (16 => 64KB, 17 => 128KB)"
range 12 25
diff --git a/init/main.c b/init/main.c
index b84818ad9685..d5774aec1aff 100644
--- a/init/main.c
+++ b/init/main.c
@@ -104,6 +104,7 @@
#include <linux/pidfs.h>
#include <linux/ptdump.h>
#include <linux/time_namespace.h>
+#include <linux/early_times.h>
#include <net/net_namespace.h>
#include <asm/io.h>
@@ -1118,6 +1119,9 @@ void start_kernel(void)
timekeeping_init();
time_init();
+ /* This must be after timekeeping is initialized */
+ early_times_start_calibration();
+
/* This must be after timekeeping is initialized */
random_init();
@@ -1600,6 +1604,8 @@ static int __ref kernel_init(void *unused)
do_sysctl_args();
+ early_times_finish_calibration();
+
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index 1d765ad242b8..5afd31c3345c 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -46,6 +46,7 @@
#include <linux/ctype.h>
#include <linux/uio.h>
#include <linux/sched/clock.h>
+#include <linux/early_times.h>
#include <linux/sched/debug.h>
#include <linux/sched/task_stack.h>
#include <linux/panic.h>
@@ -75,6 +76,13 @@ EXPORT_SYMBOL(ignore_console_lock_warning);
EXPORT_TRACEPOINT_SYMBOL_GPL(console);
+#ifdef CONFIG_EARLY_PRINTK_TIMES
+cycles_t start_cycles;
+u64 start_ns;
+u32 early_mult, early_shift;
+u64 early_ts_offset;
+#endif
+
/*
* Low level drivers may need that to know if they can schedule in
* their unblank() callback or not. So let's export it.
@@ -639,7 +647,7 @@ static void append_char(char **pp, char *e, char c)
static ssize_t info_print_ext_header(char *buf, size_t size,
struct printk_info *info)
{
- u64 ts_usec = info->ts_nsec;
+ u64 ts_usec = adjust_early_ts(info->ts_nsec);
char caller[20];
#ifdef CONFIG_PRINTK_CALLER
u32 id = info->caller_id;
@@ -1352,7 +1360,11 @@ static size_t print_syslog(unsigned int level, char *buf)
static size_t print_time(u64 ts, char *buf)
{
- unsigned long rem_nsec = do_div(ts, 1000000000);
+ unsigned long rem_nsec;
+
+ ts = adjust_early_ts(ts);
+
+ rem_nsec = do_div(ts, 1000000000);
return sprintf(buf, "[%5lu.%06lu]",
(unsigned long)ts, rem_nsec / 1000);
@@ -2242,6 +2254,8 @@ int vprintk_store(int facility, int level,
* timestamp with respect to the caller.
*/
ts_nsec = local_clock();
+ if (!ts_nsec)
+ ts_nsec = early_cycles();
caller_id = printk_caller_id();
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v2] printk: fix zero-valued printk timestamps in early boot
From: Petr Mladek @ 2026-02-02 16:23 UTC (permalink / raw)
To: Bird, Tim
Cc: Francesco Valla, rostedt@goodmis.org, john.ogness@linuxtronix.de,
senozhatsky@chromium.org, linux-embedded@vger.kernel.org,
linux-kernel@vger.kernel.org
In-Reply-To: <MW5PR13MB5632FE304C7FA3D86309D6F1FD93A@MW5PR13MB5632.namprd13.prod.outlook.com>
On Mon 2026-01-26 16:52:57, Bird, Tim wrote:
>
>
> > -----Original Message-----
> > From: Francesco Valla <francesco@valla.it>
> >
> > Hi Tim,
> >
> > I tested this both on X86_64 QEMU and on a i.MX93 (ARM64) and can
> > confirm it is working as expected. Auto-calc of calibration data is far
> > better than the configuration parameters in v1.
> >
> > It is slightly confusing to see a time value printed to serial output
> > and another one inside kmsg, but that's a human thing and should not
> > confuse any tool.
> Agreed. I wasn't too worried about it, because most serious developers working
> on boot-time will not be watching early messages over serial console. (Usually they
> use 'quiet' or some lower log level). But on qemu, it does look strange to see 0s
> on the first output sequence, and then non-zeroes when using dmesg later in the same
> boot.
>
> I just realized though, that I should go back and see if there's a discontinuity on the output via serial
> (before and after calibration), and possibly put a note about that in the config description.
I see the following in the serial console output:
[ 3.288049][ T1] Write protecting the kernel read-only data: 36864k
[ 3.298554][ T1] Freeing unused kernel image (text/rodata gap) memory: 1656K
[ 3.318942][ T1] Freeing unused kernel image (rodata/data gap) memory: 1540K
[ 12.230014][ T1] Early printk times: mult=38775352, shift=27, offset=8891950261 ns
[ 12.246008][ T1] Run /init as init process
[ 12.254944][ T1] with arguments:
[ 12.264341][ T1] /init
[ 12.272184][ T1] nosplash
And this is from dmesg -S
[ 12.179999] [ T1] Write protecting the kernel read-only data: 36864k
[ 12.190505] [ T1] Freeing unused kernel image (text/rodata gap) memory: 1656K
[ 12.210893] [ T1] Freeing unused kernel image (rodata/data gap) memory: 1540K
[ 12.230014] [ T1] Early printk times: mult=38775352, shift=27, offset=8891950261 ns
[ 12.246008] [ T1] Run /init as init process
[ 12.254944] [ T1] with arguments:
[ 12.264341] [ T1] /init
[ 12.272184] [ T1] nosplash
> I'll think about what I can do here to reduce the confusion.
I though about showing the non-adjusted timestamp with '?',
Something like:
[ 3.288049?][ T1] Write protecting the kernel read-only data: 36864k
[ 3.298554?][ T1] Freeing unused kernel image (text/rodata gap) memory: 1656K
[ 3.318942?][ T1] Freeing unused kernel image (rodata/data gap) memory: 1540K
[ 12.230014][ T1] Early printk times: mult=38775352, shift=27, offset=8891950261 ns
[ 12.246008][ T1] Run /init as init process
[ 12.254944][ T1] with arguments:
[ 12.264341][ T1] /init
[ 12.272184][ T1] nosplash
But I am afraid that it might break some monitoring tools.
Well, it might be acceptable when this feature is not enabled
in production systems.
> > > --- a/init/Kconfig
> > > +++ b/init/Kconfig
> > > @@ -777,6 +777,18 @@ config IKHEADERS
> > > or similar programs. If you build the headers as a module, a module called
> > > kheaders.ko is built which can be loaded on-demand to get access to headers.
> > >
> > > +config EARLY_PRINTK_TIMES
> > > + bool "Show non-zero printk timestamps early in boot"
> > > + default y
> >
> > Considering that this might have a significant impact on monitoring
> > mechanisms already in place (that e.g. expect a specific dmesg print to
> > have a maximum associated time value), please consider a N default here.
>
> Oops! Sorry, that was supposed to be 'default n'. You're right. I know I had
> this as default N, and I think I switched it temporarily for testing, and forgot
> to switch it back (and never caught it the numerous times I reviewed the
> patch before sending it out again, ugh). Thanks for catching this.
>
> If people like this, and we don't see any problems with tooling or virtualization, I
> could see it switching to default Y in the future. But for now this should definitely
> be 'default n'.
We need to be careful. The different output on console and via dmesg
might confuse people. The extra '?' might help poeple but it might confuse
tools.
> >
> > > + depends on PRINTK
> > > + depends on ARM64 || X86_64
> > > + help
> > > + Use a cycle-counter to provide printk timestamps during
> > > + early boot. This allows seeing timestamps for printks that
> > > + would otherwise show as 0. Note that this will shift the
> > > + printk timestamps to be relative to machine power on, instead
> > > + of relative to the start of kernel timekeeping.
> > > +
> >
> > To be precise, the timestamps will be relative to processor power on;
> > the machine might have some other processors that run before the Linux
> > one (this is the case for example of i.MX9 or AM62 SoCs) and will be
> > unaccounted for even by this mechanism.
>
> Good point. Even more precisely, it will be relative to
> cycle-counter value initialization or reset, which often (but not always)
> corresponds to processor power on.
>
> I'll adjust the wording.
>
> I'm still a bit unsure what happens in the virtualization case. qemu seems to initialize
> the TSC at qemu start, but I'm not sure what happens for e.g. client VMs on cloud servers.
I see the following via QEMU (from dmesg):
[ 8.853613] Linux version 6.19.0-rc7-default+ (pmladek@pathway) (gcc (SUSE Linux) 15.2.1 20251006, GNU ld (GNU Binutils; openSUSE Tumbleweed) 2.45.0.20251103-2) #521 SMP PREEMPT_DYNAMIC Mon Feb 2 16:36:53 CET 2026
[ 8.853617] Command line: BOOT_IMAGE=/boot/vmlinuz-6.19.0-rc7-default+ root=UUID=587ae802-e330-4059-9b48-d5b845e1075a resume=/dev/disk/by-uuid/369c7453-3d16-409d-88b2-5de027891a12 mitigations=auto nosplash earlycon=uart8250,io,0x3f8,115200 console=ttyS0,115200 console=ttynull console=tty0 debug_non_panic_cpus=1 panic=10 ignore_loglevel log_buf_len=1M
[ 8.865086] BIOS-provided physical RAM map:
[ 8.865087] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[ 8.865089] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[ 8.865090] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[ 8.865090] BIOS-e820: [mem 0x0000000000100000-0x000000007ffdbfff] usable
[ 8.865091] BIOS-e820: [mem 0x000000007ffdc000-0x000000007fffffff] reserved
[ 8.865092] BIOS-e820: [mem 0x00000000b0000000-0x00000000bfffffff] reserved
[ 8.865092] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[ 8.865093] BIOS-e820: [mem 0x00000000feffc000-0x00000000feffffff] reserved
[ 8.865093] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[ 8.865094] BIOS-e820: [mem 0x0000000100000000-0x000000017fffffff] usable
[ 8.865094] BIOS-e820: [mem 0x000000fd00000000-0x000000ffffffffff] reserved
[ 8.865171] earlycon: uart8250 at I/O port 0x3f8 (options '115200')
[ 8.865176] printk: legacy bootconsole [uart8250] enabled
[ 8.892181] printk: allow messages from non-panic CPUs in panic()
[ 8.893327] printk: debug: ignoring loglevel setting.
[...]
[ 12.162011] Freeing unused decrypted memory: 2036K
[ 12.171970] Freeing unused kernel image (initmem) memory: 7120K
[ 12.179999] Write protecting the kernel read-only data: 36864k
[ 12.190505] Freeing unused kernel image (text/rodata gap) memory: 1656K
[ 12.210893] Freeing unused kernel image (rodata/data gap) memory: 1540K
[ 12.230014] Early printk times: mult=38775352, shift=27, offset=8891950261 ns
[ 12.246008] Run /init as init process
[ 12.254944] with arguments:
[ 12.264341] /init
[ 12.272184] nosplash
[ 12.280738] with environment:
[ 12.288728] HOME=/
[ 12.296319] TERM=linux
Best Regards,
Petr
^ permalink raw reply
* Re: [PATCH v2] printk: fix zero-valued printk timestamps in early boot
From: Geert Uytterhoeven @ 2026-01-27 8:10 UTC (permalink / raw)
To: Bird, Tim
Cc: pmladek@suse.com, rostedt@goodmis.org, john.ogness@linuxtronix.de,
senozhatsky@chromium.org, francesco@valla.it,
linux-embedded@vger.kernel.org, linux-kernel@vger.kernel.org
In-Reply-To: <MW5PR13MB5632CEFB9AB7AFDA7334307BFD93A@MW5PR13MB5632.namprd13.prod.outlook.com>
Hi Tim,
On Mon, 26 Jan 2026 at 18:11, Bird, Tim <Tim.Bird@sony.com> wrote:
> > From: Geert Uytterhoeven <geert@linux-m68k.org>
> > On Sat, 24 Jan 2026 at 20:41, Tim Bird <tim.bird@sony.com> wrote:
> > > During early boot, printk timestamps are reported as zero before
> > > kernel timekeeping starts (e.g. before time_init()). This
> > > hinders boot-time optimization efforts. This period is about 400
> > > milliseconds for many current desktop and embedded machines
> > > running Linux.
> > >
> > > Add support to save cycles during early boot, and output correct
> > > timestamp values after timekeeping is initialized. get_cycles()
> > > is operational on arm64 and x86_64 from kernel start. Add code
> > > and variables to save calibration values used to later convert
> > > cycle counts to time values in the early printks. Add a config
> > > to control the feature.
> > >
> > > This yields non-zero timestamps for printks from the very start
> > > of kernel execution. The timestamps are relative to the start of
> > > the architecture-specified counter used in get_cycles
> > > (e.g. the TSC on x86_64 and cntvct_el0 on arm64).
> > >
> > > All timestamps reflect time from power-on instead of time from
> > > the kernel's timekeeping initialization.
> > >
> > > Signed-off-by: Tim Bird <tim.bird@sony.com>
> > > ---
> > > V1 -> V2
> > > Remove calibration CONFIG vars
> > > Add 'depends on' to restrict arches (to handle ppc bug)
> > > Add early_ts_offset to avoid discontinuity
> > > Save cycles in ts_nsec, and convert on output
> > > Move conditional code to include file (early_times.h)
> >
> > Thanks for the update!
> >
> > > --- /dev/null
> > > +++ b/include/linux/early_times.h
> > > @@ -0,0 +1,48 @@
> > > +/* SPDX-License-Identifier: GPL-2.0 */
> > > +
> > > +#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
> > > +#define _KERNEL_PRINTK_EARLY_TIMES_H
> > > +
> > > +#include <linux/timex.h>
> > > +
> > > +#if defined(CONFIG_EARLY_PRINTK_TIMES)
> > > +extern u32 early_mult, early_shift;
> > > +extern u64 early_ts_offset;
> > > +
> > > +static inline u64 early_cycles(void)
> > > +{
> > > + return ((u64)get_cycles() | (1ULL << 63));
> >
> > No need to cast to u64, as the second operand of the OR is u64 anyway.
> > BIT_ULL(63)
> >
> > I think it would be good to have a #define for this at the top.
>
> I'll look at this. Is BIT_ULL(63) preferred over (1ULL << 63)?
When you refer to the bit value, yes: BIT() for unsigned long, BIT_ULL()
for unsigned long long; recently we got BIT_U{8,16,32,64}(), too).
> Do you think something like "HIGH_BIT63" would be good enough?
I'd name it for what it means, not what it does, e.g. EARLY_TS_FLAG?
> > > +}
> > > +
> > > +static inline u64 adjust_early_ts(u64 ts)
> > > +{
> > > + /* High bit means ts is a cycle count */
> > > + if (unlikely(ts & (1ULL << 63)))
> > > + /*
> > > + * mask high bit and convert to ns
> > > + * Note that early_mult may be 0, but that's OK because
> > > + * we'll just multiply by 0 and return 0. This will
> > > + * only occur if we're outputting a printk message
> > > + * before the calibration of the early timestamp.
> > > + * Any output after user space start (eg. from dmesg or
> > > + * journalctl) will show correct values.
> > > + */
> > > + return (((ts & ~(1ULL << 63)) * early_mult) >> early_shift);
> >
> > Please use the mul_u64_u32_shr() helper.
> OK. I did not know about that.
>
> I can check, but do you know offhand if timestamps from local_clock() on 32-bit systems are
> always 64-bit nanoseconds? I assume so looking at the printk code and
> making some assumptions. (But that's dangerous.)
I am not 100% sure, but I think they are. Note that on systems
without high-resolution timers, they may increment in large steps,
e.g. by 10000000 if HZ=100.
> > > --- a/init/Kconfig
> > > +++ b/init/Kconfig
> > > @@ -777,6 +777,18 @@ config IKHEADERS
> > > or similar programs. If you build the headers as a module, a module called
> > > kheaders.ko is built which can be loaded on-demand to get access to headers.
> > >
> > > +config EARLY_PRINTK_TIMES
> > > + bool "Show non-zero printk timestamps early in boot"
> > > + default y
> > > + depends on PRINTK
> > > + depends on ARM64 || X86_64
> >
> > So for now this is limited to (a few) 64-bit platforms...
>
> Yes, but it really shouldn't be. I got spooked when 0-day told me that the code
> wouldn't link on powerpc, so I restricted it to just machines I was actually testing.
> But this should work on some ARM32 and most x8632 platforms. Actually, it should
> work on anything that has a cycle-counter from kernel start (some RISC machine
> might qualify as well). However, I've seen a few cases where some platforms require
> kernel initialization of their cycle-counter, and I wanted to play it safe.
>
> If such platforms are well-behaved and return 0 before they are initialized, it should
> still be safe to turn this on - it just won't have any effect.
At least on m68k, get_cycles() always returns zero ;-)
> I plan to do some more investigation of the powerpc error. It was
> powerpc-linux-ld: init/main.o: in function `kernel_init':
> main.c:(.ref.text+0x144): undefined reference to `__udivdi3'
>
> from the definition of clocks_calc_mult_shift(), which seems to indicate a bug
> in the powerpc code. In the long run, I should try to track down that bug rather than
> exclude a bunch of other (likely-working) arches. But I was playing it conservative for now.
An undefined reference to __udivdi3 means you are using a 64-by-32
division, for which you should always use the helpers from
<linux/math64.h>. Even on 64-bit, the helpers may generate better code.
> > > --- a/init/main.c
> > > +++ b/init/main.c
> > > +
> > > + /* set calibration data for early_printk_times */
> > > + end_cycles = get_cycles();
> > > + end_ns = local_clock();
> > > + clocks_calc_mult_shift(&early_mult, &early_shift,
> > > + ((end_cycles - start_cycles) * NSEC_PER_SEC)/(end_ns - start_ns),
> > > + NSEC_PER_SEC, 50);
> >
> > mul_u64_u64_div_u64() is probably the best helper that is available
> > (there is no mul_ulong_u32_div_u64()).
>
> What would the mul_u64_u64_div_u64 do if cycles_t is u32?
> Should it sill all work, just not optimized?
It should still work.
arch/x86/include/asm/div64.h uses divq, so it is up to Intel or AMD.
Everything else uses lib/math/div64.c.
If need, you can add an optimized version for e.g. arm64, too.
Gr{oetje,eeting}s,
Geert
--
Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-m68k.org
In personal conversations with technical people, I call myself a hacker. But
when I'm talking to journalists I just say "programmer" or something like that.
-- Linus Torvalds
^ permalink raw reply
* RE: [PATCH v2] printk: fix zero-valued printk timestamps in early boot
From: Bird, Tim @ 2026-01-26 17:11 UTC (permalink / raw)
To: Geert Uytterhoeven
Cc: pmladek@suse.com, rostedt@goodmis.org, john.ogness@linuxtronix.de,
senozhatsky@chromium.org, francesco@valla.it,
linux-embedded@vger.kernel.org, linux-kernel@vger.kernel.org
In-Reply-To: <CAMuHMdV9XQiBx1QBDoaqJB+CwNp0EQbeJuFpe=yCqfAe1inXWA@mail.gmail.com>
> -----Original Message-----
> From: Geert Uytterhoeven <geert@linux-m68k.org>
> Hi Tim,
>
> On Sat, 24 Jan 2026 at 20:41, Tim Bird <tim.bird@sony.com> wrote:
> > During early boot, printk timestamps are reported as zero before
> > kernel timekeeping starts (e.g. before time_init()). This
> > hinders boot-time optimization efforts. This period is about 400
> > milliseconds for many current desktop and embedded machines
> > running Linux.
> >
> > Add support to save cycles during early boot, and output correct
> > timestamp values after timekeeping is initialized. get_cycles()
> > is operational on arm64 and x86_64 from kernel start. Add code
> > and variables to save calibration values used to later convert
> > cycle counts to time values in the early printks. Add a config
> > to control the feature.
> >
> > This yields non-zero timestamps for printks from the very start
> > of kernel execution. The timestamps are relative to the start of
> > the architecture-specified counter used in get_cycles
> > (e.g. the TSC on x86_64 and cntvct_el0 on arm64).
> >
> > All timestamps reflect time from power-on instead of time from
> > the kernel's timekeeping initialization.
> >
> > Signed-off-by: Tim Bird <tim.bird@sony.com>
> > ---
> > V1 -> V2
> > Remove calibration CONFIG vars
> > Add 'depends on' to restrict arches (to handle ppc bug)
> > Add early_ts_offset to avoid discontinuity
> > Save cycles in ts_nsec, and convert on output
> > Move conditional code to include file (early_times.h)
>
> Thanks for the update!
>
> > --- /dev/null
> > +++ b/include/linux/early_times.h
> > @@ -0,0 +1,48 @@
> > +/* SPDX-License-Identifier: GPL-2.0 */
> > +
> > +#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
> > +#define _KERNEL_PRINTK_EARLY_TIMES_H
> > +
> > +#include <linux/timex.h>
> > +
> > +#if defined(CONFIG_EARLY_PRINTK_TIMES)
> > +extern u32 early_mult, early_shift;
> > +extern u64 early_ts_offset;
> > +
> > +static inline u64 early_cycles(void)
> > +{
> > + return ((u64)get_cycles() | (1ULL << 63));
>
> No need to cast to u64, as the second operand of the OR is u64 anyway.
> BIT_ULL(63)
>
> I think it would be good to have a #define for this at the top.
I'll look at this. Is BIT_ULL(63) preferred over (1ULL << 63)?
Do you think something like "HIGH_BIT63" would be good enough?
>
> > +}
> > +
> > +static inline u64 adjust_early_ts(u64 ts)
> > +{
> > + /* High bit means ts is a cycle count */
> > + if (unlikely(ts & (1ULL << 63)))
> > + /*
> > + * mask high bit and convert to ns
> > + * Note that early_mult may be 0, but that's OK because
> > + * we'll just multiply by 0 and return 0. This will
> > + * only occur if we're outputting a printk message
> > + * before the calibration of the early timestamp.
> > + * Any output after user space start (eg. from dmesg or
> > + * journalctl) will show correct values.
> > + */
> > + return (((ts & ~(1ULL << 63)) * early_mult) >> early_shift);
>
> Please use the mul_u64_u32_shr() helper.
OK. I did not know about that.
I can check, but do you know offhand if timestamps from local_clock() on 32-bit systems are
always 64-bit nanoseconds? I assume so looking at the printk code and
making some assumptions. (But that's dangerous.)
>
> Please wrap this block in curly braces.
> Alternatively, you can invert the logic:
>
> if (likely(!(ts & DEFINITION_FOR_1ULL_LSH63)))
> return ts + early_ts_offset;
This is probably a better structure anyway. Will do.
>
> > +
> > + /* If timestamp is already in ns, just add offset */
> > + return ts + early_ts_offset;
> > +}
> > +#else
> > +static inline u64 early_cycles(void)
> > +{
> > + return 0;
> > +}
> > +
> > +static inline u64 adjust_early_ts(u64 ts)
> > +{
> > + return ts;
> > +}
> > +#endif /* CONFIG_EARLY_PRINTK_TIMES */
> > +
> > +#endif /* _KERNEL_PRINTK_EARLY_TIMES_H */
> > +
> > diff --git a/init/Kconfig b/init/Kconfig
> > index fa79feb8fe57..060a22cddd17 100644
> > --- a/init/Kconfig
> > +++ b/init/Kconfig
> > @@ -777,6 +777,18 @@ config IKHEADERS
> > or similar programs. If you build the headers as a module, a module called
> > kheaders.ko is built which can be loaded on-demand to get access to headers.
> >
> > +config EARLY_PRINTK_TIMES
> > + bool "Show non-zero printk timestamps early in boot"
> > + default y
> > + depends on PRINTK
> > + depends on ARM64 || X86_64
>
> So for now this is limited to (a few) 64-bit platforms...
Yes, but it really shouldn't be. I got spooked when 0-day told me that the code
wouldn't link on powerpc, so I restricted it to just machines I was actually testing.
But this should work on some ARM32 and most x8632 platforms. Actually, it should
work on anything that has a cycle-counter from kernel start (some RISC machine
might qualify as well). However, I've seen a few cases where some platforms require
kernel initialization of their cycle-counter, and I wanted to play it safe.
If such platforms are well-behaved and return 0 before they are initialized, it should
still be safe to turn this on - it just won't have any effect.
I plan to do some more investigation of the powerpc error. It was
powerpc-linux-ld: init/main.o: in function `kernel_init':
main.c:(.ref.text+0x144): undefined reference to `__udivdi3'
from the definition of clocks_calc_mult_shift(), which seems to indicate a bug
in the powerpc code. In the long run, I should try to track down that bug rather than
exclude a bunch of other (likely-working) arches. But I was playing it conservative for now.
>
> > + help
> > + Use a cycle-counter to provide printk timestamps during
> > + early boot. This allows seeing timestamps for printks that
> > + would otherwise show as 0. Note that this will shift the
> > + printk timestamps to be relative to machine power on, instead
> > + of relative to the start of kernel timekeeping.
> > +
> > config LOG_BUF_SHIFT
> > int "Kernel log buffer size (16 => 64KB, 17 => 128KB)"
> > range 12 25
> > diff --git a/init/main.c b/init/main.c
> > index b84818ad9685..cc1af26933f7 100644
> > --- a/init/main.c
> > +++ b/init/main.c
>
> > @@ -160,6 +163,10 @@ static size_t initargs_offs;
> > # define initargs_offs 0
> > #endif
> >
> > +#ifdef CONFIG_EARLY_PRINTK_TIMES
> > +static u64 start_cycles, start_ns;
>
> cycles_t start_cycles;
>
> (cycles_t is unsigned long, i.e. either 32- or 64-bit).
OK, I’ll use this type.
>
> > +#endif
> > +
> > static char *execute_command;
> > static char *ramdisk_execute_command = "/init";
> >
> > @@ -1118,6 +1125,11 @@ void start_kernel(void)
> > timekeeping_init();
> > time_init();
> >
> > +#ifdef CONFIG_EARLY_PRINTK_TIMES
> > + start_cycles = get_cycles();
> > + start_ns = local_clock();
> > +#endif
> > +
> > /* This must be after timekeeping is initialized */
> > random_init();
> >
> > @@ -1600,6 +1612,20 @@ static int __ref kernel_init(void *unused)
> >
> > do_sysctl_args();
> >
> > +#ifdef CONFIG_EARLY_PRINTK_TIMES
> > + u64 end_cycles, end_ns;
>
> cycles_t end_cycles;
ack.
>
> > +
> > + /* set calibration data for early_printk_times */
> > + end_cycles = get_cycles();
> > + end_ns = local_clock();
> > + clocks_calc_mult_shift(&early_mult, &early_shift,
> > + ((end_cycles - start_cycles) * NSEC_PER_SEC)/(end_ns - start_ns),
> > + NSEC_PER_SEC, 50);
>
> mul_u64_u64_div_u64() is probably the best helper that is available
> (there is no mul_ulong_u32_div_u64()).
What would the mul_u64_u64_div_u64 do if cycles_t is u32?
Should it sill all work, just not optimized?
>
> > + early_ts_offset = ((start_cycles * early_mult) >> early_shift) - start_ns;
> > + pr_debug("Early printk times: mult=%u, shift=%u, offset=%llu ns\n",
> > + early_mult, early_shift, early_ts_offset);
> > +#endif
> > +
> > if (ramdisk_execute_command) {
> > ret = run_init_process(ramdisk_execute_command);
> > if (!ret)
>
> Gr{oetje,eeting}s,
>
> Geert
>
Thanks very much for the review and suggestions. I'm away from my lab for the next week,
but I'd really like to test this on a 32-bit platform that has an early cycle-counter available.
I'll have to do some research. I have some 32-bit platforms in my lab, but I'm not sure which ones
have early-cycle-counter support. Do any of the 32-bit platforms you're familiar with support
early cycle-counters (that is, cycle-counters that are running when the kernel starts, and don't
need any kernel initialization at all)?
-- Tim
^ permalink raw reply
* RE: [PATCH v2] printk: fix zero-valued printk timestamps in early boot
From: Bird, Tim @ 2026-01-26 16:52 UTC (permalink / raw)
To: Francesco Valla
Cc: pmladek@suse.com, rostedt@goodmis.org, john.ogness@linuxtronix.de,
senozhatsky@chromium.org, linux-embedded@vger.kernel.org,
linux-kernel@vger.kernel.org
In-Reply-To: <aXYrn8QFCSZ5Q8CB@bywater>
> -----Original Message-----
> From: Francesco Valla <francesco@valla.it>
>
> Hi Tim,
>
> I tested this both on X86_64 QEMU and on a i.MX93 (ARM64) and can
> confirm it is working as expected. Auto-calc of calibration data is far
> better than the configuration parameters in v1.
>
> It is slightly confusing to see a time value printed to serial output
> and another one inside kmsg, but that's a human thing and should not
> confuse any tool.
Agreed. I wasn't too worried about it, because most serious developers working
on boot-time will not be watching early messages over serial console. (Usually they
use 'quiet' or some lower log level). But on qemu, it does look strange to see 0s
on the first output sequence, and then non-zeroes when using dmesg later in the same
boot.
I just realized though, that I should go back and see if there's a discontinuity on the output via serial
(before and after calibration), and possibly put a note about that in the config description.
I'll think about what I can do here to reduce the confusion.
>
> Some notes follow.
>
> On Sat, Jan 24, 2026 at 12:40:27PM -0700, Tim Bird wrote:
> > During early boot, printk timestamps are reported as zero before
> > kernel timekeeping starts (e.g. before time_init()). This
> > hinders boot-time optimization efforts. This period is about 400
> > milliseconds for many current desktop and embedded machines
> > running Linux.
> >
> > Add support to save cycles during early boot, and output correct
> > timestamp values after timekeeping is initialized. get_cycles()
> > is operational on arm64 and x86_64 from kernel start. Add code
> > and variables to save calibration values used to later convert
> > cycle counts to time values in the early printks. Add a config
> > to control the feature.
> >
> > This yields non-zero timestamps for printks from the very start
> > of kernel execution. The timestamps are relative to the start of
> > the architecture-specified counter used in get_cycles
> > (e.g. the TSC on x86_64 and cntvct_el0 on arm64).
> >
> > All timestamps reflect time from power-on instead of time from
> > the kernel's timekeeping initialization.
> >
> > Signed-off-by: Tim Bird <tim.bird@sony.com>
> > ---
> > V1 -> V2
> > Remove calibration CONFIG vars
> > Add 'depends on' to restrict arches (to handle ppc bug)
> > Add early_ts_offset to avoid discontinuity
> > Save cycles in ts_nsec, and convert on output
> > Move conditional code to include file (early_times.h)
> > ---
> > include/linux/early_times.h | 48 +++++++++++++++++++++++++++++++++++++
> > init/Kconfig | 12 ++++++++++
> > init/main.c | 26 ++++++++++++++++++++
> > kernel/printk/printk.c | 16 +++++++++++--
> > 4 files changed, 100 insertions(+), 2 deletions(-)
> > create mode 100644 include/linux/early_times.h
> >
> > diff --git a/include/linux/early_times.h b/include/linux/early_times.h
> > new file mode 100644
> > index 000000000000..9dc31eb442c2
> > --- /dev/null
> > +++ b/include/linux/early_times.h
> > @@ -0,0 +1,48 @@
> > +/* SPDX-License-Identifier: GPL-2.0 */
> > +
> > +#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
> > +#define _KERNEL_PRINTK_EARLY_TIMES_H
> > +
> > +#include <linux/timex.h>
> > +
> > +#if defined(CONFIG_EARLY_PRINTK_TIMES)
> > +extern u32 early_mult, early_shift;
> > +extern u64 early_ts_offset;
> > +
> > +static inline u64 early_cycles(void)
> > +{
> > + return ((u64)get_cycles() | (1ULL << 63));
> > +}
> > +
> > +static inline u64 adjust_early_ts(u64 ts)
> > +{
> > + /* High bit means ts is a cycle count */
> > + if (unlikely(ts & (1ULL << 63)))
> > + /*
> > + * mask high bit and convert to ns
> > + * Note that early_mult may be 0, but that's OK because
> > + * we'll just multiply by 0 and return 0. This will
> > + * only occur if we're outputting a printk message
> > + * before the calibration of the early timestamp.
> > + * Any output after user space start (eg. from dmesg or
> > + * journalctl) will show correct values.
> > + */
> > + return (((ts & ~(1ULL << 63)) * early_mult) >> early_shift);
> > +
> > + /* If timestamp is already in ns, just add offset */
> > + return ts + early_ts_offset;
> > +}
> > +#else
> > +static inline u64 early_cycles(void)
> > +{
> > + return 0;
> > +}
> > +
> > +static inline u64 adjust_early_ts(u64 ts)
> > +{
> > + return ts;
> > +}
> > +#endif /* CONFIG_EARLY_PRINTK_TIMES */
> > +
> > +#endif /* _KERNEL_PRINTK_EARLY_TIMES_H */
> > +
> > diff --git a/init/Kconfig b/init/Kconfig
> > index fa79feb8fe57..060a22cddd17 100644
> > --- a/init/Kconfig
> > +++ b/init/Kconfig
> > @@ -777,6 +777,18 @@ config IKHEADERS
> > or similar programs. If you build the headers as a module, a module called
> > kheaders.ko is built which can be loaded on-demand to get access to headers.
> >
> > +config EARLY_PRINTK_TIMES
> > + bool "Show non-zero printk timestamps early in boot"
> > + default y
>
> Considering that this might have a significant impact on monitoring
> mechanisms already in place (that e.g. expect a specific dmesg print to
> have a maximum associated time value), please consider a N default here.
Oops! Sorry, that was supposed to be 'default n'. You're right. I know I had
this as default N, and I think I switched it temporarily for testing, and forgot
to switch it back (and never caught it the numerous times I reviewed the
patch before sending it out again, ugh). Thanks for catching this.
If people like this, and we don't see any problems with tooling or virtualization, I
could see it switching to default Y in the future. But for now this should definitely
be 'default n'.
>
> > + depends on PRINTK
> > + depends on ARM64 || X86_64
> > + help
> > + Use a cycle-counter to provide printk timestamps during
> > + early boot. This allows seeing timestamps for printks that
> > + would otherwise show as 0. Note that this will shift the
> > + printk timestamps to be relative to machine power on, instead
> > + of relative to the start of kernel timekeeping.
> > +
>
> To be precise, the timestamps will be relative to processor power on;
> the machine might have some other processors that run before the Linux
> one (this is the case for example of i.MX9 or AM62 SoCs) and will be
> unaccounted for even by this mechanism.
Good point. Even more precisely, it will be relative to
cycle-counter value initialization or reset, which often (but not always)
corresponds to processor power on.
I'll adjust the wording.
I'm still a bit unsure what happens in the virtualization case. qemu seems to initialize
the TSC at qemu start, but I'm not sure what happens for e.g. client VMs on cloud servers.
>
> > config LOG_BUF_SHIFT
> > int "Kernel log buffer size (16 => 64KB, 17 => 128KB)"
> > range 12 25
> > diff --git a/init/main.c b/init/main.c
> > index b84818ad9685..cc1af26933f7 100644
> > --- a/init/main.c
> > +++ b/init/main.c
> > @@ -104,6 +104,9 @@
> > #include <linux/pidfs.h>
> > #include <linux/ptdump.h>
> > #include <linux/time_namespace.h>
> > +#include <linux/timex.h>
> > +#include <linux/sched/clock.h>
> > +#include <linux/early_times.h>
> > #include <net/net_namespace.h>
> >
> > #include <asm/io.h>
> > @@ -160,6 +163,10 @@ static size_t initargs_offs;
> > # define initargs_offs 0
> > #endif
> >
> > +#ifdef CONFIG_EARLY_PRINTK_TIMES
> > +static u64 start_cycles, start_ns;
> > +#endif
> > +
> > static char *execute_command;
> > static char *ramdisk_execute_command = "/init";
> >
> > @@ -1118,6 +1125,11 @@ void start_kernel(void)
> > timekeeping_init();
> > time_init();
> >
> > +#ifdef CONFIG_EARLY_PRINTK_TIMES
> > + start_cycles = get_cycles();
> > + start_ns = local_clock();
> > +#endif
> > +
>
> I was wondering it it wouldn't make more sense to move this logic to its
> own file, and have a plain call e.g. to early_times_init() here
> (continue...)
>
> > /* This must be after timekeeping is initialized */
> > random_init();
> >
> > @@ -1600,6 +1612,20 @@ static int __ref kernel_init(void *unused)
> >
> > do_sysctl_args();
> >
> > +#ifdef CONFIG_EARLY_PRINTK_TIMES
> > + u64 end_cycles, end_ns;
> > +
> > + /* set calibration data for early_printk_times */
> > + end_cycles = get_cycles();
> > + end_ns = local_clock();
> > + clocks_calc_mult_shift(&early_mult, &early_shift,
> > + ((end_cycles - start_cycles) * NSEC_PER_SEC)/(end_ns - start_ns),
> > + NSEC_PER_SEC, 50);
> > + early_ts_offset = ((start_cycles * early_mult) >> early_shift) - start_ns;
> > + pr_debug("Early printk times: mult=%u, shift=%u, offset=%llu ns\n",
> > + early_mult, early_shift, early_ts_offset);
> > +#endif
> > +
>
> (...continue) and to early_times_calc() or something like that here.
>
> In this way, all related variables (i.e.: start_cycles, start_ns from
> this file, but also early_mult, early_shift, and early_ts_offset from
> kernel/printk/printk.c) can be confined to their own file and not add
> noise here.
I thought a lot about this, and wasn't sure whether to use that approach or
not. I would need two routines: early_times_start_calibration, and
early_times_finish_calibration(). I agree with the idea of keeping
all variables in the printk module, so this is more self-contained. It also
reduces the number of visible #ifdefs.
I think I'll go ahead and do this, and see what the code looks like. I suspect
that what you recommend is the better way to go.
>
> > if (ramdisk_execute_command) {
> > ret = run_init_process(ramdisk_execute_command);
> > if (!ret)
> > diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> > index 1d765ad242b8..f17877337735 100644
> > --- a/kernel/printk/printk.c
> > +++ b/kernel/printk/printk.c
> > @@ -46,6 +46,7 @@
> > #include <linux/ctype.h>
> > #include <linux/uio.h>
> > #include <linux/sched/clock.h>
> > +#include <linux/early_times.h>
> > #include <linux/sched/debug.h>
> > #include <linux/sched/task_stack.h>
> > #include <linux/panic.h>
> > @@ -75,6 +76,11 @@ EXPORT_SYMBOL(ignore_console_lock_warning);
> >
> > EXPORT_TRACEPOINT_SYMBOL_GPL(console);
> >
> > +#ifdef CONFIG_EARLY_PRINTK_TIMES
> > +u32 early_mult, early_shift;
> > +u64 early_ts_offset;
> > +#endif
> > +
> > /*
> > * Low level drivers may need that to know if they can schedule in
> > * their unblank() callback or not. So let's export it.
> > @@ -639,7 +645,7 @@ static void append_char(char **pp, char *e, char c)
> > static ssize_t info_print_ext_header(char *buf, size_t size,
> > struct printk_info *info)
> > {
> > - u64 ts_usec = info->ts_nsec;
> > + u64 ts_usec = adjust_early_ts(info->ts_nsec);
> > char caller[20];
> > #ifdef CONFIG_PRINTK_CALLER
> > u32 id = info->caller_id;
> > @@ -1352,7 +1358,11 @@ static size_t print_syslog(unsigned int level, char *buf)
> >
> > static size_t print_time(u64 ts, char *buf)
> > {
> > - unsigned long rem_nsec = do_div(ts, 1000000000);
> > + unsigned long rem_nsec;
> > +
> > + ts = adjust_early_ts(ts);
> > +
> > + rem_nsec = do_div(ts, 1000000000);
> >
> > return sprintf(buf, "[%5lu.%06lu]",
> > (unsigned long)ts, rem_nsec / 1000);
> > @@ -2242,6 +2252,8 @@ int vprintk_store(int facility, int level,
> > * timestamp with respect to the caller.
> > */
> > ts_nsec = local_clock();
> > + if (!ts_nsec)
> > + ts_nsec = early_cycles();
> >
> > caller_id = printk_caller_id();
> >
> > --
> > 2.43.0
> >
>
> Regards,
> Francesco
Thanks very much for the review and test!!
-- Tim
^ permalink raw reply
* Re: [PATCH v2] printk: fix zero-valued printk timestamps in early boot
From: Geert Uytterhoeven @ 2026-01-26 10:12 UTC (permalink / raw)
To: Tim Bird
Cc: pmladek, rostedt, john.ogness, senozhatsky, francesco,
linux-embedded, linux-kernel
In-Reply-To: <20260124194027.713991-1-tim.bird@sony.com>
Hi Tim,
On Sat, 24 Jan 2026 at 20:41, Tim Bird <tim.bird@sony.com> wrote:
> During early boot, printk timestamps are reported as zero before
> kernel timekeeping starts (e.g. before time_init()). This
> hinders boot-time optimization efforts. This period is about 400
> milliseconds for many current desktop and embedded machines
> running Linux.
>
> Add support to save cycles during early boot, and output correct
> timestamp values after timekeeping is initialized. get_cycles()
> is operational on arm64 and x86_64 from kernel start. Add code
> and variables to save calibration values used to later convert
> cycle counts to time values in the early printks. Add a config
> to control the feature.
>
> This yields non-zero timestamps for printks from the very start
> of kernel execution. The timestamps are relative to the start of
> the architecture-specified counter used in get_cycles
> (e.g. the TSC on x86_64 and cntvct_el0 on arm64).
>
> All timestamps reflect time from power-on instead of time from
> the kernel's timekeeping initialization.
>
> Signed-off-by: Tim Bird <tim.bird@sony.com>
> ---
> V1 -> V2
> Remove calibration CONFIG vars
> Add 'depends on' to restrict arches (to handle ppc bug)
> Add early_ts_offset to avoid discontinuity
> Save cycles in ts_nsec, and convert on output
> Move conditional code to include file (early_times.h)
Thanks for the update!
> --- /dev/null
> +++ b/include/linux/early_times.h
> @@ -0,0 +1,48 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +
> +#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
> +#define _KERNEL_PRINTK_EARLY_TIMES_H
> +
> +#include <linux/timex.h>
> +
> +#if defined(CONFIG_EARLY_PRINTK_TIMES)
> +extern u32 early_mult, early_shift;
> +extern u64 early_ts_offset;
> +
> +static inline u64 early_cycles(void)
> +{
> + return ((u64)get_cycles() | (1ULL << 63));
No need to cast to u64, as the second operand of the OR is u64 anyway.
BIT_ULL(63)
I think it would be good to have a #define for this at the top.
> +}
> +
> +static inline u64 adjust_early_ts(u64 ts)
> +{
> + /* High bit means ts is a cycle count */
> + if (unlikely(ts & (1ULL << 63)))
> + /*
> + * mask high bit and convert to ns
> + * Note that early_mult may be 0, but that's OK because
> + * we'll just multiply by 0 and return 0. This will
> + * only occur if we're outputting a printk message
> + * before the calibration of the early timestamp.
> + * Any output after user space start (eg. from dmesg or
> + * journalctl) will show correct values.
> + */
> + return (((ts & ~(1ULL << 63)) * early_mult) >> early_shift);
Please use the mul_u64_u32_shr() helper.
Please wrap this block in curly braces.
Alternatively, you can invert the logic:
if (likely(!(ts & DEFINITION_FOR_1ULL_LSH63)))
return ts + early_ts_offset;
> +
> + /* If timestamp is already in ns, just add offset */
> + return ts + early_ts_offset;
> +}
> +#else
> +static inline u64 early_cycles(void)
> +{
> + return 0;
> +}
> +
> +static inline u64 adjust_early_ts(u64 ts)
> +{
> + return ts;
> +}
> +#endif /* CONFIG_EARLY_PRINTK_TIMES */
> +
> +#endif /* _KERNEL_PRINTK_EARLY_TIMES_H */
> +
> diff --git a/init/Kconfig b/init/Kconfig
> index fa79feb8fe57..060a22cddd17 100644
> --- a/init/Kconfig
> +++ b/init/Kconfig
> @@ -777,6 +777,18 @@ config IKHEADERS
> or similar programs. If you build the headers as a module, a module called
> kheaders.ko is built which can be loaded on-demand to get access to headers.
>
> +config EARLY_PRINTK_TIMES
> + bool "Show non-zero printk timestamps early in boot"
> + default y
> + depends on PRINTK
> + depends on ARM64 || X86_64
So for now this is limited to (a few) 64-bit platforms...
> + help
> + Use a cycle-counter to provide printk timestamps during
> + early boot. This allows seeing timestamps for printks that
> + would otherwise show as 0. Note that this will shift the
> + printk timestamps to be relative to machine power on, instead
> + of relative to the start of kernel timekeeping.
> +
> config LOG_BUF_SHIFT
> int "Kernel log buffer size (16 => 64KB, 17 => 128KB)"
> range 12 25
> diff --git a/init/main.c b/init/main.c
> index b84818ad9685..cc1af26933f7 100644
> --- a/init/main.c
> +++ b/init/main.c
> @@ -160,6 +163,10 @@ static size_t initargs_offs;
> # define initargs_offs 0
> #endif
>
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> +static u64 start_cycles, start_ns;
cycles_t start_cycles;
(cycles_t is unsigned long, i.e. either 32- or 64-bit).
> +#endif
> +
> static char *execute_command;
> static char *ramdisk_execute_command = "/init";
>
> @@ -1118,6 +1125,11 @@ void start_kernel(void)
> timekeeping_init();
> time_init();
>
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> + start_cycles = get_cycles();
> + start_ns = local_clock();
> +#endif
> +
> /* This must be after timekeeping is initialized */
> random_init();
>
> @@ -1600,6 +1612,20 @@ static int __ref kernel_init(void *unused)
>
> do_sysctl_args();
>
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> + u64 end_cycles, end_ns;
cycles_t end_cycles;
> +
> + /* set calibration data for early_printk_times */
> + end_cycles = get_cycles();
> + end_ns = local_clock();
> + clocks_calc_mult_shift(&early_mult, &early_shift,
> + ((end_cycles - start_cycles) * NSEC_PER_SEC)/(end_ns - start_ns),
> + NSEC_PER_SEC, 50);
mul_u64_u64_div_u64() is probably the best helper that is available
(there is no mul_ulong_u32_div_u64()).
> + early_ts_offset = ((start_cycles * early_mult) >> early_shift) - start_ns;
> + pr_debug("Early printk times: mult=%u, shift=%u, offset=%llu ns\n",
> + early_mult, early_shift, early_ts_offset);
> +#endif
> +
> if (ramdisk_execute_command) {
> ret = run_init_process(ramdisk_execute_command);
> if (!ret)
Gr{oetje,eeting}s,
Geert
--
Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-m68k.org
In personal conversations with technical people, I call myself a hacker. But
when I'm talking to journalists I just say "programmer" or something like that.
-- Linus Torvalds
^ permalink raw reply
* Re: [PATCH v2] printk: fix zero-valued printk timestamps in early boot
From: Francesco Valla @ 2026-01-25 14:41 UTC (permalink / raw)
To: Tim Bird
Cc: pmladek, rostedt, john.ogness, senozhatsky, linux-embedded,
linux-kernel
In-Reply-To: <20260124194027.713991-1-tim.bird@sony.com>
Hi Tim,
I tested this both on X86_64 QEMU and on a i.MX93 (ARM64) and can
confirm it is working as expected. Auto-calc of calibration data is far
better than the configuration parameters in v1.
It is slightly confusing to see a time value printed to serial output
and another one inside kmsg, but that's a human thing and should not
confuse any tool.
Some notes follow.
On Sat, Jan 24, 2026 at 12:40:27PM -0700, Tim Bird wrote:
> During early boot, printk timestamps are reported as zero before
> kernel timekeeping starts (e.g. before time_init()). This
> hinders boot-time optimization efforts. This period is about 400
> milliseconds for many current desktop and embedded machines
> running Linux.
>
> Add support to save cycles during early boot, and output correct
> timestamp values after timekeeping is initialized. get_cycles()
> is operational on arm64 and x86_64 from kernel start. Add code
> and variables to save calibration values used to later convert
> cycle counts to time values in the early printks. Add a config
> to control the feature.
>
> This yields non-zero timestamps for printks from the very start
> of kernel execution. The timestamps are relative to the start of
> the architecture-specified counter used in get_cycles
> (e.g. the TSC on x86_64 and cntvct_el0 on arm64).
>
> All timestamps reflect time from power-on instead of time from
> the kernel's timekeeping initialization.
>
> Signed-off-by: Tim Bird <tim.bird@sony.com>
> ---
> V1 -> V2
> Remove calibration CONFIG vars
> Add 'depends on' to restrict arches (to handle ppc bug)
> Add early_ts_offset to avoid discontinuity
> Save cycles in ts_nsec, and convert on output
> Move conditional code to include file (early_times.h)
> ---
> include/linux/early_times.h | 48 +++++++++++++++++++++++++++++++++++++
> init/Kconfig | 12 ++++++++++
> init/main.c | 26 ++++++++++++++++++++
> kernel/printk/printk.c | 16 +++++++++++--
> 4 files changed, 100 insertions(+), 2 deletions(-)
> create mode 100644 include/linux/early_times.h
>
> diff --git a/include/linux/early_times.h b/include/linux/early_times.h
> new file mode 100644
> index 000000000000..9dc31eb442c2
> --- /dev/null
> +++ b/include/linux/early_times.h
> @@ -0,0 +1,48 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +
> +#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
> +#define _KERNEL_PRINTK_EARLY_TIMES_H
> +
> +#include <linux/timex.h>
> +
> +#if defined(CONFIG_EARLY_PRINTK_TIMES)
> +extern u32 early_mult, early_shift;
> +extern u64 early_ts_offset;
> +
> +static inline u64 early_cycles(void)
> +{
> + return ((u64)get_cycles() | (1ULL << 63));
> +}
> +
> +static inline u64 adjust_early_ts(u64 ts)
> +{
> + /* High bit means ts is a cycle count */
> + if (unlikely(ts & (1ULL << 63)))
> + /*
> + * mask high bit and convert to ns
> + * Note that early_mult may be 0, but that's OK because
> + * we'll just multiply by 0 and return 0. This will
> + * only occur if we're outputting a printk message
> + * before the calibration of the early timestamp.
> + * Any output after user space start (eg. from dmesg or
> + * journalctl) will show correct values.
> + */
> + return (((ts & ~(1ULL << 63)) * early_mult) >> early_shift);
> +
> + /* If timestamp is already in ns, just add offset */
> + return ts + early_ts_offset;
> +}
> +#else
> +static inline u64 early_cycles(void)
> +{
> + return 0;
> +}
> +
> +static inline u64 adjust_early_ts(u64 ts)
> +{
> + return ts;
> +}
> +#endif /* CONFIG_EARLY_PRINTK_TIMES */
> +
> +#endif /* _KERNEL_PRINTK_EARLY_TIMES_H */
> +
> diff --git a/init/Kconfig b/init/Kconfig
> index fa79feb8fe57..060a22cddd17 100644
> --- a/init/Kconfig
> +++ b/init/Kconfig
> @@ -777,6 +777,18 @@ config IKHEADERS
> or similar programs. If you build the headers as a module, a module called
> kheaders.ko is built which can be loaded on-demand to get access to headers.
>
> +config EARLY_PRINTK_TIMES
> + bool "Show non-zero printk timestamps early in boot"
> + default y
Considering that this might have a significant impact on monitoring
mechanisms already in place (that e.g. expect a specific dmesg print to
have a maximum associated time value), please consider a N default here.
> + depends on PRINTK
> + depends on ARM64 || X86_64
> + help
> + Use a cycle-counter to provide printk timestamps during
> + early boot. This allows seeing timestamps for printks that
> + would otherwise show as 0. Note that this will shift the
> + printk timestamps to be relative to machine power on, instead
> + of relative to the start of kernel timekeeping.
> +
To be precise, the timestamps will be relative to processor power on;
the machine might have some other processors that run before the Linux
one (this is the case for example of i.MX9 or AM62 SoCs) and will be
unaccounted for even by this mechanism.
> config LOG_BUF_SHIFT
> int "Kernel log buffer size (16 => 64KB, 17 => 128KB)"
> range 12 25
> diff --git a/init/main.c b/init/main.c
> index b84818ad9685..cc1af26933f7 100644
> --- a/init/main.c
> +++ b/init/main.c
> @@ -104,6 +104,9 @@
> #include <linux/pidfs.h>
> #include <linux/ptdump.h>
> #include <linux/time_namespace.h>
> +#include <linux/timex.h>
> +#include <linux/sched/clock.h>
> +#include <linux/early_times.h>
> #include <net/net_namespace.h>
>
> #include <asm/io.h>
> @@ -160,6 +163,10 @@ static size_t initargs_offs;
> # define initargs_offs 0
> #endif
>
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> +static u64 start_cycles, start_ns;
> +#endif
> +
> static char *execute_command;
> static char *ramdisk_execute_command = "/init";
>
> @@ -1118,6 +1125,11 @@ void start_kernel(void)
> timekeeping_init();
> time_init();
>
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> + start_cycles = get_cycles();
> + start_ns = local_clock();
> +#endif
> +
I was wondering it it wouldn't make more sense to move this logic to its
own file, and have a plain call e.g. to early_times_init() here
(continue...)
> /* This must be after timekeeping is initialized */
> random_init();
>
> @@ -1600,6 +1612,20 @@ static int __ref kernel_init(void *unused)
>
> do_sysctl_args();
>
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> + u64 end_cycles, end_ns;
> +
> + /* set calibration data for early_printk_times */
> + end_cycles = get_cycles();
> + end_ns = local_clock();
> + clocks_calc_mult_shift(&early_mult, &early_shift,
> + ((end_cycles - start_cycles) * NSEC_PER_SEC)/(end_ns - start_ns),
> + NSEC_PER_SEC, 50);
> + early_ts_offset = ((start_cycles * early_mult) >> early_shift) - start_ns;
> + pr_debug("Early printk times: mult=%u, shift=%u, offset=%llu ns\n",
> + early_mult, early_shift, early_ts_offset);
> +#endif
> +
(...continue) and to early_times_calc() or something like that here.
In this way, all related variables (i.e.: start_cycles, start_ns from
this file, but also early_mult, early_shift, and early_ts_offset from
kernel/printk/printk.c) can be confined to their own file and not add
noise here.
> if (ramdisk_execute_command) {
> ret = run_init_process(ramdisk_execute_command);
> if (!ret)
> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> index 1d765ad242b8..f17877337735 100644
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
> @@ -46,6 +46,7 @@
> #include <linux/ctype.h>
> #include <linux/uio.h>
> #include <linux/sched/clock.h>
> +#include <linux/early_times.h>
> #include <linux/sched/debug.h>
> #include <linux/sched/task_stack.h>
> #include <linux/panic.h>
> @@ -75,6 +76,11 @@ EXPORT_SYMBOL(ignore_console_lock_warning);
>
> EXPORT_TRACEPOINT_SYMBOL_GPL(console);
>
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> +u32 early_mult, early_shift;
> +u64 early_ts_offset;
> +#endif
> +
> /*
> * Low level drivers may need that to know if they can schedule in
> * their unblank() callback or not. So let's export it.
> @@ -639,7 +645,7 @@ static void append_char(char **pp, char *e, char c)
> static ssize_t info_print_ext_header(char *buf, size_t size,
> struct printk_info *info)
> {
> - u64 ts_usec = info->ts_nsec;
> + u64 ts_usec = adjust_early_ts(info->ts_nsec);
> char caller[20];
> #ifdef CONFIG_PRINTK_CALLER
> u32 id = info->caller_id;
> @@ -1352,7 +1358,11 @@ static size_t print_syslog(unsigned int level, char *buf)
>
> static size_t print_time(u64 ts, char *buf)
> {
> - unsigned long rem_nsec = do_div(ts, 1000000000);
> + unsigned long rem_nsec;
> +
> + ts = adjust_early_ts(ts);
> +
> + rem_nsec = do_div(ts, 1000000000);
>
> return sprintf(buf, "[%5lu.%06lu]",
> (unsigned long)ts, rem_nsec / 1000);
> @@ -2242,6 +2252,8 @@ int vprintk_store(int facility, int level,
> * timestamp with respect to the caller.
> */
> ts_nsec = local_clock();
> + if (!ts_nsec)
> + ts_nsec = early_cycles();
>
> caller_id = printk_caller_id();
>
> --
> 2.43.0
>
Regards,
Francesco
^ permalink raw reply
* [PATCH v2] printk: fix zero-valued printk timestamps in early boot
From: Tim Bird @ 2026-01-24 19:40 UTC (permalink / raw)
To: pmladek, rostedt, john.ogness, senozhatsky
Cc: francesco, linux-embedded, linux-kernel, Tim Bird
In-Reply-To: <39b09edb-8998-4ebd-a564-7d594434a981@bird.org>
During early boot, printk timestamps are reported as zero before
kernel timekeeping starts (e.g. before time_init()). This
hinders boot-time optimization efforts. This period is about 400
milliseconds for many current desktop and embedded machines
running Linux.
Add support to save cycles during early boot, and output correct
timestamp values after timekeeping is initialized. get_cycles()
is operational on arm64 and x86_64 from kernel start. Add code
and variables to save calibration values used to later convert
cycle counts to time values in the early printks. Add a config
to control the feature.
This yields non-zero timestamps for printks from the very start
of kernel execution. The timestamps are relative to the start of
the architecture-specified counter used in get_cycles
(e.g. the TSC on x86_64 and cntvct_el0 on arm64).
All timestamps reflect time from power-on instead of time from
the kernel's timekeeping initialization.
Signed-off-by: Tim Bird <tim.bird@sony.com>
---
V1 -> V2
Remove calibration CONFIG vars
Add 'depends on' to restrict arches (to handle ppc bug)
Add early_ts_offset to avoid discontinuity
Save cycles in ts_nsec, and convert on output
Move conditional code to include file (early_times.h)
---
include/linux/early_times.h | 48 +++++++++++++++++++++++++++++++++++++
init/Kconfig | 12 ++++++++++
init/main.c | 26 ++++++++++++++++++++
kernel/printk/printk.c | 16 +++++++++++--
4 files changed, 100 insertions(+), 2 deletions(-)
create mode 100644 include/linux/early_times.h
diff --git a/include/linux/early_times.h b/include/linux/early_times.h
new file mode 100644
index 000000000000..9dc31eb442c2
--- /dev/null
+++ b/include/linux/early_times.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+
+#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
+#define _KERNEL_PRINTK_EARLY_TIMES_H
+
+#include <linux/timex.h>
+
+#if defined(CONFIG_EARLY_PRINTK_TIMES)
+extern u32 early_mult, early_shift;
+extern u64 early_ts_offset;
+
+static inline u64 early_cycles(void)
+{
+ return ((u64)get_cycles() | (1ULL << 63));
+}
+
+static inline u64 adjust_early_ts(u64 ts)
+{
+ /* High bit means ts is a cycle count */
+ if (unlikely(ts & (1ULL << 63)))
+ /*
+ * mask high bit and convert to ns
+ * Note that early_mult may be 0, but that's OK because
+ * we'll just multiply by 0 and return 0. This will
+ * only occur if we're outputting a printk message
+ * before the calibration of the early timestamp.
+ * Any output after user space start (eg. from dmesg or
+ * journalctl) will show correct values.
+ */
+ return (((ts & ~(1ULL << 63)) * early_mult) >> early_shift);
+
+ /* If timestamp is already in ns, just add offset */
+ return ts + early_ts_offset;
+}
+#else
+static inline u64 early_cycles(void)
+{
+ return 0;
+}
+
+static inline u64 adjust_early_ts(u64 ts)
+{
+ return ts;
+}
+#endif /* CONFIG_EARLY_PRINTK_TIMES */
+
+#endif /* _KERNEL_PRINTK_EARLY_TIMES_H */
+
diff --git a/init/Kconfig b/init/Kconfig
index fa79feb8fe57..060a22cddd17 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -777,6 +777,18 @@ config IKHEADERS
or similar programs. If you build the headers as a module, a module called
kheaders.ko is built which can be loaded on-demand to get access to headers.
+config EARLY_PRINTK_TIMES
+ bool "Show non-zero printk timestamps early in boot"
+ default y
+ depends on PRINTK
+ depends on ARM64 || X86_64
+ help
+ Use a cycle-counter to provide printk timestamps during
+ early boot. This allows seeing timestamps for printks that
+ would otherwise show as 0. Note that this will shift the
+ printk timestamps to be relative to machine power on, instead
+ of relative to the start of kernel timekeeping.
+
config LOG_BUF_SHIFT
int "Kernel log buffer size (16 => 64KB, 17 => 128KB)"
range 12 25
diff --git a/init/main.c b/init/main.c
index b84818ad9685..cc1af26933f7 100644
--- a/init/main.c
+++ b/init/main.c
@@ -104,6 +104,9 @@
#include <linux/pidfs.h>
#include <linux/ptdump.h>
#include <linux/time_namespace.h>
+#include <linux/timex.h>
+#include <linux/sched/clock.h>
+#include <linux/early_times.h>
#include <net/net_namespace.h>
#include <asm/io.h>
@@ -160,6 +163,10 @@ static size_t initargs_offs;
# define initargs_offs 0
#endif
+#ifdef CONFIG_EARLY_PRINTK_TIMES
+static u64 start_cycles, start_ns;
+#endif
+
static char *execute_command;
static char *ramdisk_execute_command = "/init";
@@ -1118,6 +1125,11 @@ void start_kernel(void)
timekeeping_init();
time_init();
+#ifdef CONFIG_EARLY_PRINTK_TIMES
+ start_cycles = get_cycles();
+ start_ns = local_clock();
+#endif
+
/* This must be after timekeeping is initialized */
random_init();
@@ -1600,6 +1612,20 @@ static int __ref kernel_init(void *unused)
do_sysctl_args();
+#ifdef CONFIG_EARLY_PRINTK_TIMES
+ u64 end_cycles, end_ns;
+
+ /* set calibration data for early_printk_times */
+ end_cycles = get_cycles();
+ end_ns = local_clock();
+ clocks_calc_mult_shift(&early_mult, &early_shift,
+ ((end_cycles - start_cycles) * NSEC_PER_SEC)/(end_ns - start_ns),
+ NSEC_PER_SEC, 50);
+ early_ts_offset = ((start_cycles * early_mult) >> early_shift) - start_ns;
+ pr_debug("Early printk times: mult=%u, shift=%u, offset=%llu ns\n",
+ early_mult, early_shift, early_ts_offset);
+#endif
+
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index 1d765ad242b8..f17877337735 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -46,6 +46,7 @@
#include <linux/ctype.h>
#include <linux/uio.h>
#include <linux/sched/clock.h>
+#include <linux/early_times.h>
#include <linux/sched/debug.h>
#include <linux/sched/task_stack.h>
#include <linux/panic.h>
@@ -75,6 +76,11 @@ EXPORT_SYMBOL(ignore_console_lock_warning);
EXPORT_TRACEPOINT_SYMBOL_GPL(console);
+#ifdef CONFIG_EARLY_PRINTK_TIMES
+u32 early_mult, early_shift;
+u64 early_ts_offset;
+#endif
+
/*
* Low level drivers may need that to know if they can schedule in
* their unblank() callback or not. So let's export it.
@@ -639,7 +645,7 @@ static void append_char(char **pp, char *e, char c)
static ssize_t info_print_ext_header(char *buf, size_t size,
struct printk_info *info)
{
- u64 ts_usec = info->ts_nsec;
+ u64 ts_usec = adjust_early_ts(info->ts_nsec);
char caller[20];
#ifdef CONFIG_PRINTK_CALLER
u32 id = info->caller_id;
@@ -1352,7 +1358,11 @@ static size_t print_syslog(unsigned int level, char *buf)
static size_t print_time(u64 ts, char *buf)
{
- unsigned long rem_nsec = do_div(ts, 1000000000);
+ unsigned long rem_nsec;
+
+ ts = adjust_early_ts(ts);
+
+ rem_nsec = do_div(ts, 1000000000);
return sprintf(buf, "[%5lu.%06lu]",
(unsigned long)ts, rem_nsec / 1000);
@@ -2242,6 +2252,8 @@ int vprintk_store(int facility, int level,
* timestamp with respect to the caller.
*/
ts_nsec = local_clock();
+ if (!ts_nsec)
+ ts_nsec = early_cycles();
caller_id = printk_caller_id();
--
2.43.0
^ permalink raw reply related
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Francesco Valla @ 2026-01-23 20:59 UTC (permalink / raw)
To: Maxime Ripard
Cc: Maarten Lankhorst, Thomas Zimmermann, David Airlie, Simona Vetter,
Jonathan Corbet, Jocelyn Falempe, Javier Martinez Canillas,
Sam Ravnborg, Mario Limonciello, linux-kernel, dri-devel,
linux-doc, linux-embedded
In-Reply-To: <20260122-scallop-of-original-domination-3a554a@houat>
Hi Maxime,
On Thu, Jan 22, 2026 at 02:36:56PM +0100, Maxime Ripard wrote:
> Hi,
>
> On Tue, Jan 06, 2026 at 03:25:40PM +0100, Francesco Valla wrote:
> > Add a DRM client that draws a simple splash, with possibility to show:
> >
> > - a colored background;
> > - a static BMP image (loaded as firmware);
> > - the logo provided by EFI BGRT.
> >
> > The client is not meant to replace a full-featured bootsplash, but
> > rather to remove some complexity (and hopefully boot time) on small
> > embedded platforms or on systems with a limited scope (e.g: recovery
> > or manufacturing images).
> >
> > The background color can be set either at build time from a dedicated
> > config option or at runtime through the drm_client_lib.splash_color
> > command line parameter. Any color in RGB888 format can be used.
> >
> > If enabled, the static BMP image is loaded using the kernel firmware
> > infrastructure; a valid BMP image with 24bpp color and no compression
> > is expected. The name of the image can be set through the
> > drm_client_lib.splash_bmp kernel command line parameter, with the
> > default being 'drm_splash.bmp'.
> >
> > Just like the existing DRM clients, the splash can be enabled from the
> > kernel command line using drm_client_lib.active=splash.
> >
> > Signed-off-by: Francesco Valla <francesco@valla.it>
> > ---
> > drivers/gpu/drm/clients/Kconfig | 79 ++-
> > drivers/gpu/drm/clients/Makefile | 1 +
> > drivers/gpu/drm/clients/drm_client_internal.h | 9 +
> > drivers/gpu/drm/clients/drm_client_setup.c | 8 +
> > drivers/gpu/drm/clients/drm_splash.c | 883 ++++++++++++++++++++++++++
> > 5 files changed, 979 insertions(+), 1 deletion(-)
> >
> > diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
> > index 6096c623d9d5b1a3d4a40d986c45aad2f8277767..dd8cd6cacd1166932eb3890dd816b9ae2d26330f 100644
> > --- a/drivers/gpu/drm/clients/Kconfig
> > +++ b/drivers/gpu/drm/clients/Kconfig
> > @@ -12,6 +12,7 @@ config DRM_CLIENT_LIB
> > config DRM_CLIENT_SELECTION
> > tristate
> > depends on DRM
> > + select DRM_CLIENT_LIB if DRM_CLIENT_SPLASH
> > select DRM_CLIENT_LIB if DRM_CLIENT_LOG
> > select DRM_CLIENT_LIB if DRM_FBDEV_EMULATION
> > help
> > @@ -85,10 +86,79 @@ config DRM_CLIENT_LOG
> > If you only need logs, but no terminal, or if you prefer userspace
> > terminal, say "Y".
> >
> > +config DRM_CLIENT_SPLASH
> > + bool "Display graphic splash"
> > + depends on DRM_CLIENT_SELECTION
> > + select DRM_CLIENT
> > + select DRM_CLIENT_SETUP
> > + select DRM_DRAW
> > + help
> > + This enables a splash drm client, able to display either a plain
> > + color or a static image until the userspace is ready to take over.
> > + The splash will be displayed on all screens available at boot, if
> > + any, or on the ones part of the first hotplug event.
> > +
> > +config DRM_CLIENT_SPLASH_BACKGROUND_COLOR
> > + hex "Splash background color"
> > + depends on DRM_CLIENT_SPLASH
> > + default 0x000000
> > + help
> > + The default splash background color, in RGB888 format.
> > +
> > + The color can be overridden through the drm_client_lib.splash_color
> > + kernel command line parameter.
> > +
> > +config DRM_CLIENT_SPLASH_BMP_SUPPORT
> > + bool
> > +
> > +choice
> > + prompt "Splash source"
> > + depends on DRM_CLIENT_SPLASH
> > + default DRM_CLIENT_SPLASH_SRC_COLOR
> > + help
> > + Selects the source for the splash graphic.
> > +
> > +config DRM_CLIENT_SPLASH_SRC_COLOR
> > + bool "Solid color"
> > + help
> > + Use a solid color as splash. The color is selected through the
> > + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option.
> > +
> > + The image will be loaded using the firmware loading facility the
> > + kernel provides.
> > +
> > +config DRM_CLIENT_SPLASH_SRC_BMP
> > + bool "BMP image"
> > + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> > + select FW_LOADER
> > + help
> > + Use a BMP (bitmap) image as splash. If the image is smaller than the
> > + display(s), it will be centered and the color specified through the
> > + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option will be used as
> > + background.
> > +
> > + The image will be loaded using the firmware loading facility the
> > + kernel provides; it shall use 24 bits per pixel and shall not be
> > + compressed. The name of the file can be set through the
> > + drm_client_lib.splash_bmp command line parameter, with the default
> > + being 'drm_splash.bmp'.
> > +
> > +config DRM_CLIENT_SPLASH_SRC_BGRT
> > + bool "EFI BGRT"
> > + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> > + depends on EFI
> > + help
> > + Use the BGRT image provided by the EFI bootloader. If the image is
> > + smaller than the display(s), it will be centered and the color
> > + specified through the DRM_CLIENT_SPLASH_BACKGROUND_COLOR config
> > + option will be used as background.
> > +
> > +endchoice
>
> I'm not sure we should consider it a xor choice. If we do, that means
> that it's effectively unusable by distros, since you don't know ahead of
> time if the platform it's going to boot on will have a BGRT or not.
>
> Trying BGRT, and then falling back to either an image or a solid
> background would be easier to work with.
>
Thanks for the feedback!
Also considering that BGRT requires some logic to work properly, which
is not present in this version (and here I am referring mostly to the
rotation/positioning quirks), I think I'll go down this very route for
the next version and get rid of the function pointer logic.
The idea was to avoid ifdefs _and_ be open for future expansions, but
in the end it would probably look ugly.
> Maxime
Regards,
Francesco
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Maxime Ripard @ 2026-01-22 13:36 UTC (permalink / raw)
To: Francesco Valla
Cc: Maarten Lankhorst, Thomas Zimmermann, David Airlie, Simona Vetter,
Jonathan Corbet, Jocelyn Falempe, Javier Martinez Canillas,
Sam Ravnborg, Mario Limonciello, linux-kernel, dri-devel,
linux-doc, linux-embedded
In-Reply-To: <20260106-drm_client_splash-v2-1-6e86a7434b59@valla.it>
[-- Attachment #1: Type: text/plain, Size: 5075 bytes --]
Hi,
On Tue, Jan 06, 2026 at 03:25:40PM +0100, Francesco Valla wrote:
> Add a DRM client that draws a simple splash, with possibility to show:
>
> - a colored background;
> - a static BMP image (loaded as firmware);
> - the logo provided by EFI BGRT.
>
> The client is not meant to replace a full-featured bootsplash, but
> rather to remove some complexity (and hopefully boot time) on small
> embedded platforms or on systems with a limited scope (e.g: recovery
> or manufacturing images).
>
> The background color can be set either at build time from a dedicated
> config option or at runtime through the drm_client_lib.splash_color
> command line parameter. Any color in RGB888 format can be used.
>
> If enabled, the static BMP image is loaded using the kernel firmware
> infrastructure; a valid BMP image with 24bpp color and no compression
> is expected. The name of the image can be set through the
> drm_client_lib.splash_bmp kernel command line parameter, with the
> default being 'drm_splash.bmp'.
>
> Just like the existing DRM clients, the splash can be enabled from the
> kernel command line using drm_client_lib.active=splash.
>
> Signed-off-by: Francesco Valla <francesco@valla.it>
> ---
> drivers/gpu/drm/clients/Kconfig | 79 ++-
> drivers/gpu/drm/clients/Makefile | 1 +
> drivers/gpu/drm/clients/drm_client_internal.h | 9 +
> drivers/gpu/drm/clients/drm_client_setup.c | 8 +
> drivers/gpu/drm/clients/drm_splash.c | 883 ++++++++++++++++++++++++++
> 5 files changed, 979 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
> index 6096c623d9d5b1a3d4a40d986c45aad2f8277767..dd8cd6cacd1166932eb3890dd816b9ae2d26330f 100644
> --- a/drivers/gpu/drm/clients/Kconfig
> +++ b/drivers/gpu/drm/clients/Kconfig
> @@ -12,6 +12,7 @@ config DRM_CLIENT_LIB
> config DRM_CLIENT_SELECTION
> tristate
> depends on DRM
> + select DRM_CLIENT_LIB if DRM_CLIENT_SPLASH
> select DRM_CLIENT_LIB if DRM_CLIENT_LOG
> select DRM_CLIENT_LIB if DRM_FBDEV_EMULATION
> help
> @@ -85,10 +86,79 @@ config DRM_CLIENT_LOG
> If you only need logs, but no terminal, or if you prefer userspace
> terminal, say "Y".
>
> +config DRM_CLIENT_SPLASH
> + bool "Display graphic splash"
> + depends on DRM_CLIENT_SELECTION
> + select DRM_CLIENT
> + select DRM_CLIENT_SETUP
> + select DRM_DRAW
> + help
> + This enables a splash drm client, able to display either a plain
> + color or a static image until the userspace is ready to take over.
> + The splash will be displayed on all screens available at boot, if
> + any, or on the ones part of the first hotplug event.
> +
> +config DRM_CLIENT_SPLASH_BACKGROUND_COLOR
> + hex "Splash background color"
> + depends on DRM_CLIENT_SPLASH
> + default 0x000000
> + help
> + The default splash background color, in RGB888 format.
> +
> + The color can be overridden through the drm_client_lib.splash_color
> + kernel command line parameter.
> +
> +config DRM_CLIENT_SPLASH_BMP_SUPPORT
> + bool
> +
> +choice
> + prompt "Splash source"
> + depends on DRM_CLIENT_SPLASH
> + default DRM_CLIENT_SPLASH_SRC_COLOR
> + help
> + Selects the source for the splash graphic.
> +
> +config DRM_CLIENT_SPLASH_SRC_COLOR
> + bool "Solid color"
> + help
> + Use a solid color as splash. The color is selected through the
> + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option.
> +
> + The image will be loaded using the firmware loading facility the
> + kernel provides.
> +
> +config DRM_CLIENT_SPLASH_SRC_BMP
> + bool "BMP image"
> + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> + select FW_LOADER
> + help
> + Use a BMP (bitmap) image as splash. If the image is smaller than the
> + display(s), it will be centered and the color specified through the
> + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option will be used as
> + background.
> +
> + The image will be loaded using the firmware loading facility the
> + kernel provides; it shall use 24 bits per pixel and shall not be
> + compressed. The name of the file can be set through the
> + drm_client_lib.splash_bmp command line parameter, with the default
> + being 'drm_splash.bmp'.
> +
> +config DRM_CLIENT_SPLASH_SRC_BGRT
> + bool "EFI BGRT"
> + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> + depends on EFI
> + help
> + Use the BGRT image provided by the EFI bootloader. If the image is
> + smaller than the display(s), it will be centered and the color
> + specified through the DRM_CLIENT_SPLASH_BACKGROUND_COLOR config
> + option will be used as background.
> +
> +endchoice
I'm not sure we should consider it a xor choice. If we do, that means
that it's effectively unusable by distros, since you don't know ahead of
time if the platform it's going to boot on will have a BGRT or not.
Trying BGRT, and then falling back to either an image or a solid
background would be easier to work with.
Maxime
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 273 bytes --]
^ permalink raw reply
* Reminder of Boot-Time SIG meeting (Jan 27)
From: Bird, Tim @ 2026-01-21 23:20 UTC (permalink / raw)
To: Linux Embedded
Hey Linux Boot-Time SIG interested parties (and other interested Linux kernel developers),
Here is the information for the next Linux Boot-Time SIG conference call.
The meeting will be held via the Jitsi online meeting platform.
To Join the meeting via web, click on:
https://meet.jit.si/LinuxBootTimeSIG
----
Our next meeting is Tuesday, January 27, at 9:00 am Mountain Standard Time.
See this link for other time zones:
https://www.timeanddate.com/worldclock/meetingdetails.html?year=2026&month=01&day=27&hour=16&min=0&sec=0&p1=220&p2=137&p3=195&p4=771
(That makes it 8:00 am Pacific, 15:00 UTC, 16:00 CET, and 20:30 IST)
I'm planning on 1 hour for this meeting.
The agenda for the meeting (and where we'll keep the minutes) is here:
https://docs.google.com/document/d/1XAufoTT6VVJOTMzKMoz8SyOss-JA9H4J1_yVXQq5mN0/edit?usp=sharing
The agenda for the January 27 meeting will be available in the above document before the call.
Here are a few highlights:
- review of material presented at Linux Plumbers Conference
- ongoing work in progress (UBL, boot cache, printk 0-timestamps fix, etc.)
- information on the elinux wiki
- upcoming conferences and collaboration opportunities
If you have items you'd like to add to the agenda, please let me know.
(Please e-mail me and/or put them in the document.)
I look forward to talking to you then.
Thanks,
-- Tim
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Francesco Valla @ 2026-01-07 22:28 UTC (permalink / raw)
To: Mario Limonciello (AMD) (kernel.org)
Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas, Sam Ravnborg, linux-kernel, dri-devel,
linux-doc, linux-embedded
In-Reply-To: <12c72476-a4e8-4b00-80ae-b8eed0b6f7a7@kernel.org>
On Tue, Jan 06, 2026 at 08:40:46PM -0600, Mario Limonciello (AMD) (kernel.org) wrote:
> > > Yes, that's the idea. I am still searching a EFI-enabled platform I can
> > > perform some proper tests on, as I'm not really familiar with EFI.
> > > This version was tested with OVMF on QEMU, but without a real userspace.
> >
> > Almost any modern x86 platform except for a chromebook uses EFI.
> >
> > I'll add them to my local dev tree and test too and see if I can get you
> > some feedback if I run into any problems.
> >
> From my side when testing I hit this during kernel build:
>
> ERROR: modpost: "bgrt_tab" [drivers/gpu/drm/clients/drm_client_lib.ko]
> undefined!
> ERROR: modpost: "bgrt_image_size"
> [drivers/gpu/drm/clients/drm_client_lib.ko] undefined!
>
I got the dependency wrong there, mistrusting the efifb driver (but I
should have checked). I made CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT dependent
on CONFIG_EFI, while it should depend on CONFIG_ACPI_BGRT (which in turn
depends on CONFIG_EFI). I'll fix in the next revision (if the overall
work is deemed useful).
Also, please note that the current version is not considering the x/y
offset provided by the BGRT, nor the associated quirks and sanity
checks.
Thank you
Regards,
Francesco
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Mario Limonciello (AMD) (kernel.org) @ 2026-01-07 2:40 UTC (permalink / raw)
To: Francesco Valla
Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas, Sam Ravnborg, linux-kernel, dri-devel,
linux-doc, linux-embedded
In-Reply-To: <dedc1825-26ca-4976-b174-8d7a740e25e3@kernel.org>
>> Yes, that's the idea. I am still searching a EFI-enabled platform I can
>> perform some proper tests on, as I'm not really familiar with EFI.
>> This version was tested with OVMF on QEMU, but without a real userspace.
>
> Almost any modern x86 platform except for a chromebook uses EFI.
>
> I'll add them to my local dev tree and test too and see if I can get you
> some feedback if I run into any problems.
>
From my side when testing I hit this during kernel build:
ERROR: modpost: "bgrt_tab" [drivers/gpu/drm/clients/drm_client_lib.ko]
undefined!
ERROR: modpost: "bgrt_image_size"
[drivers/gpu/drm/clients/drm_client_lib.ko] undefined!
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Mario Limonciello (AMD) (kernel.org) @ 2026-01-06 20:46 UTC (permalink / raw)
To: Francesco Valla
Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas, Sam Ravnborg, linux-kernel, dri-devel,
linux-doc, linux-embedded
In-Reply-To: <aV1xe09kYUwj4ocm@bywater>
On 1/6/2026 2:32 PM, Francesco Valla wrote:
> Hello Mario,
>
> On Tue, Jan 06, 2026 at 12:58:28PM -0600, Mario Limonciello (AMD) (kernel.org) wrote:
>>
>>
>> On 1/6/2026 8:25 AM, Francesco Valla wrote:
>>> Add a DRM client that draws a simple splash, with possibility to show:
>>>
>>> - a colored background;
>>> - a static BMP image (loaded as firmware);
>>> - the logo provided by EFI BGRT.
>>
>> That's cool, thanks for implementing my suggestion!
>>
>>>
>>> The client is not meant to replace a full-featured bootsplash, but
>>> rather to remove some complexity (and hopefully boot time) on small
>>> embedded platforms or on systems with a limited scope (e.g: recovery
>>> or manufacturing images).
>>
>> In theory when using EFI BGRT you could:
>> * have the BIOS show the BGRT
>> * bootloader not show anything
>> * kernel overwrite the framebuffer containing the BIOS BGRT with the exact
>> same image (should be invisible to the user)
>> * regular bootsplash like plymouth overwrite the framebuffer again with the
>> same BGRT but then also add progress bar/distro logo etc.
>>
>
> Yes, that's the idea. I am still searching a EFI-enabled platform I can
> perform some proper tests on, as I'm not really familiar with EFI.
> This version was tested with OVMF on QEMU, but without a real userspace.
Almost any modern x86 platform except for a chromebook uses EFI.
I'll add them to my local dev tree and test too and see if I can get you
some feedback if I run into any problems.
>
>>>
>>> The background color can be set either at build time from a dedicated
>>> config option or at runtime through the drm_client_lib.splash_color
>>> command line parameter. Any color in RGB888 format can be used.
>>>
>>> If enabled, the static BMP image is loaded using the kernel firmware
>>> infrastructure; a valid BMP image with 24bpp color and no compression
>>> is expected. The name of the image can be set through the
>>> drm_client_lib.splash_bmp kernel command line parameter, with the
>>> default being 'drm_splash.bmp'.
>>>
>>> Just like the existing DRM clients, the splash can be enabled from the
>>> kernel command line using drm_client_lib.active=splash.
>>>
>>> Signed-off-by: Francesco Valla <francesco@valla.it>
>>> ---
>>> drivers/gpu/drm/clients/Kconfig | 79 ++-
>>> drivers/gpu/drm/clients/Makefile | 1 +
>>> drivers/gpu/drm/clients/drm_client_internal.h | 9 +
>>> drivers/gpu/drm/clients/drm_client_setup.c | 8 +
>>> drivers/gpu/drm/clients/drm_splash.c | 883 ++++++++++++++++++++++++++
>>> 5 files changed, 979 insertions(+), 1 deletion(-)
>>>
>>> diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
>>> index 6096c623d9d5b1a3d4a40d986c45aad2f8277767..dd8cd6cacd1166932eb3890dd816b9ae2d26330f 100644
>>> --- a/drivers/gpu/drm/clients/Kconfig
>>> +++ b/drivers/gpu/drm/clients/Kconfig
>>> @@ -12,6 +12,7 @@ config DRM_CLIENT_LIB
>>> config DRM_CLIENT_SELECTION
>>> tristate
>>> depends on DRM
>>> + select DRM_CLIENT_LIB if DRM_CLIENT_SPLASH
>>> select DRM_CLIENT_LIB if DRM_CLIENT_LOG
>>> select DRM_CLIENT_LIB if DRM_FBDEV_EMULATION
>>> help
>>> @@ -85,10 +86,79 @@ config DRM_CLIENT_LOG
>>> If you only need logs, but no terminal, or if you prefer userspace
>>> terminal, say "Y".
>>> +config DRM_CLIENT_SPLASH
>>> + bool "Display graphic splash"
>>> + depends on DRM_CLIENT_SELECTION
>>> + select DRM_CLIENT
>>> + select DRM_CLIENT_SETUP
>>> + select DRM_DRAW
>>> + help
>>> + This enables a splash drm client, able to display either a plain
>>> + color or a static image until the userspace is ready to take over.
>>> + The splash will be displayed on all screens available at boot, if
>>> + any, or on the ones part of the first hotplug event.
>>> +
>>> +config DRM_CLIENT_SPLASH_BACKGROUND_COLOR
>>> + hex "Splash background color"
>>> + depends on DRM_CLIENT_SPLASH
>>> + default 0x000000
>>> + help
>>> + The default splash background color, in RGB888 format.
>>> +
>>> + The color can be overridden through the drm_client_lib.splash_color
>>> + kernel command line parameter.
>>> +
>>> +config DRM_CLIENT_SPLASH_BMP_SUPPORT
>>> + bool
>>> +
>>> +choice
>>> + prompt "Splash source"
>>> + depends on DRM_CLIENT_SPLASH
>>> + default DRM_CLIENT_SPLASH_SRC_COLOR
>>> + help
>>> + Selects the source for the splash graphic.
>>> +
>>> +config DRM_CLIENT_SPLASH_SRC_COLOR
>>> + bool "Solid color"
>>> + help
>>> + Use a solid color as splash. The color is selected through the
>>> + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option.
>>> +
>>> + The image will be loaded using the firmware loading facility the
>>> + kernel provides.
>>> +
>>> +config DRM_CLIENT_SPLASH_SRC_BMP
>>> + bool "BMP image"
>>> + select DRM_CLIENT_SPLASH_BMP_SUPPORT
>>> + select FW_LOADER
>>> + help
>>> + Use a BMP (bitmap) image as splash. If the image is smaller than the
>>> + display(s), it will be centered and the color specified through the
>>> + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option will be used as
>>> + background.
>>> +
>>> + The image will be loaded using the firmware loading facility the
>>> + kernel provides; it shall use 24 bits per pixel and shall not be
>>> + compressed. The name of the file can be set through the
>>> + drm_client_lib.splash_bmp command line parameter, with the default
>>> + being 'drm_splash.bmp'.
>>> +
>>> +config DRM_CLIENT_SPLASH_SRC_BGRT
>>> + bool "EFI BGRT"
>>> + select DRM_CLIENT_SPLASH_BMP_SUPPORT
>>> + depends on EFI
>>> + help
>>> + Use the BGRT image provided by the EFI bootloader. If the image is
>>> + smaller than the display(s), it will be centered and the color
>>> + specified through the DRM_CLIENT_SPLASH_BACKGROUND_COLOR config
>>> + option will be used as background.
>>> +
>>> +endchoice
>>> +
>>> choice
>>> prompt "Default DRM Client"
>>> depends on DRM_CLIENT_SELECTION
>>> - depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG
>>> + depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG || DRM_CLIENT_SPLASH
>>> default DRM_CLIENT_DEFAULT_FBDEV
>>> help
>>> Selects the default drm client.
>>> @@ -111,6 +181,12 @@ config DRM_CLIENT_DEFAULT_LOG
>>> screen, but doesn't implement a full terminal. For that you will need
>>> a userspace terminal using drm/kms.
>>> +config DRM_CLIENT_DEFAULT_SPLASH
>>> + bool "splash"
>>> + depends on DRM_CLIENT_SPLASH
>>> + help
>>> + Use splash as default drm client.
>>> +
>>> endchoice
>>> config DRM_CLIENT_DEFAULT
>>> @@ -118,6 +194,7 @@ config DRM_CLIENT_DEFAULT
>>> depends on DRM_CLIENT
>>> default "fbdev" if DRM_CLIENT_DEFAULT_FBDEV
>>> default "log" if DRM_CLIENT_DEFAULT_LOG
>>> + default "splash" if DRM_CLIENT_DEFAULT_SPLASH
>>
>> Whitespace off by a character?
>>
>
> No, the entire 'config DRM_CLIENT_DEFAULT' block uses spaces instead of
> tabulations. I might send a dedicated patch to fix just that.
>
>>> default ""
>>> endmenu
>>> diff --git a/drivers/gpu/drm/clients/Makefile b/drivers/gpu/drm/clients/Makefile
>>> index c16addbc327f09572aa3142cbf0d1d13f172a9e9..3df02d10cd18a47d7e8d7cee70163b0ef0129b51 100644
>>> --- a/drivers/gpu/drm/clients/Makefile
>>> +++ b/drivers/gpu/drm/clients/Makefile
>>> @@ -5,4 +5,5 @@ subdir-ccflags-y += -I$(src)/..
>>> drm_client_lib-y := drm_client_setup.o
>>> drm_client_lib-$(CONFIG_DRM_CLIENT_LOG) += drm_log.o
>>> drm_client_lib-$(CONFIG_DRM_FBDEV_EMULATION) += drm_fbdev_client.o
>>> +drm_client_lib-$(CONFIG_DRM_CLIENT_SPLASH) += drm_splash.o
>>> obj-$(CONFIG_DRM_CLIENT_LIB) += drm_client_lib.o
>>> diff --git a/drivers/gpu/drm/clients/drm_client_internal.h b/drivers/gpu/drm/clients/drm_client_internal.h
>>> index 6dc078bf6503b902cbb3267b64ea42d9f1c23375..48ee0c1c2529882b2bf5dc786788390823e25cd6 100644
>>> --- a/drivers/gpu/drm/clients/drm_client_internal.h
>>> +++ b/drivers/gpu/drm/clients/drm_client_internal.h
>>> @@ -22,4 +22,13 @@ void drm_log_register(struct drm_device *dev);
>>> static inline void drm_log_register(struct drm_device *dev) {}
>>> #endif
>>> +#ifdef CONFIG_DRM_CLIENT_SPLASH
>>> +void drm_splash_register(struct drm_device *dev,
>>> + const struct drm_format_info *format);
>>> +#else
>>> +static inline void drm_splash_register(struct drm_device *dev,
>>> + const struct drm_format_info *format)
>>> +{}
>>> +#endif
>>> +
>>> #endif
>>> diff --git a/drivers/gpu/drm/clients/drm_client_setup.c b/drivers/gpu/drm/clients/drm_client_setup.c
>>> index 515aceac22b18161a14bd60be4acedc1ddd05bc5..c19498938ee3ba442f7502b27a9c33dfff45a203 100644
>>> --- a/drivers/gpu/drm/clients/drm_client_setup.c
>>> +++ b/drivers/gpu/drm/clients/drm_client_setup.c
>>> @@ -56,6 +56,14 @@ void drm_client_setup(struct drm_device *dev, const struct drm_format_info *form
>>> return;
>>> }
>>> #endif
>>> +
>>> +#ifdef CONFIG_DRM_CLIENT_SPLASH
>>> + if (!strcmp(drm_client_default, "splash")) {
>>> + drm_splash_register(dev, format);
>>> + return;
>>> + }
>>> +#endif
>>> +
>>> if (strcmp(drm_client_default, ""))
>>> drm_warn(dev, "Unknown DRM client %s\n", drm_client_default);
>>> }
>>> diff --git a/drivers/gpu/drm/clients/drm_splash.c b/drivers/gpu/drm/clients/drm_splash.c
>>> new file mode 100644
>>> index 0000000000000000000000000000000000000000..204a5256ef03b6edb81e5dc8c49b5929cb51ff92
>>> --- /dev/null
>>> +++ b/drivers/gpu/drm/clients/drm_splash.c
>>> @@ -0,0 +1,883 @@
>>> +// SPDX-License-Identifier: GPL-2.0 or MIT
>>> +/*
>>> + * Copyright (c) 2025-2026 Francesco Valla <francesco@valla.it>
>>> + *
>>> + */
>>> +
>>> +#include <linux/atomic.h>
>>> +#include <linux/device.h>
>>> +#include <linux/efi-bgrt.h>
>>> +#include <linux/firmware.h>
>>> +#include <linux/init.h>
>>> +#include <linux/iosys-map.h>
>>> +#include <linux/kthread.h>
>>> +#include <linux/module.h>
>>> +#include <linux/types.h>
>>> +
>>> +#include <drm/drm_client.h>
>>> +#include <drm/drm_drv.h>
>>> +#include <drm/drm_fourcc.h>
>>> +#include <drm/drm_framebuffer.h>
>>> +#include <drm/drm_plane.h>
>>> +#include <drm/drm_print.h>
>>> +
>>> +#include "drm_client_internal.h"
>>> +#include "drm_draw_internal.h"
>>> +#include "drm_internal.h"
>>> +
>>> +/**
>>> + * DOC: overview
>>> + *
>>> + * This is a simple graphic bootsplash, able to display either a plain color or
>>> + * a static image.
>>> + */
>>> +
>>> +static unsigned int splash_color = CONFIG_DRM_CLIENT_SPLASH_BACKGROUND_COLOR;
>>> +module_param(splash_color, uint, 0400);
>>> +MODULE_PARM_DESC(splash_color, "Splash background color (RGB888)");
>>
>> Does this really need to be a module parameter(s) in addition to the kconfig
>> option(s)? Just wondering what the use case you envisioned is. In my mind I
>> was thinking in an embedded system manufacturing flow maybe you would set it
>> to red in the case of a hardware test failure on the line or something.
>>
>
> My usecase is slightly different: a single BSP (bootloader + kernel +
> rootfs) for multiple devices belonging to the same family, each one with
> its own splash logo. The device detection would be done by the
> bootloader (using an EEPROM parameter) and propagated to the built-in
> splash client through command line options.
>
> Q: Why not only the BMP logo, then?
> A: Because the logo itself is not a square, and each logo flavor has its
> own background color.
>
>>> +
>>> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
>>> +#define DEFAULT_SPLASH_BMP "drm_splash.bmp"
>>> +static char *splash_bmp = DEFAULT_SPLASH_BMP;
>>> +module_param(splash_bmp, charp, 0400);
>>> +MODULE_PARM_DESC(splash_bmp, "Name of splash image (default: \"" DEFAULT_SPLASH_BMP "\")");
>>> +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
>>> +
>>> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
>>> +#define BMP_FILE_MAGIC_ID 0x4d42
>>> +
>>> +/* BMP header structures copied from drivers/video/fbdev/efifb.c */
>>> +struct bmp_file_header {
>>> + u16 id;
>>> + u32 file_size;
>>> + u32 reserved;
>>> + u32 bitmap_offset;
>>> +} __packed;
>>> +
>>> +struct bmp_dib_header {
>>> + u32 dib_header_size;
>>> + s32 width;
>>> + s32 height;
>>> + u16 planes;
>>> + u16 bpp;
>>> + u32 compression;
>>> + u32 bitmap_size;
>>> + u32 horz_resolution;
>>> + u32 vert_resolution;
>>> + u32 colors_used;
>>> + u32 colors_important;
>>> +} __packed;
>>> +#endif // CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT
>>> +
>>> +typedef int (*drm_splash_data_get_func_t)(void *priv, const u8 **data, size_t *size);
>>> +typedef void (*drm_splash_data_release_func_t)(void *priv);
>>> +
>>> +struct drm_splash_scanout {
>>> + int id;
>>> + u32 format;
>>> + unsigned int width;
>>> + unsigned int height;
>>> + struct drm_client_buffer *buffer;
>>> + bool bg_drawn;
>>> +};
>>> +
>>> +struct drm_splash {
>>> + struct drm_client_dev client;
>>> + u32 preferred_format;
>>> + struct device dev;
>>> +
>>> + struct task_struct *thread;
>>> + atomic_t pending;
>>> +
>>> + struct mutex hotplug_lock;
>>> + bool initialized;
>>> +
>>> + u32 n_scanout;
>>> + struct drm_splash_scanout *scanout;
>>> +
>>> + spinlock_t data_lock;
>>> + drm_splash_data_get_func_t data_get;
>>> + drm_splash_data_release_func_t data_release;
>>> + void *data_priv;
>>> +};
>>> +
>>> +static struct drm_splash *client_to_drm_splash(struct drm_client_dev *client)
>>> +{
>>> + return container_of_const(client, struct drm_splash, client);
>>> +}
>>> +
>>> +static void __maybe_unused
>>> +drm_splash_data_source_push(struct drm_splash *splash,
>>> + drm_splash_data_get_func_t get,
>>> + drm_splash_data_release_func_t release,
>>> + void *priv)
>>> +{
>>> + guard(spinlock)(&splash->data_lock);
>>> +
>>> + /* Release previous data */
>>> + if (splash->data_release)
>>> + splash->data_release(splash->data_priv);
>>> +
>>> + splash->data_get = get;
>>> + splash->data_release = release;
>>> + splash->data_priv = priv;
>>> +}
>>> +
>>> +static void drm_splash_data_source_pop(struct drm_splash *splash,
>>> + drm_splash_data_get_func_t *get,
>>> + drm_splash_data_release_func_t *release,
>>> + void **priv)
>>> +{
>>> + guard(spinlock)(&splash->data_lock);
>>> +
>>> + *get = splash->data_get;
>>> + splash->data_get = NULL;
>>> +
>>> + *release = splash->data_release;
>>> + splash->data_release = NULL;
>>> +
>>> + *priv = splash->data_priv;
>>> + splash->data_priv = NULL;
>>> +}
>>> +
>>> +static struct drm_splash_scanout *
>>> +get_scanout_from_tile_group(struct drm_splash *splash, int id)
>>> +{
>>> + int j;
>>> +
>>> + for (j = 0; j < splash->n_scanout; j++)
>>> + if (splash->scanout[j].id == id)
>>> + return &splash->scanout[j];
>>> +
>>> + return NULL;
>>> +}
>>> +
>>> +static u32 drm_splash_find_usable_format(struct drm_plane *plane,
>>> + u32 preferred_format)
>>> +{
>>> + int i;
>>> +
>>> + /* Check if the preferred format can be used */
>>> + for (i = 0; i < plane->format_count; i++)
>>> + if (plane->format_types[i] == preferred_format)
>>> + return preferred_format;
>>> +
>>> + /* Otherwise, find the first format that can be converted from XRGB8888 */
>>> + for (i = 0; i < plane->format_count; i++)
>>> + if (drm_draw_color_from_xrgb8888(0xffffffff, plane->format_types[i]) != 0)
>>> + return plane->format_types[i];
>>> +
>>> + return DRM_FORMAT_INVALID;
>>> +}
>>> +
>>> +static void drm_splash_fill(struct iosys_map *map, unsigned int dst_pitch,
>>> + unsigned int height, unsigned int width,
>>> + u32 px_width, u32 color)
>>> +{
>>> + switch (px_width) {
>>> + case 2:
>>> + drm_draw_fill16(map, dst_pitch, height, width, color);
>>> + break;
>>> + case 3:
>>> + drm_draw_fill24(map, dst_pitch, height, width, color);
>>> + break;
>>> + case 4:
>>> + drm_draw_fill32(map, dst_pitch, height, width, color);
>>> + break;
>>> + default:
>>> + WARN_ONCE(1, "Can't fill with pixel width %d", px_width);
>>> + }
>>> +}
>>> +
>>> +static int drm_splash_fill_solid_color(struct drm_client_buffer *buffer, u32 color)
>>> +{
>>> + struct drm_client_dev *client = buffer->client;
>>> + struct drm_framebuffer *fb = buffer->fb;
>>> + struct drm_rect r = DRM_RECT_INIT(0, 0, fb->width, fb->height);
>>> + u32 px_width = fb->format->cpp[0];
>>> + struct iosys_map map;
>>> + int ret;
>>> +
>>> + ret = drm_client_buffer_vmap_local(buffer, &map);
>>> + if (ret) {
>>> + drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
>>> + return ret;
>>> + }
>>> +
>>> + drm_splash_fill(&map, fb->pitches[0], drm_rect_height(&r),
>>> + drm_rect_width(&r), px_width, color);
>>> +
>>> + drm_client_buffer_vunmap_local(buffer);
>>> +
>>> + return drm_client_buffer_flush(buffer, &r);
>>> +}
>>> +
>>> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
>>> +static void drm_splash_blit_pix16(struct iosys_map *map, unsigned int dpitch,
>>> + unsigned int x_pad, unsigned int y_pad,
>>> + const u8 *sbuf8, unsigned int spitch,
>>> + unsigned int width, unsigned int height,
>>> + bool invert_y, u32 format)
>>> +{
>>> + unsigned int x, y, src_offset, dst_offset;
>>> + u32 scolor, dcolor, wr_off;
>>> +
>>> + for (y = 0; y < height; y++) {
>>> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
>>> + dst_offset = (y_pad + y) * dpitch;
>>> +
>>> + for (x = 0; x < width; x++) {
>>> + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
>>> + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
>>> + wr_off = dst_offset + (x_pad + x) * sizeof(u16);
>>> +
>>> + iosys_map_wr(map, wr_off, u16, dcolor);
>>> + }
>>> + }
>>> +}
>>> +
>>> +static void drm_splash_blit_pix24(struct iosys_map *map, unsigned int dpitch,
>>> + unsigned int x_pad, unsigned int y_pad,
>>> + const u8 *sbuf8, unsigned int spitch,
>>> + unsigned int width, unsigned int height,
>>> + bool invert_y, u32 format)
>>> +{
>>> + unsigned int x, y, src_offset, dst_offset;
>>> + u32 scolor, dcolor, wr_off;
>>> +
>>> + for (y = 0; y < height; y++) {
>>> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
>>> + dst_offset = (y_pad + y) * dpitch;
>>> +
>>> + for (x = 0; x < width; x++) {
>>> + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
>>> + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
>>> + wr_off = dst_offset + (x_pad + x) * 3;
>>> +
>>> + iosys_map_wr(map, wr_off, u8, (dcolor & 0x000000FF) >> 0);
>>> + iosys_map_wr(map, wr_off + 1, u8, (dcolor & 0x0000FF00) >> 8);
>>> + iosys_map_wr(map, wr_off + 2, u8, (dcolor & 0x00FF0000) >> 16);
>>> + }
>>> + }
>>> +}
>>> +
>>> +static void drm_splash_blit_pix32(struct iosys_map *map, unsigned int dpitch,
>>> + unsigned int x_pad, unsigned int y_pad,
>>> + const u8 *sbuf8, unsigned int spitch,
>>> + unsigned int width, unsigned int height,
>>> + bool invert_y, u32 format)
>>> +{
>>> + unsigned int x, y, src_offset, dst_offset;
>>> + u32 scolor, dcolor, wr_off;
>>> +
>>> + for (y = 0; y < height; y++) {
>>> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
>>> + dst_offset = (y_pad + y) * dpitch;
>>> +
>>> + for (x = 0; x < width; x++) {
>>> + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
>>> + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
>>> + wr_off = dst_offset + (x_pad + x) * sizeof(u32);
>>> +
>>> + iosys_map_wr(map, wr_off, u32, dcolor);
>>> + }
>>> + }
>>> +}
>>> +
>>> +static void drm_splash_blit_rgb888(struct iosys_map *map, unsigned int dpitch,
>>> + unsigned int x_pad, unsigned int y_pad,
>>> + const u8 *sbuf8, unsigned int spitch,
>>> + unsigned int width, unsigned int height,
>>> + bool invert_y)
>>> +{
>>> + unsigned int y, src_offset, dst_offset;
>>> +
>>> + for (y = 0; y < height; y++) {
>>> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
>>> + dst_offset = (y_pad + y) * dpitch + x_pad * 3;
>>> +
>>> + iosys_map_memcpy_to(map, dst_offset, &sbuf8[src_offset], width * 3);
>>> + }
>>> +}
>>> +
>>> +static int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
>>> + const u8 *data, size_t data_len)
>>> +
>>> +{
>>> + struct drm_client_buffer *buffer = scanout->buffer;
>>> + struct drm_client_dev *client = buffer->client;
>>> + struct drm_framebuffer *fb = buffer->fb;
>>> + u32 px_width = fb->format->cpp[0];
>>> + const struct bmp_file_header *file_header;
>>> + const struct bmp_dib_header *dib_header;
>>> + u32 bmp_width, bmp_height, bmp_pitch;
>>> + bool bmp_invert_y;
>>> + unsigned int x_pad, y_pad;
>>> + const u8 *image_data;
>>> + struct iosys_map map;
>>> + struct drm_rect r;
>>> + int ret;
>>> +
>>> + if (data_len < (sizeof(*file_header) + sizeof(*dib_header))) {
>>> + drm_err(client->dev, "splash: BMP file too short");
>>> + return -EINVAL;
>>> + }
>>> +
>>> + file_header = (const struct bmp_file_header *)data;
>>> + if (file_header->id != BMP_FILE_MAGIC_ID) {
>>> + drm_err(client->dev, "splash: invalid BMP magic 0x%04X",
>>> + file_header->id);
>>> + return -EINVAL;
>>> + }
>>> +
>>> + dib_header = (const struct bmp_dib_header *)(data + sizeof(*file_header));
>>> +
>>> + /* Restrict supported format to uncompressed, 24bit RGB888 */
>>> + if (dib_header->dib_header_size != 40 || dib_header->width < 0 ||
>>> + dib_header->planes != 1 || dib_header->compression != 0 ||
>>> + dib_header->bpp != 24) {
>>> + drm_err(client->dev, "splash: invalid BMP format");
>>> + return -EINVAL;
>>> + }
>>> +
>>> + bmp_width = dib_header->width;
>>> + bmp_height = abs(dib_header->height);
>>> + bmp_pitch = round_up(3 * bmp_width, 4);
>>> + bmp_invert_y = (dib_header->height > 0);
>>> +
>>> + if ((file_header->bitmap_offset + bmp_pitch * bmp_height) > data_len) {
>>> + drm_err(client->dev, "splash: invalid BMP size");
>>> + return -EINVAL;
>>> + }
>>> +
>>> + if (bmp_width > scanout->width || bmp_height > scanout->height) {
>>> + drm_err(client->dev, "splash: BMP image is too big for the screen");
>>> + return -EINVAL;
>>> + }
>>> +
>>> + image_data = data + file_header->bitmap_offset;
>>> +
>>> + ret = drm_client_buffer_vmap_local(buffer, &map);
>>> + if (ret) {
>>> + drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
>>> + return ret;
>>> + }
>>> +
>>> + /* Center X and Y */
>>> + x_pad = (scanout->width - bmp_width) / 2;
>>> + y_pad = (scanout->height - bmp_height) / 2;
>>> + r = DRM_RECT_INIT(x_pad, y_pad, bmp_width, bmp_height);
>>> +
>>> + /* In case the target format is RGB888, source data can be copied to
>>> + * the video buffer line by line, avoiding some overhead.
>>> + */
>>> + if (scanout->format == DRM_FORMAT_RGB888) {
>>> + drm_splash_blit_rgb888(&map, fb->pitches[0], x_pad, y_pad,
>>> + image_data, bmp_pitch, bmp_width,
>>> + bmp_height, bmp_invert_y);
>>> + } else {
>>> + switch (px_width) {
>>> + case 2:
>>> + drm_splash_blit_pix16(&map, fb->pitches[0], x_pad,
>>> + y_pad, image_data, bmp_pitch,
>>> + bmp_width, bmp_height,
>>> + bmp_invert_y, scanout->format);
>>> + break;
>>> + case 3:
>>> + drm_splash_blit_pix24(&map, fb->pitches[0], x_pad,
>>> + y_pad, image_data, bmp_pitch,
>>> + bmp_width, bmp_height,
>>> + bmp_invert_y, scanout->format);
>>> + break;
>>> + case 4:
>>> + drm_splash_blit_pix32(&map, fb->pitches[0], x_pad,
>>> + y_pad, image_data, bmp_pitch,
>>> + bmp_width, bmp_height,
>>> + bmp_invert_y, scanout->format);
>>> + break;
>>> + default:
>>> + drm_warn_once(client->dev,
>>> + "splash: can't blit with pixel width %d",
>>> + px_width);
>>> + }
>>> + }
>>> +
>>> + drm_client_buffer_vunmap_local(buffer);
>>> +
>>> + return drm_client_buffer_flush(buffer, &r);
>>> +}
>>> +#else
>>> +static inline int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
>>> + const u8 *data, size_t data_len)
>>> +{
>>> + return -EOPNOTSUPP;
>>> +}
>>> +#endif
>>> +
>>> +static int drm_splash_draw_scanout(struct drm_splash *splash,
>>> + struct drm_splash_scanout *scanout,
>>> + const u8 *data, size_t data_len)
>>> +{
>>> + if (!scanout->buffer)
>>> + return -ENODEV;
>>> +
>>> + if (!scanout->bg_drawn) {
>>> + u32 color = drm_draw_color_from_xrgb8888(splash_color,
>>> + scanout->format);
>>> + drm_splash_fill_solid_color(scanout->buffer, color);
>>> + scanout->bg_drawn = true;
>>> + }
>>> +
>>> + if (data != NULL) {
>>> + /* Ignore the return value, since the solid color has already
>>> + * been drawn to screen.
>>> + */
>>> + drm_splash_bmp_to_scanout(scanout, data, data_len);
>>> + }
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +static int drm_splash_render_thread(void *data)
>>> +{
>>> + struct drm_splash *splash = data;
>>> + struct drm_client_dev *client = &splash->client;
>>> +
>>> + while (!kthread_should_stop()) {
>>> + unsigned int draw_count = 0;
>>> + drm_splash_data_get_func_t get_fn = NULL;
>>> + drm_splash_data_release_func_t release_fn = NULL;
>>> + void *priv = NULL;
>>> + const u8 *img_data = NULL;
>>> + size_t img_data_len = 0;
>>> + int i, ret;
>>> +
>>> + drm_splash_data_source_pop(splash, &get_fn, &release_fn, &priv);
>>> +
>>> + if (get_fn) {
>>> + ret = get_fn(priv, &img_data, &img_data_len);
>>> + if (ret) {
>>> + drm_err(client->dev,
>>> + "splash: failed to get image data: %d",
>>> + ret);
>>> + }
>>> + }
>>> +
>>> + for (i = 0; i < splash->n_scanout; i++) {
>>> + ret = drm_splash_draw_scanout(splash,
>>> + &splash->scanout[i],
>>> + img_data, img_data_len);
>>> + if (ret) {
>>> + drm_err(client->dev,
>>> + "splash: failed to fill scanout %d: %d",
>>> + i, ret);
>>> + continue;
>>> + }
>>> +
>>> + draw_count++;
>>> + }
>>> +
>>> + if (release_fn)
>>> + release_fn(priv);
>>> +
>>> + if (draw_count > 0) {
>>> + ret = drm_client_modeset_commit(client);
>>> + /* If commit returns EBUSY, another master showed up.
>>> + * This means that the splash is no more required.
>>> + */
>>> + if (ret == -EBUSY) {
>>> + drm_info(client->dev,
>>> + "splash: not master anymore, exiting");
>>> + break;
>>> + }
>>> + }
>>> +
>>> + /* If no changes arrived in the mean time, wait to be awaken,
>>> + * e.g.by a firmware callback.
>>> + */
>>> + if (atomic_xchg(&splash->pending, 0) == 0)
>>> + set_current_state(TASK_UNINTERRUPTIBLE);
>>> +
>>> + schedule();
>>> + }
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +static inline void drm_splash_wake_render_thread(struct drm_splash *splash)
>>> +{
>>> + atomic_set(&splash->pending, 1);
>>> + wake_up_process(splash->thread);
>>> +}
>>> +
>>> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
>>> +static int drm_splash_fw_get(void *priv, const u8 **data, size_t *size)
>>> +{
>>> + const struct firmware *fw = priv;
>>> +
>>> + if (!fw)
>>> + return -ENODATA;
>>> +
>>> + *data = fw->data;
>>> + *size = fw->size;
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +static void drm_splash_fw_release(void *priv)
>>> +{
>>> + const struct firmware *fw = priv;
>>> +
>>> + if (fw)
>>> + release_firmware(fw);
>>> +}
>>> +
>>> +static void drm_splash_fw_callback(const struct firmware *fw, void *context)
>>> +{
>>> + struct drm_splash *splash = context;
>>> + struct drm_client_dev *client = &splash->client;
>>> +
>>> + if (!fw || !fw->data) {
>>> + drm_err(client->dev, "splash: no firmware");
>>> + return;
>>> + }
>>> +
>>> + drm_splash_data_source_push(splash, drm_splash_fw_get,
>>> + drm_splash_fw_release, (void *)fw);
>>> +
>>> + /* Wake the render thread */
>>> + drm_dbg(client->dev, "splash: firmware loaded, wake up drawing thread");
>>> + drm_splash_wake_render_thread(splash);
>>> +}
>>> +
>>> +static int drm_splash_fw_request_bmp(struct drm_splash *splash)
>>> +{
>>> + struct drm_client_dev *client = &splash->client;
>>> +
>>> + drm_info(client->dev, "splash: request %s as firmware", splash_bmp);
>>> +
>>> + return request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
>>> + splash_bmp, client->dev->dev, GFP_KERNEL,
>>> + splash, drm_splash_fw_callback);
>>> +}
>>> +#else
>>> +static inline int drm_splash_fw_request_bmp(struct drm_splash *splash)
>>> +{
>>> + return -EOPNOTSUPP;
>>> +}
>>> +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
>>> +
>>> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT)
>>> +static int drm_splash_bgrt_get_data(void *priv, const u8 **data, size_t *size)
>>> +{
>>> + void *bgrt_image = priv;
>>> +
>>> + *data = bgrt_image;
>>> + *size = bgrt_image_size;
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +static void drm_splash_bgrt_release(void *priv)
>>> +{
>>> + void *bgrt_image = priv;
>>> +
>>> + if (bgrt_image)
>>> + memunmap(bgrt_image);
>>> +}
>>> +
>>> +static int drm_splash_bgrt_load(struct drm_splash *splash)
>>> +{
>>> + struct drm_client_dev *client = &splash->client;
>>> + void *bgrt_image = NULL;
>>> +
>>> + drm_info(client->dev, "splash: using EFI BGRT");
>>> +
>>> + if (!bgrt_tab.image_address) {
>>> + drm_info(client->dev, "splash: no BGRT found");
>>> + return -ENOENT;
>>> + }
>>> +
>>> + if (bgrt_tab.status & 0x06) {
>>> + drm_info(client->dev, "splash: BGRT rotation bits set, skipping");
>>> + return -EOPNOTSUPP;
>>> + }
>>> +
>>> + drm_dbg(client->dev, "splash: BGRT image is at 0x%016llx, size=%zX",
>>> + bgrt_tab.image_address, bgrt_image_size);
>>> +
>>> + bgrt_image = memremap(bgrt_tab.image_address, bgrt_image_size,
>>> + MEMREMAP_WB);
>>> + if (!bgrt_image) {
>>> + drm_warn(client->dev, "splash: failed to map BGRT image memory");
>>> + return -ENOMEM;
>>> + }
>>> +
>>> + drm_splash_data_source_push(splash, drm_splash_bgrt_get_data,
>>> + drm_splash_bgrt_release, bgrt_image);
>>> +
>>> + drm_splash_wake_render_thread(splash);
>>> +
>>> + return 0;
>>> +}
>>> +#else
>>> +static inline int drm_splash_bgrt_load(struct drm_splash *splash)
>>> +{
>>> + return -EOPNOTSUPP;
>>> +}
>>> +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT
>>> +
>>> +static int drm_splash_init_client(struct drm_splash *splash)
>>> +{
>>> + struct drm_client_dev *client = &splash->client;
>>> + struct drm_mode_set *modeset;
>>> + unsigned int modeset_mask = 0;
>>> + unsigned int fb_count = 0;
>>> + int j;
>>> +
>>> + if (drm_client_modeset_probe(client, 0, 0))
>>> + return -1;
>>> +
>>> + j = 0;
>>> + drm_client_for_each_modeset(modeset, client) {
>>> + struct drm_splash_scanout *tmp;
>>> + struct drm_splash_scanout *scanout;
>>> + u32 format;
>>> + int id = -1;
>>> +
>>> + /* Skip modesets without a mode */
>>> + if (!modeset->mode)
>>> + continue;
>>> +
>>> + if (modeset->connectors[0]->has_tile) {
>>> + struct drm_splash_scanout *tiled;
>>> + int new_id = modeset->connectors[0]->tile_group->id;
>>> +
>>> + /* Tiled modesets contribute to a single framebuffer,
>>> + * check if this tiled group has already been seen.
>>> + */
>>> + tiled = get_scanout_from_tile_group(splash, new_id);
>>> + if (tiled != NULL) {
>>> + if (!modeset->x)
>>> + tiled->width += modeset->mode->vdisplay;
>>> + if (!modeset->y)
>>> + tiled->height += modeset->mode->hdisplay;
>>> + modeset->fb = tiled->buffer->fb;
>>> + continue;
>>> + }
>>> +
>>> + /* New tile group, save its ID for later */
>>> + id = new_id;
>>> + }
>>> +
>>> + format = drm_splash_find_usable_format(modeset->crtc->primary,
>>> + splash->preferred_format);
>>> + if (format == DRM_FORMAT_INVALID) {
>>> + drm_warn(client->dev,
>>> + "splash: can't find a usable format for modeset");
>>> + continue;
>>> + }
>>> +
>>> + tmp = krealloc(splash->scanout,
>>> + (splash->n_scanout + 1) * sizeof(*splash->scanout),
>>> + GFP_KERNEL);
>>> + if (!tmp) {
>>> + drm_warn(client->dev,
>>> + "splash: can't reallocate the scanout array");
>>> + break;
>>> + }
>>> +
>>> + splash->scanout = tmp;
>>> + scanout = &splash->scanout[splash->n_scanout];
>>> + splash->n_scanout++;
>>> +
>>> + memset(scanout, 0, sizeof(*scanout));
>>> + scanout->id = id;
>>> + scanout->format = format;
>>> + scanout->width = modeset->mode->hdisplay;
>>> + scanout->height = modeset->mode->vdisplay;
>>> +
>>> + modeset_mask |= BIT(j);
>>> + j++;
>>> + }
>>> +
>>> + /* Now that all sensible modesets have been collected, allocate buffers */
>>> + j = 0;
>>> + drm_client_for_each_modeset(modeset, client) {
>>> + struct drm_splash_scanout *scanout;
>>> +
>>> + if (!(modeset_mask & BIT(j)))
>>> + continue;
>>> +
>>> + scanout = &splash->scanout[j];
>>> + j++;
>>> +
>>> + scanout->buffer = drm_client_buffer_create_dumb(client,
>>> + scanout->width,
>>> + scanout->height,
>>> + scanout->format);
>>> + if (IS_ERR(scanout->buffer)) {
>>> + drm_warn(client->dev,
>>> + "splash: can't create dumb buffer %d %d %p4cc",
>>> + scanout->width, scanout->height, &scanout->format);
>>> + continue;
>>> + }
>>> +
>>> + drm_info(client->dev, "splash: created dumb buffer %d %d %p4cc",
>>> + scanout->width, scanout->height, &scanout->format);
>>> +
>>> + modeset->fb = scanout->buffer->fb;
>>> + fb_count++;
>>> + }
>>> +
>>> + return (fb_count == 0) ? -ENODEV : 0;
>>> +}
>>> +
>>> +static void drm_splash_free_scanout(struct drm_client_dev *client)
>>> +{
>>> + struct drm_splash *splash = client_to_drm_splash(client);
>>> + int i;
>>> +
>>> + if (splash->n_scanout) {
>>> + for (i = 0; i < splash->n_scanout; i++)
>>> + drm_client_buffer_delete(splash->scanout[i].buffer);
>>> +
>>> + splash->n_scanout = 0;
>>> + kfree(splash->scanout);
>>> + splash->scanout = NULL;
>>> + }
>>> +}
>>> +
>>> +static int drm_splash_client_hotplug(struct drm_client_dev *client)
>>> +{
>>> + struct drm_splash *splash = client_to_drm_splash(client);
>>> + int ret;
>>> +
>>> + guard(mutex)(&splash->hotplug_lock);
>>> +
>>> + /* The modesets that get a splash are defined at first hotplug event */
>>> + if (splash->initialized)
>>> + return 0;
>>> +
>>> + ret = drm_splash_init_client(splash);
>>> + if (ret == -ENODEV) {
>>> + drm_info(client->dev, "splash: no modeset found");
>>> + return 0;
>>> + } else if (ret) {
>>> + drm_err(client->dev,
>>> + "splash: failed to init client: %d", ret);
>>> + return ret;
>>> + }
>>> +
>>> + /* Create the render thread, waken later */
>>> + splash->thread = kthread_create(drm_splash_render_thread,
>>> + splash, "drm_splash_%s",
>>> + client->dev->unique);
>>> + if (IS_ERR(splash->thread)) {
>>> + ret = PTR_ERR(splash->thread);
>>> + drm_err(client->dev, "splash: failed to create render thread: %d", ret);
>>> + drm_splash_free_scanout(client);
>>> + return ret;
>>> + }
>>> +
>>> + if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP))
>>> + ret = drm_splash_fw_request_bmp(splash);
>>> + else if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT))
>>> + ret = drm_splash_bgrt_load(splash);
>>
>> I'm not sure this logic will actually work in this order.
>> CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT selects
>> CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
>>
>> So won't the BGRT path never run?
>>
>
> Actually, CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT selects
> CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT, which builds the bitmap file
> format support shared by both the bmp-as-a-firmware and BGRT "backends".
Ah, thanks for clarifying!
>
>>> + else
>>> + ret = 0;
>>> +
>>> + if (ret) {
>>> + drm_err(client->dev, "splash: failed to kick image load: %d", ret);
>>> + kthread_stop(splash->thread);
>>> + drm_splash_free_scanout(client);
>>> + return ret;
>>> + }
>>> +
>>> + /* Wake the render thread to show initial contents */
>>> + drm_splash_wake_render_thread(splash);
>>> +
>>> + splash->initialized = true;
>>> +
>>> + return 0;
>>> +}
>>> +
>>> +static int drm_splash_client_restore(struct drm_client_dev *client, bool force)
>>> +{
>>> + int ret;
>>> +
>>> + if (force)
>>> + ret = drm_client_modeset_commit_locked(client);
>>> + else
>>> + ret = drm_client_modeset_commit(client);
>>> +
>>> + return ret;
>>> +}
>>> +
>>> +static void drm_splash_client_unregister(struct drm_client_dev *client)
>>> +{
>>> + struct drm_splash *splash = client_to_drm_splash(client);
>>> +
>>> + kthread_stop(splash->thread);
>>> + drm_splash_free_scanout(client);
>>> + drm_client_release(client);
>>> +
>>> + if (splash->data_release)
>>> + splash->data_release(splash->data_priv);
>>> +}
>>> +
>>> +static void drm_splash_client_free(struct drm_client_dev *client)
>>> +{
>>> + struct drm_splash *splash = client_to_drm_splash(client);
>>> + struct drm_device *dev = client->dev;
>>> +
>>> + mutex_destroy(&splash->hotplug_lock);
>>> + kfree(splash);
>>> +
>>> + drm_dbg(dev, "Unregistered with drm splash");
>>> +}
>>> +
>>> +static const struct drm_client_funcs drm_splash_client_funcs = {
>>> + .owner = THIS_MODULE,
>>> + .hotplug = drm_splash_client_hotplug,
>>> + .restore = drm_splash_client_restore,
>>> + .unregister = drm_splash_client_unregister,
>>> + .free = drm_splash_client_free,
>>> +};
>>> +
>>> +/**
>>> + * drm_splash_register() - Register a drm device to drm_splash
>>> + * @dev: the drm device to register.
>>> + * @format: drm device preferred format.
>>> + */
>>> +void drm_splash_register(struct drm_device *dev,
>>> + const struct drm_format_info *format)
>>> +{
>>> + struct drm_splash *splash;
>>> +
>>> + splash = kzalloc(sizeof(*splash), GFP_KERNEL);
>>> + if (!splash)
>>> + goto err_warn;
>>> +
>>> + mutex_init(&splash->hotplug_lock);
>>> + spin_lock_init(&splash->data_lock);
>>> +
>>> + if (format && format->num_planes == 1)
>>> + splash->preferred_format = format->format;
>>> + else
>>> + splash->preferred_format = DRM_FORMAT_RGB888;
>>> +
>>> + if (drm_client_init(dev, &splash->client, "drm_splash",
>>> + &drm_splash_client_funcs))
>>> + goto err_free;
>>> +
>>> + drm_client_register(&splash->client);
>>> + drm_dbg(dev, "Registered with drm splash");
>>> +
>>> + return;
>>> +
>>> +err_free:
>>> + kfree(splash);
>>> +err_warn:
>>> + drm_warn(dev, "Failed to register with drm splash");
>>> +}
>>>
>>
>>
>
> Thank you!
>
> Reagrds,
> Francesco
>
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Francesco Valla @ 2026-01-06 20:32 UTC (permalink / raw)
To: Mario Limonciello (AMD) (kernel.org)
Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas, Sam Ravnborg, linux-kernel, dri-devel,
linux-doc, linux-embedded
In-Reply-To: <894581a9-2a30-428d-ab94-34697147d68e@kernel.org>
Hello Mario,
On Tue, Jan 06, 2026 at 12:58:28PM -0600, Mario Limonciello (AMD) (kernel.org) wrote:
>
>
> On 1/6/2026 8:25 AM, Francesco Valla wrote:
> > Add a DRM client that draws a simple splash, with possibility to show:
> >
> > - a colored background;
> > - a static BMP image (loaded as firmware);
> > - the logo provided by EFI BGRT.
>
> That's cool, thanks for implementing my suggestion!
>
> >
> > The client is not meant to replace a full-featured bootsplash, but
> > rather to remove some complexity (and hopefully boot time) on small
> > embedded platforms or on systems with a limited scope (e.g: recovery
> > or manufacturing images).
>
> In theory when using EFI BGRT you could:
> * have the BIOS show the BGRT
> * bootloader not show anything
> * kernel overwrite the framebuffer containing the BIOS BGRT with the exact
> same image (should be invisible to the user)
> * regular bootsplash like plymouth overwrite the framebuffer again with the
> same BGRT but then also add progress bar/distro logo etc.
>
Yes, that's the idea. I am still searching a EFI-enabled platform I can
perform some proper tests on, as I'm not really familiar with EFI.
This version was tested with OVMF on QEMU, but without a real userspace.
> >
> > The background color can be set either at build time from a dedicated
> > config option or at runtime through the drm_client_lib.splash_color
> > command line parameter. Any color in RGB888 format can be used.
> >
> > If enabled, the static BMP image is loaded using the kernel firmware
> > infrastructure; a valid BMP image with 24bpp color and no compression
> > is expected. The name of the image can be set through the
> > drm_client_lib.splash_bmp kernel command line parameter, with the
> > default being 'drm_splash.bmp'.
> >
> > Just like the existing DRM clients, the splash can be enabled from the
> > kernel command line using drm_client_lib.active=splash.
> >
> > Signed-off-by: Francesco Valla <francesco@valla.it>
> > ---
> > drivers/gpu/drm/clients/Kconfig | 79 ++-
> > drivers/gpu/drm/clients/Makefile | 1 +
> > drivers/gpu/drm/clients/drm_client_internal.h | 9 +
> > drivers/gpu/drm/clients/drm_client_setup.c | 8 +
> > drivers/gpu/drm/clients/drm_splash.c | 883 ++++++++++++++++++++++++++
> > 5 files changed, 979 insertions(+), 1 deletion(-)
> >
> > diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
> > index 6096c623d9d5b1a3d4a40d986c45aad2f8277767..dd8cd6cacd1166932eb3890dd816b9ae2d26330f 100644
> > --- a/drivers/gpu/drm/clients/Kconfig
> > +++ b/drivers/gpu/drm/clients/Kconfig
> > @@ -12,6 +12,7 @@ config DRM_CLIENT_LIB
> > config DRM_CLIENT_SELECTION
> > tristate
> > depends on DRM
> > + select DRM_CLIENT_LIB if DRM_CLIENT_SPLASH
> > select DRM_CLIENT_LIB if DRM_CLIENT_LOG
> > select DRM_CLIENT_LIB if DRM_FBDEV_EMULATION
> > help
> > @@ -85,10 +86,79 @@ config DRM_CLIENT_LOG
> > If you only need logs, but no terminal, or if you prefer userspace
> > terminal, say "Y".
> > +config DRM_CLIENT_SPLASH
> > + bool "Display graphic splash"
> > + depends on DRM_CLIENT_SELECTION
> > + select DRM_CLIENT
> > + select DRM_CLIENT_SETUP
> > + select DRM_DRAW
> > + help
> > + This enables a splash drm client, able to display either a plain
> > + color or a static image until the userspace is ready to take over.
> > + The splash will be displayed on all screens available at boot, if
> > + any, or on the ones part of the first hotplug event.
> > +
> > +config DRM_CLIENT_SPLASH_BACKGROUND_COLOR
> > + hex "Splash background color"
> > + depends on DRM_CLIENT_SPLASH
> > + default 0x000000
> > + help
> > + The default splash background color, in RGB888 format.
> > +
> > + The color can be overridden through the drm_client_lib.splash_color
> > + kernel command line parameter.
> > +
> > +config DRM_CLIENT_SPLASH_BMP_SUPPORT
> > + bool
> > +
> > +choice
> > + prompt "Splash source"
> > + depends on DRM_CLIENT_SPLASH
> > + default DRM_CLIENT_SPLASH_SRC_COLOR
> > + help
> > + Selects the source for the splash graphic.
> > +
> > +config DRM_CLIENT_SPLASH_SRC_COLOR
> > + bool "Solid color"
> > + help
> > + Use a solid color as splash. The color is selected through the
> > + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option.
> > +
> > + The image will be loaded using the firmware loading facility the
> > + kernel provides.
> > +
> > +config DRM_CLIENT_SPLASH_SRC_BMP
> > + bool "BMP image"
> > + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> > + select FW_LOADER
> > + help
> > + Use a BMP (bitmap) image as splash. If the image is smaller than the
> > + display(s), it will be centered and the color specified through the
> > + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option will be used as
> > + background.
> > +
> > + The image will be loaded using the firmware loading facility the
> > + kernel provides; it shall use 24 bits per pixel and shall not be
> > + compressed. The name of the file can be set through the
> > + drm_client_lib.splash_bmp command line parameter, with the default
> > + being 'drm_splash.bmp'.
> > +
> > +config DRM_CLIENT_SPLASH_SRC_BGRT
> > + bool "EFI BGRT"
> > + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> > + depends on EFI
> > + help
> > + Use the BGRT image provided by the EFI bootloader. If the image is
> > + smaller than the display(s), it will be centered and the color
> > + specified through the DRM_CLIENT_SPLASH_BACKGROUND_COLOR config
> > + option will be used as background.
> > +
> > +endchoice
> > +
> > choice
> > prompt "Default DRM Client"
> > depends on DRM_CLIENT_SELECTION
> > - depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG
> > + depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG || DRM_CLIENT_SPLASH
> > default DRM_CLIENT_DEFAULT_FBDEV
> > help
> > Selects the default drm client.
> > @@ -111,6 +181,12 @@ config DRM_CLIENT_DEFAULT_LOG
> > screen, but doesn't implement a full terminal. For that you will need
> > a userspace terminal using drm/kms.
> > +config DRM_CLIENT_DEFAULT_SPLASH
> > + bool "splash"
> > + depends on DRM_CLIENT_SPLASH
> > + help
> > + Use splash as default drm client.
> > +
> > endchoice
> > config DRM_CLIENT_DEFAULT
> > @@ -118,6 +194,7 @@ config DRM_CLIENT_DEFAULT
> > depends on DRM_CLIENT
> > default "fbdev" if DRM_CLIENT_DEFAULT_FBDEV
> > default "log" if DRM_CLIENT_DEFAULT_LOG
> > + default "splash" if DRM_CLIENT_DEFAULT_SPLASH
>
> Whitespace off by a character?
>
No, the entire 'config DRM_CLIENT_DEFAULT' block uses spaces instead of
tabulations. I might send a dedicated patch to fix just that.
> > default ""
> > endmenu
> > diff --git a/drivers/gpu/drm/clients/Makefile b/drivers/gpu/drm/clients/Makefile
> > index c16addbc327f09572aa3142cbf0d1d13f172a9e9..3df02d10cd18a47d7e8d7cee70163b0ef0129b51 100644
> > --- a/drivers/gpu/drm/clients/Makefile
> > +++ b/drivers/gpu/drm/clients/Makefile
> > @@ -5,4 +5,5 @@ subdir-ccflags-y += -I$(src)/..
> > drm_client_lib-y := drm_client_setup.o
> > drm_client_lib-$(CONFIG_DRM_CLIENT_LOG) += drm_log.o
> > drm_client_lib-$(CONFIG_DRM_FBDEV_EMULATION) += drm_fbdev_client.o
> > +drm_client_lib-$(CONFIG_DRM_CLIENT_SPLASH) += drm_splash.o
> > obj-$(CONFIG_DRM_CLIENT_LIB) += drm_client_lib.o
> > diff --git a/drivers/gpu/drm/clients/drm_client_internal.h b/drivers/gpu/drm/clients/drm_client_internal.h
> > index 6dc078bf6503b902cbb3267b64ea42d9f1c23375..48ee0c1c2529882b2bf5dc786788390823e25cd6 100644
> > --- a/drivers/gpu/drm/clients/drm_client_internal.h
> > +++ b/drivers/gpu/drm/clients/drm_client_internal.h
> > @@ -22,4 +22,13 @@ void drm_log_register(struct drm_device *dev);
> > static inline void drm_log_register(struct drm_device *dev) {}
> > #endif
> > +#ifdef CONFIG_DRM_CLIENT_SPLASH
> > +void drm_splash_register(struct drm_device *dev,
> > + const struct drm_format_info *format);
> > +#else
> > +static inline void drm_splash_register(struct drm_device *dev,
> > + const struct drm_format_info *format)
> > +{}
> > +#endif
> > +
> > #endif
> > diff --git a/drivers/gpu/drm/clients/drm_client_setup.c b/drivers/gpu/drm/clients/drm_client_setup.c
> > index 515aceac22b18161a14bd60be4acedc1ddd05bc5..c19498938ee3ba442f7502b27a9c33dfff45a203 100644
> > --- a/drivers/gpu/drm/clients/drm_client_setup.c
> > +++ b/drivers/gpu/drm/clients/drm_client_setup.c
> > @@ -56,6 +56,14 @@ void drm_client_setup(struct drm_device *dev, const struct drm_format_info *form
> > return;
> > }
> > #endif
> > +
> > +#ifdef CONFIG_DRM_CLIENT_SPLASH
> > + if (!strcmp(drm_client_default, "splash")) {
> > + drm_splash_register(dev, format);
> > + return;
> > + }
> > +#endif
> > +
> > if (strcmp(drm_client_default, ""))
> > drm_warn(dev, "Unknown DRM client %s\n", drm_client_default);
> > }
> > diff --git a/drivers/gpu/drm/clients/drm_splash.c b/drivers/gpu/drm/clients/drm_splash.c
> > new file mode 100644
> > index 0000000000000000000000000000000000000000..204a5256ef03b6edb81e5dc8c49b5929cb51ff92
> > --- /dev/null
> > +++ b/drivers/gpu/drm/clients/drm_splash.c
> > @@ -0,0 +1,883 @@
> > +// SPDX-License-Identifier: GPL-2.0 or MIT
> > +/*
> > + * Copyright (c) 2025-2026 Francesco Valla <francesco@valla.it>
> > + *
> > + */
> > +
> > +#include <linux/atomic.h>
> > +#include <linux/device.h>
> > +#include <linux/efi-bgrt.h>
> > +#include <linux/firmware.h>
> > +#include <linux/init.h>
> > +#include <linux/iosys-map.h>
> > +#include <linux/kthread.h>
> > +#include <linux/module.h>
> > +#include <linux/types.h>
> > +
> > +#include <drm/drm_client.h>
> > +#include <drm/drm_drv.h>
> > +#include <drm/drm_fourcc.h>
> > +#include <drm/drm_framebuffer.h>
> > +#include <drm/drm_plane.h>
> > +#include <drm/drm_print.h>
> > +
> > +#include "drm_client_internal.h"
> > +#include "drm_draw_internal.h"
> > +#include "drm_internal.h"
> > +
> > +/**
> > + * DOC: overview
> > + *
> > + * This is a simple graphic bootsplash, able to display either a plain color or
> > + * a static image.
> > + */
> > +
> > +static unsigned int splash_color = CONFIG_DRM_CLIENT_SPLASH_BACKGROUND_COLOR;
> > +module_param(splash_color, uint, 0400);
> > +MODULE_PARM_DESC(splash_color, "Splash background color (RGB888)");
>
> Does this really need to be a module parameter(s) in addition to the kconfig
> option(s)? Just wondering what the use case you envisioned is. In my mind I
> was thinking in an embedded system manufacturing flow maybe you would set it
> to red in the case of a hardware test failure on the line or something.
>
My usecase is slightly different: a single BSP (bootloader + kernel +
rootfs) for multiple devices belonging to the same family, each one with
its own splash logo. The device detection would be done by the
bootloader (using an EEPROM parameter) and propagated to the built-in
splash client through command line options.
Q: Why not only the BMP logo, then?
A: Because the logo itself is not a square, and each logo flavor has its
own background color.
> > +
> > +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
> > +#define DEFAULT_SPLASH_BMP "drm_splash.bmp"
> > +static char *splash_bmp = DEFAULT_SPLASH_BMP;
> > +module_param(splash_bmp, charp, 0400);
> > +MODULE_PARM_DESC(splash_bmp, "Name of splash image (default: \"" DEFAULT_SPLASH_BMP "\")");
> > +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
> > +
> > +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
> > +#define BMP_FILE_MAGIC_ID 0x4d42
> > +
> > +/* BMP header structures copied from drivers/video/fbdev/efifb.c */
> > +struct bmp_file_header {
> > + u16 id;
> > + u32 file_size;
> > + u32 reserved;
> > + u32 bitmap_offset;
> > +} __packed;
> > +
> > +struct bmp_dib_header {
> > + u32 dib_header_size;
> > + s32 width;
> > + s32 height;
> > + u16 planes;
> > + u16 bpp;
> > + u32 compression;
> > + u32 bitmap_size;
> > + u32 horz_resolution;
> > + u32 vert_resolution;
> > + u32 colors_used;
> > + u32 colors_important;
> > +} __packed;
> > +#endif // CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT
> > +
> > +typedef int (*drm_splash_data_get_func_t)(void *priv, const u8 **data, size_t *size);
> > +typedef void (*drm_splash_data_release_func_t)(void *priv);
> > +
> > +struct drm_splash_scanout {
> > + int id;
> > + u32 format;
> > + unsigned int width;
> > + unsigned int height;
> > + struct drm_client_buffer *buffer;
> > + bool bg_drawn;
> > +};
> > +
> > +struct drm_splash {
> > + struct drm_client_dev client;
> > + u32 preferred_format;
> > + struct device dev;
> > +
> > + struct task_struct *thread;
> > + atomic_t pending;
> > +
> > + struct mutex hotplug_lock;
> > + bool initialized;
> > +
> > + u32 n_scanout;
> > + struct drm_splash_scanout *scanout;
> > +
> > + spinlock_t data_lock;
> > + drm_splash_data_get_func_t data_get;
> > + drm_splash_data_release_func_t data_release;
> > + void *data_priv;
> > +};
> > +
> > +static struct drm_splash *client_to_drm_splash(struct drm_client_dev *client)
> > +{
> > + return container_of_const(client, struct drm_splash, client);
> > +}
> > +
> > +static void __maybe_unused
> > +drm_splash_data_source_push(struct drm_splash *splash,
> > + drm_splash_data_get_func_t get,
> > + drm_splash_data_release_func_t release,
> > + void *priv)
> > +{
> > + guard(spinlock)(&splash->data_lock);
> > +
> > + /* Release previous data */
> > + if (splash->data_release)
> > + splash->data_release(splash->data_priv);
> > +
> > + splash->data_get = get;
> > + splash->data_release = release;
> > + splash->data_priv = priv;
> > +}
> > +
> > +static void drm_splash_data_source_pop(struct drm_splash *splash,
> > + drm_splash_data_get_func_t *get,
> > + drm_splash_data_release_func_t *release,
> > + void **priv)
> > +{
> > + guard(spinlock)(&splash->data_lock);
> > +
> > + *get = splash->data_get;
> > + splash->data_get = NULL;
> > +
> > + *release = splash->data_release;
> > + splash->data_release = NULL;
> > +
> > + *priv = splash->data_priv;
> > + splash->data_priv = NULL;
> > +}
> > +
> > +static struct drm_splash_scanout *
> > +get_scanout_from_tile_group(struct drm_splash *splash, int id)
> > +{
> > + int j;
> > +
> > + for (j = 0; j < splash->n_scanout; j++)
> > + if (splash->scanout[j].id == id)
> > + return &splash->scanout[j];
> > +
> > + return NULL;
> > +}
> > +
> > +static u32 drm_splash_find_usable_format(struct drm_plane *plane,
> > + u32 preferred_format)
> > +{
> > + int i;
> > +
> > + /* Check if the preferred format can be used */
> > + for (i = 0; i < plane->format_count; i++)
> > + if (plane->format_types[i] == preferred_format)
> > + return preferred_format;
> > +
> > + /* Otherwise, find the first format that can be converted from XRGB8888 */
> > + for (i = 0; i < plane->format_count; i++)
> > + if (drm_draw_color_from_xrgb8888(0xffffffff, plane->format_types[i]) != 0)
> > + return plane->format_types[i];
> > +
> > + return DRM_FORMAT_INVALID;
> > +}
> > +
> > +static void drm_splash_fill(struct iosys_map *map, unsigned int dst_pitch,
> > + unsigned int height, unsigned int width,
> > + u32 px_width, u32 color)
> > +{
> > + switch (px_width) {
> > + case 2:
> > + drm_draw_fill16(map, dst_pitch, height, width, color);
> > + break;
> > + case 3:
> > + drm_draw_fill24(map, dst_pitch, height, width, color);
> > + break;
> > + case 4:
> > + drm_draw_fill32(map, dst_pitch, height, width, color);
> > + break;
> > + default:
> > + WARN_ONCE(1, "Can't fill with pixel width %d", px_width);
> > + }
> > +}
> > +
> > +static int drm_splash_fill_solid_color(struct drm_client_buffer *buffer, u32 color)
> > +{
> > + struct drm_client_dev *client = buffer->client;
> > + struct drm_framebuffer *fb = buffer->fb;
> > + struct drm_rect r = DRM_RECT_INIT(0, 0, fb->width, fb->height);
> > + u32 px_width = fb->format->cpp[0];
> > + struct iosys_map map;
> > + int ret;
> > +
> > + ret = drm_client_buffer_vmap_local(buffer, &map);
> > + if (ret) {
> > + drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
> > + return ret;
> > + }
> > +
> > + drm_splash_fill(&map, fb->pitches[0], drm_rect_height(&r),
> > + drm_rect_width(&r), px_width, color);
> > +
> > + drm_client_buffer_vunmap_local(buffer);
> > +
> > + return drm_client_buffer_flush(buffer, &r);
> > +}
> > +
> > +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
> > +static void drm_splash_blit_pix16(struct iosys_map *map, unsigned int dpitch,
> > + unsigned int x_pad, unsigned int y_pad,
> > + const u8 *sbuf8, unsigned int spitch,
> > + unsigned int width, unsigned int height,
> > + bool invert_y, u32 format)
> > +{
> > + unsigned int x, y, src_offset, dst_offset;
> > + u32 scolor, dcolor, wr_off;
> > +
> > + for (y = 0; y < height; y++) {
> > + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> > + dst_offset = (y_pad + y) * dpitch;
> > +
> > + for (x = 0; x < width; x++) {
> > + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
> > + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
> > + wr_off = dst_offset + (x_pad + x) * sizeof(u16);
> > +
> > + iosys_map_wr(map, wr_off, u16, dcolor);
> > + }
> > + }
> > +}
> > +
> > +static void drm_splash_blit_pix24(struct iosys_map *map, unsigned int dpitch,
> > + unsigned int x_pad, unsigned int y_pad,
> > + const u8 *sbuf8, unsigned int spitch,
> > + unsigned int width, unsigned int height,
> > + bool invert_y, u32 format)
> > +{
> > + unsigned int x, y, src_offset, dst_offset;
> > + u32 scolor, dcolor, wr_off;
> > +
> > + for (y = 0; y < height; y++) {
> > + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> > + dst_offset = (y_pad + y) * dpitch;
> > +
> > + for (x = 0; x < width; x++) {
> > + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
> > + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
> > + wr_off = dst_offset + (x_pad + x) * 3;
> > +
> > + iosys_map_wr(map, wr_off, u8, (dcolor & 0x000000FF) >> 0);
> > + iosys_map_wr(map, wr_off + 1, u8, (dcolor & 0x0000FF00) >> 8);
> > + iosys_map_wr(map, wr_off + 2, u8, (dcolor & 0x00FF0000) >> 16);
> > + }
> > + }
> > +}
> > +
> > +static void drm_splash_blit_pix32(struct iosys_map *map, unsigned int dpitch,
> > + unsigned int x_pad, unsigned int y_pad,
> > + const u8 *sbuf8, unsigned int spitch,
> > + unsigned int width, unsigned int height,
> > + bool invert_y, u32 format)
> > +{
> > + unsigned int x, y, src_offset, dst_offset;
> > + u32 scolor, dcolor, wr_off;
> > +
> > + for (y = 0; y < height; y++) {
> > + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> > + dst_offset = (y_pad + y) * dpitch;
> > +
> > + for (x = 0; x < width; x++) {
> > + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
> > + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
> > + wr_off = dst_offset + (x_pad + x) * sizeof(u32);
> > +
> > + iosys_map_wr(map, wr_off, u32, dcolor);
> > + }
> > + }
> > +}
> > +
> > +static void drm_splash_blit_rgb888(struct iosys_map *map, unsigned int dpitch,
> > + unsigned int x_pad, unsigned int y_pad,
> > + const u8 *sbuf8, unsigned int spitch,
> > + unsigned int width, unsigned int height,
> > + bool invert_y)
> > +{
> > + unsigned int y, src_offset, dst_offset;
> > +
> > + for (y = 0; y < height; y++) {
> > + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> > + dst_offset = (y_pad + y) * dpitch + x_pad * 3;
> > +
> > + iosys_map_memcpy_to(map, dst_offset, &sbuf8[src_offset], width * 3);
> > + }
> > +}
> > +
> > +static int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
> > + const u8 *data, size_t data_len)
> > +
> > +{
> > + struct drm_client_buffer *buffer = scanout->buffer;
> > + struct drm_client_dev *client = buffer->client;
> > + struct drm_framebuffer *fb = buffer->fb;
> > + u32 px_width = fb->format->cpp[0];
> > + const struct bmp_file_header *file_header;
> > + const struct bmp_dib_header *dib_header;
> > + u32 bmp_width, bmp_height, bmp_pitch;
> > + bool bmp_invert_y;
> > + unsigned int x_pad, y_pad;
> > + const u8 *image_data;
> > + struct iosys_map map;
> > + struct drm_rect r;
> > + int ret;
> > +
> > + if (data_len < (sizeof(*file_header) + sizeof(*dib_header))) {
> > + drm_err(client->dev, "splash: BMP file too short");
> > + return -EINVAL;
> > + }
> > +
> > + file_header = (const struct bmp_file_header *)data;
> > + if (file_header->id != BMP_FILE_MAGIC_ID) {
> > + drm_err(client->dev, "splash: invalid BMP magic 0x%04X",
> > + file_header->id);
> > + return -EINVAL;
> > + }
> > +
> > + dib_header = (const struct bmp_dib_header *)(data + sizeof(*file_header));
> > +
> > + /* Restrict supported format to uncompressed, 24bit RGB888 */
> > + if (dib_header->dib_header_size != 40 || dib_header->width < 0 ||
> > + dib_header->planes != 1 || dib_header->compression != 0 ||
> > + dib_header->bpp != 24) {
> > + drm_err(client->dev, "splash: invalid BMP format");
> > + return -EINVAL;
> > + }
> > +
> > + bmp_width = dib_header->width;
> > + bmp_height = abs(dib_header->height);
> > + bmp_pitch = round_up(3 * bmp_width, 4);
> > + bmp_invert_y = (dib_header->height > 0);
> > +
> > + if ((file_header->bitmap_offset + bmp_pitch * bmp_height) > data_len) {
> > + drm_err(client->dev, "splash: invalid BMP size");
> > + return -EINVAL;
> > + }
> > +
> > + if (bmp_width > scanout->width || bmp_height > scanout->height) {
> > + drm_err(client->dev, "splash: BMP image is too big for the screen");
> > + return -EINVAL;
> > + }
> > +
> > + image_data = data + file_header->bitmap_offset;
> > +
> > + ret = drm_client_buffer_vmap_local(buffer, &map);
> > + if (ret) {
> > + drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
> > + return ret;
> > + }
> > +
> > + /* Center X and Y */
> > + x_pad = (scanout->width - bmp_width) / 2;
> > + y_pad = (scanout->height - bmp_height) / 2;
> > + r = DRM_RECT_INIT(x_pad, y_pad, bmp_width, bmp_height);
> > +
> > + /* In case the target format is RGB888, source data can be copied to
> > + * the video buffer line by line, avoiding some overhead.
> > + */
> > + if (scanout->format == DRM_FORMAT_RGB888) {
> > + drm_splash_blit_rgb888(&map, fb->pitches[0], x_pad, y_pad,
> > + image_data, bmp_pitch, bmp_width,
> > + bmp_height, bmp_invert_y);
> > + } else {
> > + switch (px_width) {
> > + case 2:
> > + drm_splash_blit_pix16(&map, fb->pitches[0], x_pad,
> > + y_pad, image_data, bmp_pitch,
> > + bmp_width, bmp_height,
> > + bmp_invert_y, scanout->format);
> > + break;
> > + case 3:
> > + drm_splash_blit_pix24(&map, fb->pitches[0], x_pad,
> > + y_pad, image_data, bmp_pitch,
> > + bmp_width, bmp_height,
> > + bmp_invert_y, scanout->format);
> > + break;
> > + case 4:
> > + drm_splash_blit_pix32(&map, fb->pitches[0], x_pad,
> > + y_pad, image_data, bmp_pitch,
> > + bmp_width, bmp_height,
> > + bmp_invert_y, scanout->format);
> > + break;
> > + default:
> > + drm_warn_once(client->dev,
> > + "splash: can't blit with pixel width %d",
> > + px_width);
> > + }
> > + }
> > +
> > + drm_client_buffer_vunmap_local(buffer);
> > +
> > + return drm_client_buffer_flush(buffer, &r);
> > +}
> > +#else
> > +static inline int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
> > + const u8 *data, size_t data_len)
> > +{
> > + return -EOPNOTSUPP;
> > +}
> > +#endif
> > +
> > +static int drm_splash_draw_scanout(struct drm_splash *splash,
> > + struct drm_splash_scanout *scanout,
> > + const u8 *data, size_t data_len)
> > +{
> > + if (!scanout->buffer)
> > + return -ENODEV;
> > +
> > + if (!scanout->bg_drawn) {
> > + u32 color = drm_draw_color_from_xrgb8888(splash_color,
> > + scanout->format);
> > + drm_splash_fill_solid_color(scanout->buffer, color);
> > + scanout->bg_drawn = true;
> > + }
> > +
> > + if (data != NULL) {
> > + /* Ignore the return value, since the solid color has already
> > + * been drawn to screen.
> > + */
> > + drm_splash_bmp_to_scanout(scanout, data, data_len);
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static int drm_splash_render_thread(void *data)
> > +{
> > + struct drm_splash *splash = data;
> > + struct drm_client_dev *client = &splash->client;
> > +
> > + while (!kthread_should_stop()) {
> > + unsigned int draw_count = 0;
> > + drm_splash_data_get_func_t get_fn = NULL;
> > + drm_splash_data_release_func_t release_fn = NULL;
> > + void *priv = NULL;
> > + const u8 *img_data = NULL;
> > + size_t img_data_len = 0;
> > + int i, ret;
> > +
> > + drm_splash_data_source_pop(splash, &get_fn, &release_fn, &priv);
> > +
> > + if (get_fn) {
> > + ret = get_fn(priv, &img_data, &img_data_len);
> > + if (ret) {
> > + drm_err(client->dev,
> > + "splash: failed to get image data: %d",
> > + ret);
> > + }
> > + }
> > +
> > + for (i = 0; i < splash->n_scanout; i++) {
> > + ret = drm_splash_draw_scanout(splash,
> > + &splash->scanout[i],
> > + img_data, img_data_len);
> > + if (ret) {
> > + drm_err(client->dev,
> > + "splash: failed to fill scanout %d: %d",
> > + i, ret);
> > + continue;
> > + }
> > +
> > + draw_count++;
> > + }
> > +
> > + if (release_fn)
> > + release_fn(priv);
> > +
> > + if (draw_count > 0) {
> > + ret = drm_client_modeset_commit(client);
> > + /* If commit returns EBUSY, another master showed up.
> > + * This means that the splash is no more required.
> > + */
> > + if (ret == -EBUSY) {
> > + drm_info(client->dev,
> > + "splash: not master anymore, exiting");
> > + break;
> > + }
> > + }
> > +
> > + /* If no changes arrived in the mean time, wait to be awaken,
> > + * e.g.by a firmware callback.
> > + */
> > + if (atomic_xchg(&splash->pending, 0) == 0)
> > + set_current_state(TASK_UNINTERRUPTIBLE);
> > +
> > + schedule();
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static inline void drm_splash_wake_render_thread(struct drm_splash *splash)
> > +{
> > + atomic_set(&splash->pending, 1);
> > + wake_up_process(splash->thread);
> > +}
> > +
> > +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
> > +static int drm_splash_fw_get(void *priv, const u8 **data, size_t *size)
> > +{
> > + const struct firmware *fw = priv;
> > +
> > + if (!fw)
> > + return -ENODATA;
> > +
> > + *data = fw->data;
> > + *size = fw->size;
> > +
> > + return 0;
> > +}
> > +
> > +static void drm_splash_fw_release(void *priv)
> > +{
> > + const struct firmware *fw = priv;
> > +
> > + if (fw)
> > + release_firmware(fw);
> > +}
> > +
> > +static void drm_splash_fw_callback(const struct firmware *fw, void *context)
> > +{
> > + struct drm_splash *splash = context;
> > + struct drm_client_dev *client = &splash->client;
> > +
> > + if (!fw || !fw->data) {
> > + drm_err(client->dev, "splash: no firmware");
> > + return;
> > + }
> > +
> > + drm_splash_data_source_push(splash, drm_splash_fw_get,
> > + drm_splash_fw_release, (void *)fw);
> > +
> > + /* Wake the render thread */
> > + drm_dbg(client->dev, "splash: firmware loaded, wake up drawing thread");
> > + drm_splash_wake_render_thread(splash);
> > +}
> > +
> > +static int drm_splash_fw_request_bmp(struct drm_splash *splash)
> > +{
> > + struct drm_client_dev *client = &splash->client;
> > +
> > + drm_info(client->dev, "splash: request %s as firmware", splash_bmp);
> > +
> > + return request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
> > + splash_bmp, client->dev->dev, GFP_KERNEL,
> > + splash, drm_splash_fw_callback);
> > +}
> > +#else
> > +static inline int drm_splash_fw_request_bmp(struct drm_splash *splash)
> > +{
> > + return -EOPNOTSUPP;
> > +}
> > +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
> > +
> > +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT)
> > +static int drm_splash_bgrt_get_data(void *priv, const u8 **data, size_t *size)
> > +{
> > + void *bgrt_image = priv;
> > +
> > + *data = bgrt_image;
> > + *size = bgrt_image_size;
> > +
> > + return 0;
> > +}
> > +
> > +static void drm_splash_bgrt_release(void *priv)
> > +{
> > + void *bgrt_image = priv;
> > +
> > + if (bgrt_image)
> > + memunmap(bgrt_image);
> > +}
> > +
> > +static int drm_splash_bgrt_load(struct drm_splash *splash)
> > +{
> > + struct drm_client_dev *client = &splash->client;
> > + void *bgrt_image = NULL;
> > +
> > + drm_info(client->dev, "splash: using EFI BGRT");
> > +
> > + if (!bgrt_tab.image_address) {
> > + drm_info(client->dev, "splash: no BGRT found");
> > + return -ENOENT;
> > + }
> > +
> > + if (bgrt_tab.status & 0x06) {
> > + drm_info(client->dev, "splash: BGRT rotation bits set, skipping");
> > + return -EOPNOTSUPP;
> > + }
> > +
> > + drm_dbg(client->dev, "splash: BGRT image is at 0x%016llx, size=%zX",
> > + bgrt_tab.image_address, bgrt_image_size);
> > +
> > + bgrt_image = memremap(bgrt_tab.image_address, bgrt_image_size,
> > + MEMREMAP_WB);
> > + if (!bgrt_image) {
> > + drm_warn(client->dev, "splash: failed to map BGRT image memory");
> > + return -ENOMEM;
> > + }
> > +
> > + drm_splash_data_source_push(splash, drm_splash_bgrt_get_data,
> > + drm_splash_bgrt_release, bgrt_image);
> > +
> > + drm_splash_wake_render_thread(splash);
> > +
> > + return 0;
> > +}
> > +#else
> > +static inline int drm_splash_bgrt_load(struct drm_splash *splash)
> > +{
> > + return -EOPNOTSUPP;
> > +}
> > +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT
> > +
> > +static int drm_splash_init_client(struct drm_splash *splash)
> > +{
> > + struct drm_client_dev *client = &splash->client;
> > + struct drm_mode_set *modeset;
> > + unsigned int modeset_mask = 0;
> > + unsigned int fb_count = 0;
> > + int j;
> > +
> > + if (drm_client_modeset_probe(client, 0, 0))
> > + return -1;
> > +
> > + j = 0;
> > + drm_client_for_each_modeset(modeset, client) {
> > + struct drm_splash_scanout *tmp;
> > + struct drm_splash_scanout *scanout;
> > + u32 format;
> > + int id = -1;
> > +
> > + /* Skip modesets without a mode */
> > + if (!modeset->mode)
> > + continue;
> > +
> > + if (modeset->connectors[0]->has_tile) {
> > + struct drm_splash_scanout *tiled;
> > + int new_id = modeset->connectors[0]->tile_group->id;
> > +
> > + /* Tiled modesets contribute to a single framebuffer,
> > + * check if this tiled group has already been seen.
> > + */
> > + tiled = get_scanout_from_tile_group(splash, new_id);
> > + if (tiled != NULL) {
> > + if (!modeset->x)
> > + tiled->width += modeset->mode->vdisplay;
> > + if (!modeset->y)
> > + tiled->height += modeset->mode->hdisplay;
> > + modeset->fb = tiled->buffer->fb;
> > + continue;
> > + }
> > +
> > + /* New tile group, save its ID for later */
> > + id = new_id;
> > + }
> > +
> > + format = drm_splash_find_usable_format(modeset->crtc->primary,
> > + splash->preferred_format);
> > + if (format == DRM_FORMAT_INVALID) {
> > + drm_warn(client->dev,
> > + "splash: can't find a usable format for modeset");
> > + continue;
> > + }
> > +
> > + tmp = krealloc(splash->scanout,
> > + (splash->n_scanout + 1) * sizeof(*splash->scanout),
> > + GFP_KERNEL);
> > + if (!tmp) {
> > + drm_warn(client->dev,
> > + "splash: can't reallocate the scanout array");
> > + break;
> > + }
> > +
> > + splash->scanout = tmp;
> > + scanout = &splash->scanout[splash->n_scanout];
> > + splash->n_scanout++;
> > +
> > + memset(scanout, 0, sizeof(*scanout));
> > + scanout->id = id;
> > + scanout->format = format;
> > + scanout->width = modeset->mode->hdisplay;
> > + scanout->height = modeset->mode->vdisplay;
> > +
> > + modeset_mask |= BIT(j);
> > + j++;
> > + }
> > +
> > + /* Now that all sensible modesets have been collected, allocate buffers */
> > + j = 0;
> > + drm_client_for_each_modeset(modeset, client) {
> > + struct drm_splash_scanout *scanout;
> > +
> > + if (!(modeset_mask & BIT(j)))
> > + continue;
> > +
> > + scanout = &splash->scanout[j];
> > + j++;
> > +
> > + scanout->buffer = drm_client_buffer_create_dumb(client,
> > + scanout->width,
> > + scanout->height,
> > + scanout->format);
> > + if (IS_ERR(scanout->buffer)) {
> > + drm_warn(client->dev,
> > + "splash: can't create dumb buffer %d %d %p4cc",
> > + scanout->width, scanout->height, &scanout->format);
> > + continue;
> > + }
> > +
> > + drm_info(client->dev, "splash: created dumb buffer %d %d %p4cc",
> > + scanout->width, scanout->height, &scanout->format);
> > +
> > + modeset->fb = scanout->buffer->fb;
> > + fb_count++;
> > + }
> > +
> > + return (fb_count == 0) ? -ENODEV : 0;
> > +}
> > +
> > +static void drm_splash_free_scanout(struct drm_client_dev *client)
> > +{
> > + struct drm_splash *splash = client_to_drm_splash(client);
> > + int i;
> > +
> > + if (splash->n_scanout) {
> > + for (i = 0; i < splash->n_scanout; i++)
> > + drm_client_buffer_delete(splash->scanout[i].buffer);
> > +
> > + splash->n_scanout = 0;
> > + kfree(splash->scanout);
> > + splash->scanout = NULL;
> > + }
> > +}
> > +
> > +static int drm_splash_client_hotplug(struct drm_client_dev *client)
> > +{
> > + struct drm_splash *splash = client_to_drm_splash(client);
> > + int ret;
> > +
> > + guard(mutex)(&splash->hotplug_lock);
> > +
> > + /* The modesets that get a splash are defined at first hotplug event */
> > + if (splash->initialized)
> > + return 0;
> > +
> > + ret = drm_splash_init_client(splash);
> > + if (ret == -ENODEV) {
> > + drm_info(client->dev, "splash: no modeset found");
> > + return 0;
> > + } else if (ret) {
> > + drm_err(client->dev,
> > + "splash: failed to init client: %d", ret);
> > + return ret;
> > + }
> > +
> > + /* Create the render thread, waken later */
> > + splash->thread = kthread_create(drm_splash_render_thread,
> > + splash, "drm_splash_%s",
> > + client->dev->unique);
> > + if (IS_ERR(splash->thread)) {
> > + ret = PTR_ERR(splash->thread);
> > + drm_err(client->dev, "splash: failed to create render thread: %d", ret);
> > + drm_splash_free_scanout(client);
> > + return ret;
> > + }
> > +
> > + if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP))
> > + ret = drm_splash_fw_request_bmp(splash);
> > + else if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT))
> > + ret = drm_splash_bgrt_load(splash);
>
> I'm not sure this logic will actually work in this order.
> CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT selects
> CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
>
> So won't the BGRT path never run?
>
Actually, CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT selects
CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT, which builds the bitmap file
format support shared by both the bmp-as-a-firmware and BGRT "backends".
> > + else
> > + ret = 0;
> > +
> > + if (ret) {
> > + drm_err(client->dev, "splash: failed to kick image load: %d", ret);
> > + kthread_stop(splash->thread);
> > + drm_splash_free_scanout(client);
> > + return ret;
> > + }
> > +
> > + /* Wake the render thread to show initial contents */
> > + drm_splash_wake_render_thread(splash);
> > +
> > + splash->initialized = true;
> > +
> > + return 0;
> > +}
> > +
> > +static int drm_splash_client_restore(struct drm_client_dev *client, bool force)
> > +{
> > + int ret;
> > +
> > + if (force)
> > + ret = drm_client_modeset_commit_locked(client);
> > + else
> > + ret = drm_client_modeset_commit(client);
> > +
> > + return ret;
> > +}
> > +
> > +static void drm_splash_client_unregister(struct drm_client_dev *client)
> > +{
> > + struct drm_splash *splash = client_to_drm_splash(client);
> > +
> > + kthread_stop(splash->thread);
> > + drm_splash_free_scanout(client);
> > + drm_client_release(client);
> > +
> > + if (splash->data_release)
> > + splash->data_release(splash->data_priv);
> > +}
> > +
> > +static void drm_splash_client_free(struct drm_client_dev *client)
> > +{
> > + struct drm_splash *splash = client_to_drm_splash(client);
> > + struct drm_device *dev = client->dev;
> > +
> > + mutex_destroy(&splash->hotplug_lock);
> > + kfree(splash);
> > +
> > + drm_dbg(dev, "Unregistered with drm splash");
> > +}
> > +
> > +static const struct drm_client_funcs drm_splash_client_funcs = {
> > + .owner = THIS_MODULE,
> > + .hotplug = drm_splash_client_hotplug,
> > + .restore = drm_splash_client_restore,
> > + .unregister = drm_splash_client_unregister,
> > + .free = drm_splash_client_free,
> > +};
> > +
> > +/**
> > + * drm_splash_register() - Register a drm device to drm_splash
> > + * @dev: the drm device to register.
> > + * @format: drm device preferred format.
> > + */
> > +void drm_splash_register(struct drm_device *dev,
> > + const struct drm_format_info *format)
> > +{
> > + struct drm_splash *splash;
> > +
> > + splash = kzalloc(sizeof(*splash), GFP_KERNEL);
> > + if (!splash)
> > + goto err_warn;
> > +
> > + mutex_init(&splash->hotplug_lock);
> > + spin_lock_init(&splash->data_lock);
> > +
> > + if (format && format->num_planes == 1)
> > + splash->preferred_format = format->format;
> > + else
> > + splash->preferred_format = DRM_FORMAT_RGB888;
> > +
> > + if (drm_client_init(dev, &splash->client, "drm_splash",
> > + &drm_splash_client_funcs))
> > + goto err_free;
> > +
> > + drm_client_register(&splash->client);
> > + drm_dbg(dev, "Registered with drm splash");
> > +
> > + return;
> > +
> > +err_free:
> > + kfree(splash);
> > +err_warn:
> > + drm_warn(dev, "Failed to register with drm splash");
> > +}
> >
>
>
Thank you!
Reagrds,
Francesco
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Francesco Valla @ 2026-01-06 20:22 UTC (permalink / raw)
To: Geert Uytterhoeven
Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas, Sam Ravnborg, Mario Limonciello,
linux-kernel, dri-devel, linux-doc, linux-embedded
In-Reply-To: <CAMuHMdUtsx1gQffk9c-U9UkeqG_Dopv5vXNrp72ewh0EQQgwjQ@mail.gmail.com>
Hi Geert,
On Tue, Jan 06, 2026 at 03:47:49PM +0100, Geert Uytterhoeven wrote:
> Hi Franceso,
>
> On Tue, 6 Jan 2026 at 15:26, Francesco Valla <francesco@valla.it> wrote:
> > Add a DRM client that draws a simple splash, with possibility to show:
> >
> > - a colored background;
> > - a static BMP image (loaded as firmware);
> > - the logo provided by EFI BGRT.
> >
> > The client is not meant to replace a full-featured bootsplash, but
> > rather to remove some complexity (and hopefully boot time) on small
> > embedded platforms or on systems with a limited scope (e.g: recovery
> > or manufacturing images).
> >
> > The background color can be set either at build time from a dedicated
> > config option or at runtime through the drm_client_lib.splash_color
> > command line parameter. Any color in RGB888 format can be used.
> >
> > If enabled, the static BMP image is loaded using the kernel firmware
> > infrastructure; a valid BMP image with 24bpp color and no compression
> > is expected. The name of the image can be set through the
> > drm_client_lib.splash_bmp kernel command line parameter, with the
> > default being 'drm_splash.bmp'.
> >
> > Just like the existing DRM clients, the splash can be enabled from the
> > kernel command line using drm_client_lib.active=splash.
> >
> > Signed-off-by: Francesco Valla <francesco@valla.it>
>
> Thanks for your patch!
>
> > --- /dev/null
> > +++ b/drivers/gpu/drm/clients/drm_splash.c
>
> > +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
>
> There is no need to protect this block with #if, as it does not generate
> any code.
>
This was left here when I moved some code along - I'll remove the
guards.
> > +#define BMP_FILE_MAGIC_ID 0x4d42
> > +
> > +/* BMP header structures copied from drivers/video/fbdev/efifb.c */
> > +struct bmp_file_header {
> > + u16 id;
> > + u32 file_size;
> > + u32 reserved;
> > + u32 bitmap_offset;
> > +} __packed;
> > +
> > +struct bmp_dib_header {
> > + u32 dib_header_size;
> > + s32 width;
> > + s32 height;
> > + u16 planes;
> > + u16 bpp;
> > + u32 compression;
> > + u32 bitmap_size;
> > + u32 horz_resolution;
> > + u32 vert_resolution;
> > + u32 colors_used;
> > + u32 colors_important;
> > +} __packed;
> > +#endif // CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT
>
> As per [1], all these values are little-endian. Hence they should
> be declared as such using le16 or le32, and accessed using
> get_unalined_le{16,32}().
>
> [1] https://en.wikipedia.org/wiki/BMP_file_format#File_structure
>
This is obviously right, I'll use proper accessors in next version.
I wonder why this is done precisely this way inside the efifb driver,
but that's maybe because all EFI platforms are little-endian?
> Gr{oetje,eeting}s,
>
> Geert
>
Thank you!
Reagrds,
Francesco
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Mario Limonciello (AMD) (kernel.org) @ 2026-01-06 18:58 UTC (permalink / raw)
To: Francesco Valla, Maarten Lankhorst, Maxime Ripard,
Thomas Zimmermann, David Airlie, Simona Vetter, Jonathan Corbet,
Jocelyn Falempe, Javier Martinez Canillas
Cc: Sam Ravnborg, linux-kernel, dri-devel, linux-doc, linux-embedded
In-Reply-To: <20260106-drm_client_splash-v2-1-6e86a7434b59@valla.it>
On 1/6/2026 8:25 AM, Francesco Valla wrote:
> Add a DRM client that draws a simple splash, with possibility to show:
>
> - a colored background;
> - a static BMP image (loaded as firmware);
> - the logo provided by EFI BGRT.
That's cool, thanks for implementing my suggestion!
>
> The client is not meant to replace a full-featured bootsplash, but
> rather to remove some complexity (and hopefully boot time) on small
> embedded platforms or on systems with a limited scope (e.g: recovery
> or manufacturing images).
In theory when using EFI BGRT you could:
* have the BIOS show the BGRT
* bootloader not show anything
* kernel overwrite the framebuffer containing the BIOS BGRT with the
exact same image (should be invisible to the user)
* regular bootsplash like plymouth overwrite the framebuffer again with
the same BGRT but then also add progress bar/distro logo etc.
>
> The background color can be set either at build time from a dedicated
> config option or at runtime through the drm_client_lib.splash_color
> command line parameter. Any color in RGB888 format can be used.
>
> If enabled, the static BMP image is loaded using the kernel firmware
> infrastructure; a valid BMP image with 24bpp color and no compression
> is expected. The name of the image can be set through the
> drm_client_lib.splash_bmp kernel command line parameter, with the
> default being 'drm_splash.bmp'.
>
> Just like the existing DRM clients, the splash can be enabled from the
> kernel command line using drm_client_lib.active=splash.
>
> Signed-off-by: Francesco Valla <francesco@valla.it>
> ---
> drivers/gpu/drm/clients/Kconfig | 79 ++-
> drivers/gpu/drm/clients/Makefile | 1 +
> drivers/gpu/drm/clients/drm_client_internal.h | 9 +
> drivers/gpu/drm/clients/drm_client_setup.c | 8 +
> drivers/gpu/drm/clients/drm_splash.c | 883 ++++++++++++++++++++++++++
> 5 files changed, 979 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
> index 6096c623d9d5b1a3d4a40d986c45aad2f8277767..dd8cd6cacd1166932eb3890dd816b9ae2d26330f 100644
> --- a/drivers/gpu/drm/clients/Kconfig
> +++ b/drivers/gpu/drm/clients/Kconfig
> @@ -12,6 +12,7 @@ config DRM_CLIENT_LIB
> config DRM_CLIENT_SELECTION
> tristate
> depends on DRM
> + select DRM_CLIENT_LIB if DRM_CLIENT_SPLASH
> select DRM_CLIENT_LIB if DRM_CLIENT_LOG
> select DRM_CLIENT_LIB if DRM_FBDEV_EMULATION
> help
> @@ -85,10 +86,79 @@ config DRM_CLIENT_LOG
> If you only need logs, but no terminal, or if you prefer userspace
> terminal, say "Y".
>
> +config DRM_CLIENT_SPLASH
> + bool "Display graphic splash"
> + depends on DRM_CLIENT_SELECTION
> + select DRM_CLIENT
> + select DRM_CLIENT_SETUP
> + select DRM_DRAW
> + help
> + This enables a splash drm client, able to display either a plain
> + color or a static image until the userspace is ready to take over.
> + The splash will be displayed on all screens available at boot, if
> + any, or on the ones part of the first hotplug event.
> +
> +config DRM_CLIENT_SPLASH_BACKGROUND_COLOR
> + hex "Splash background color"
> + depends on DRM_CLIENT_SPLASH
> + default 0x000000
> + help
> + The default splash background color, in RGB888 format.
> +
> + The color can be overridden through the drm_client_lib.splash_color
> + kernel command line parameter.
> +
> +config DRM_CLIENT_SPLASH_BMP_SUPPORT
> + bool
> +
> +choice
> + prompt "Splash source"
> + depends on DRM_CLIENT_SPLASH
> + default DRM_CLIENT_SPLASH_SRC_COLOR
> + help
> + Selects the source for the splash graphic.
> +
> +config DRM_CLIENT_SPLASH_SRC_COLOR
> + bool "Solid color"
> + help
> + Use a solid color as splash. The color is selected through the
> + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option.
> +
> + The image will be loaded using the firmware loading facility the
> + kernel provides.
> +
> +config DRM_CLIENT_SPLASH_SRC_BMP
> + bool "BMP image"
> + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> + select FW_LOADER
> + help
> + Use a BMP (bitmap) image as splash. If the image is smaller than the
> + display(s), it will be centered and the color specified through the
> + DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option will be used as
> + background.
> +
> + The image will be loaded using the firmware loading facility the
> + kernel provides; it shall use 24 bits per pixel and shall not be
> + compressed. The name of the file can be set through the
> + drm_client_lib.splash_bmp command line parameter, with the default
> + being 'drm_splash.bmp'.
> +
> +config DRM_CLIENT_SPLASH_SRC_BGRT
> + bool "EFI BGRT"
> + select DRM_CLIENT_SPLASH_BMP_SUPPORT
> + depends on EFI
> + help
> + Use the BGRT image provided by the EFI bootloader. If the image is
> + smaller than the display(s), it will be centered and the color
> + specified through the DRM_CLIENT_SPLASH_BACKGROUND_COLOR config
> + option will be used as background.
> +
> +endchoice
> +
> choice
> prompt "Default DRM Client"
> depends on DRM_CLIENT_SELECTION
> - depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG
> + depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG || DRM_CLIENT_SPLASH
> default DRM_CLIENT_DEFAULT_FBDEV
> help
> Selects the default drm client.
> @@ -111,6 +181,12 @@ config DRM_CLIENT_DEFAULT_LOG
> screen, but doesn't implement a full terminal. For that you will need
> a userspace terminal using drm/kms.
>
> +config DRM_CLIENT_DEFAULT_SPLASH
> + bool "splash"
> + depends on DRM_CLIENT_SPLASH
> + help
> + Use splash as default drm client.
> +
> endchoice
>
> config DRM_CLIENT_DEFAULT
> @@ -118,6 +194,7 @@ config DRM_CLIENT_DEFAULT
> depends on DRM_CLIENT
> default "fbdev" if DRM_CLIENT_DEFAULT_FBDEV
> default "log" if DRM_CLIENT_DEFAULT_LOG
> + default "splash" if DRM_CLIENT_DEFAULT_SPLASH
Whitespace off by a character?
> default ""
>
> endmenu
> diff --git a/drivers/gpu/drm/clients/Makefile b/drivers/gpu/drm/clients/Makefile
> index c16addbc327f09572aa3142cbf0d1d13f172a9e9..3df02d10cd18a47d7e8d7cee70163b0ef0129b51 100644
> --- a/drivers/gpu/drm/clients/Makefile
> +++ b/drivers/gpu/drm/clients/Makefile
> @@ -5,4 +5,5 @@ subdir-ccflags-y += -I$(src)/..
> drm_client_lib-y := drm_client_setup.o
> drm_client_lib-$(CONFIG_DRM_CLIENT_LOG) += drm_log.o
> drm_client_lib-$(CONFIG_DRM_FBDEV_EMULATION) += drm_fbdev_client.o
> +drm_client_lib-$(CONFIG_DRM_CLIENT_SPLASH) += drm_splash.o
> obj-$(CONFIG_DRM_CLIENT_LIB) += drm_client_lib.o
> diff --git a/drivers/gpu/drm/clients/drm_client_internal.h b/drivers/gpu/drm/clients/drm_client_internal.h
> index 6dc078bf6503b902cbb3267b64ea42d9f1c23375..48ee0c1c2529882b2bf5dc786788390823e25cd6 100644
> --- a/drivers/gpu/drm/clients/drm_client_internal.h
> +++ b/drivers/gpu/drm/clients/drm_client_internal.h
> @@ -22,4 +22,13 @@ void drm_log_register(struct drm_device *dev);
> static inline void drm_log_register(struct drm_device *dev) {}
> #endif
>
> +#ifdef CONFIG_DRM_CLIENT_SPLASH
> +void drm_splash_register(struct drm_device *dev,
> + const struct drm_format_info *format);
> +#else
> +static inline void drm_splash_register(struct drm_device *dev,
> + const struct drm_format_info *format)
> +{}
> +#endif
> +
> #endif
> diff --git a/drivers/gpu/drm/clients/drm_client_setup.c b/drivers/gpu/drm/clients/drm_client_setup.c
> index 515aceac22b18161a14bd60be4acedc1ddd05bc5..c19498938ee3ba442f7502b27a9c33dfff45a203 100644
> --- a/drivers/gpu/drm/clients/drm_client_setup.c
> +++ b/drivers/gpu/drm/clients/drm_client_setup.c
> @@ -56,6 +56,14 @@ void drm_client_setup(struct drm_device *dev, const struct drm_format_info *form
> return;
> }
> #endif
> +
> +#ifdef CONFIG_DRM_CLIENT_SPLASH
> + if (!strcmp(drm_client_default, "splash")) {
> + drm_splash_register(dev, format);
> + return;
> + }
> +#endif
> +
> if (strcmp(drm_client_default, ""))
> drm_warn(dev, "Unknown DRM client %s\n", drm_client_default);
> }
> diff --git a/drivers/gpu/drm/clients/drm_splash.c b/drivers/gpu/drm/clients/drm_splash.c
> new file mode 100644
> index 0000000000000000000000000000000000000000..204a5256ef03b6edb81e5dc8c49b5929cb51ff92
> --- /dev/null
> +++ b/drivers/gpu/drm/clients/drm_splash.c
> @@ -0,0 +1,883 @@
> +// SPDX-License-Identifier: GPL-2.0 or MIT
> +/*
> + * Copyright (c) 2025-2026 Francesco Valla <francesco@valla.it>
> + *
> + */
> +
> +#include <linux/atomic.h>
> +#include <linux/device.h>
> +#include <linux/efi-bgrt.h>
> +#include <linux/firmware.h>
> +#include <linux/init.h>
> +#include <linux/iosys-map.h>
> +#include <linux/kthread.h>
> +#include <linux/module.h>
> +#include <linux/types.h>
> +
> +#include <drm/drm_client.h>
> +#include <drm/drm_drv.h>
> +#include <drm/drm_fourcc.h>
> +#include <drm/drm_framebuffer.h>
> +#include <drm/drm_plane.h>
> +#include <drm/drm_print.h>
> +
> +#include "drm_client_internal.h"
> +#include "drm_draw_internal.h"
> +#include "drm_internal.h"
> +
> +/**
> + * DOC: overview
> + *
> + * This is a simple graphic bootsplash, able to display either a plain color or
> + * a static image.
> + */
> +
> +static unsigned int splash_color = CONFIG_DRM_CLIENT_SPLASH_BACKGROUND_COLOR;
> +module_param(splash_color, uint, 0400);
> +MODULE_PARM_DESC(splash_color, "Splash background color (RGB888)");
Does this really need to be a module parameter(s) in addition to the
kconfig option(s)? Just wondering what the use case you envisioned is.
In my mind I was thinking in an embedded system manufacturing flow maybe
you would set it to red in the case of a hardware test failure on the
line or something.
> +
> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
> +#define DEFAULT_SPLASH_BMP "drm_splash.bmp"
> +static char *splash_bmp = DEFAULT_SPLASH_BMP;
> +module_param(splash_bmp, charp, 0400);
> +MODULE_PARM_DESC(splash_bmp, "Name of splash image (default: \"" DEFAULT_SPLASH_BMP "\")");
> +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
> +
> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
> +#define BMP_FILE_MAGIC_ID 0x4d42
> +
> +/* BMP header structures copied from drivers/video/fbdev/efifb.c */
> +struct bmp_file_header {
> + u16 id;
> + u32 file_size;
> + u32 reserved;
> + u32 bitmap_offset;
> +} __packed;
> +
> +struct bmp_dib_header {
> + u32 dib_header_size;
> + s32 width;
> + s32 height;
> + u16 planes;
> + u16 bpp;
> + u32 compression;
> + u32 bitmap_size;
> + u32 horz_resolution;
> + u32 vert_resolution;
> + u32 colors_used;
> + u32 colors_important;
> +} __packed;
> +#endif // CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT
> +
> +typedef int (*drm_splash_data_get_func_t)(void *priv, const u8 **data, size_t *size);
> +typedef void (*drm_splash_data_release_func_t)(void *priv);
> +
> +struct drm_splash_scanout {
> + int id;
> + u32 format;
> + unsigned int width;
> + unsigned int height;
> + struct drm_client_buffer *buffer;
> + bool bg_drawn;
> +};
> +
> +struct drm_splash {
> + struct drm_client_dev client;
> + u32 preferred_format;
> + struct device dev;
> +
> + struct task_struct *thread;
> + atomic_t pending;
> +
> + struct mutex hotplug_lock;
> + bool initialized;
> +
> + u32 n_scanout;
> + struct drm_splash_scanout *scanout;
> +
> + spinlock_t data_lock;
> + drm_splash_data_get_func_t data_get;
> + drm_splash_data_release_func_t data_release;
> + void *data_priv;
> +};
> +
> +static struct drm_splash *client_to_drm_splash(struct drm_client_dev *client)
> +{
> + return container_of_const(client, struct drm_splash, client);
> +}
> +
> +static void __maybe_unused
> +drm_splash_data_source_push(struct drm_splash *splash,
> + drm_splash_data_get_func_t get,
> + drm_splash_data_release_func_t release,
> + void *priv)
> +{
> + guard(spinlock)(&splash->data_lock);
> +
> + /* Release previous data */
> + if (splash->data_release)
> + splash->data_release(splash->data_priv);
> +
> + splash->data_get = get;
> + splash->data_release = release;
> + splash->data_priv = priv;
> +}
> +
> +static void drm_splash_data_source_pop(struct drm_splash *splash,
> + drm_splash_data_get_func_t *get,
> + drm_splash_data_release_func_t *release,
> + void **priv)
> +{
> + guard(spinlock)(&splash->data_lock);
> +
> + *get = splash->data_get;
> + splash->data_get = NULL;
> +
> + *release = splash->data_release;
> + splash->data_release = NULL;
> +
> + *priv = splash->data_priv;
> + splash->data_priv = NULL;
> +}
> +
> +static struct drm_splash_scanout *
> +get_scanout_from_tile_group(struct drm_splash *splash, int id)
> +{
> + int j;
> +
> + for (j = 0; j < splash->n_scanout; j++)
> + if (splash->scanout[j].id == id)
> + return &splash->scanout[j];
> +
> + return NULL;
> +}
> +
> +static u32 drm_splash_find_usable_format(struct drm_plane *plane,
> + u32 preferred_format)
> +{
> + int i;
> +
> + /* Check if the preferred format can be used */
> + for (i = 0; i < plane->format_count; i++)
> + if (plane->format_types[i] == preferred_format)
> + return preferred_format;
> +
> + /* Otherwise, find the first format that can be converted from XRGB8888 */
> + for (i = 0; i < plane->format_count; i++)
> + if (drm_draw_color_from_xrgb8888(0xffffffff, plane->format_types[i]) != 0)
> + return plane->format_types[i];
> +
> + return DRM_FORMAT_INVALID;
> +}
> +
> +static void drm_splash_fill(struct iosys_map *map, unsigned int dst_pitch,
> + unsigned int height, unsigned int width,
> + u32 px_width, u32 color)
> +{
> + switch (px_width) {
> + case 2:
> + drm_draw_fill16(map, dst_pitch, height, width, color);
> + break;
> + case 3:
> + drm_draw_fill24(map, dst_pitch, height, width, color);
> + break;
> + case 4:
> + drm_draw_fill32(map, dst_pitch, height, width, color);
> + break;
> + default:
> + WARN_ONCE(1, "Can't fill with pixel width %d", px_width);
> + }
> +}
> +
> +static int drm_splash_fill_solid_color(struct drm_client_buffer *buffer, u32 color)
> +{
> + struct drm_client_dev *client = buffer->client;
> + struct drm_framebuffer *fb = buffer->fb;
> + struct drm_rect r = DRM_RECT_INIT(0, 0, fb->width, fb->height);
> + u32 px_width = fb->format->cpp[0];
> + struct iosys_map map;
> + int ret;
> +
> + ret = drm_client_buffer_vmap_local(buffer, &map);
> + if (ret) {
> + drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
> + return ret;
> + }
> +
> + drm_splash_fill(&map, fb->pitches[0], drm_rect_height(&r),
> + drm_rect_width(&r), px_width, color);
> +
> + drm_client_buffer_vunmap_local(buffer);
> +
> + return drm_client_buffer_flush(buffer, &r);
> +}
> +
> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
> +static void drm_splash_blit_pix16(struct iosys_map *map, unsigned int dpitch,
> + unsigned int x_pad, unsigned int y_pad,
> + const u8 *sbuf8, unsigned int spitch,
> + unsigned int width, unsigned int height,
> + bool invert_y, u32 format)
> +{
> + unsigned int x, y, src_offset, dst_offset;
> + u32 scolor, dcolor, wr_off;
> +
> + for (y = 0; y < height; y++) {
> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> + dst_offset = (y_pad + y) * dpitch;
> +
> + for (x = 0; x < width; x++) {
> + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
> + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
> + wr_off = dst_offset + (x_pad + x) * sizeof(u16);
> +
> + iosys_map_wr(map, wr_off, u16, dcolor);
> + }
> + }
> +}
> +
> +static void drm_splash_blit_pix24(struct iosys_map *map, unsigned int dpitch,
> + unsigned int x_pad, unsigned int y_pad,
> + const u8 *sbuf8, unsigned int spitch,
> + unsigned int width, unsigned int height,
> + bool invert_y, u32 format)
> +{
> + unsigned int x, y, src_offset, dst_offset;
> + u32 scolor, dcolor, wr_off;
> +
> + for (y = 0; y < height; y++) {
> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> + dst_offset = (y_pad + y) * dpitch;
> +
> + for (x = 0; x < width; x++) {
> + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
> + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
> + wr_off = dst_offset + (x_pad + x) * 3;
> +
> + iosys_map_wr(map, wr_off, u8, (dcolor & 0x000000FF) >> 0);
> + iosys_map_wr(map, wr_off + 1, u8, (dcolor & 0x0000FF00) >> 8);
> + iosys_map_wr(map, wr_off + 2, u8, (dcolor & 0x00FF0000) >> 16);
> + }
> + }
> +}
> +
> +static void drm_splash_blit_pix32(struct iosys_map *map, unsigned int dpitch,
> + unsigned int x_pad, unsigned int y_pad,
> + const u8 *sbuf8, unsigned int spitch,
> + unsigned int width, unsigned int height,
> + bool invert_y, u32 format)
> +{
> + unsigned int x, y, src_offset, dst_offset;
> + u32 scolor, dcolor, wr_off;
> +
> + for (y = 0; y < height; y++) {
> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> + dst_offset = (y_pad + y) * dpitch;
> +
> + for (x = 0; x < width; x++) {
> + scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
> + dcolor = drm_draw_color_from_xrgb8888(scolor, format);
> + wr_off = dst_offset + (x_pad + x) * sizeof(u32);
> +
> + iosys_map_wr(map, wr_off, u32, dcolor);
> + }
> + }
> +}
> +
> +static void drm_splash_blit_rgb888(struct iosys_map *map, unsigned int dpitch,
> + unsigned int x_pad, unsigned int y_pad,
> + const u8 *sbuf8, unsigned int spitch,
> + unsigned int width, unsigned int height,
> + bool invert_y)
> +{
> + unsigned int y, src_offset, dst_offset;
> +
> + for (y = 0; y < height; y++) {
> + src_offset = (invert_y ? (height - y - 1) : y) * spitch;
> + dst_offset = (y_pad + y) * dpitch + x_pad * 3;
> +
> + iosys_map_memcpy_to(map, dst_offset, &sbuf8[src_offset], width * 3);
> + }
> +}
> +
> +static int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
> + const u8 *data, size_t data_len)
> +
> +{
> + struct drm_client_buffer *buffer = scanout->buffer;
> + struct drm_client_dev *client = buffer->client;
> + struct drm_framebuffer *fb = buffer->fb;
> + u32 px_width = fb->format->cpp[0];
> + const struct bmp_file_header *file_header;
> + const struct bmp_dib_header *dib_header;
> + u32 bmp_width, bmp_height, bmp_pitch;
> + bool bmp_invert_y;
> + unsigned int x_pad, y_pad;
> + const u8 *image_data;
> + struct iosys_map map;
> + struct drm_rect r;
> + int ret;
> +
> + if (data_len < (sizeof(*file_header) + sizeof(*dib_header))) {
> + drm_err(client->dev, "splash: BMP file too short");
> + return -EINVAL;
> + }
> +
> + file_header = (const struct bmp_file_header *)data;
> + if (file_header->id != BMP_FILE_MAGIC_ID) {
> + drm_err(client->dev, "splash: invalid BMP magic 0x%04X",
> + file_header->id);
> + return -EINVAL;
> + }
> +
> + dib_header = (const struct bmp_dib_header *)(data + sizeof(*file_header));
> +
> + /* Restrict supported format to uncompressed, 24bit RGB888 */
> + if (dib_header->dib_header_size != 40 || dib_header->width < 0 ||
> + dib_header->planes != 1 || dib_header->compression != 0 ||
> + dib_header->bpp != 24) {
> + drm_err(client->dev, "splash: invalid BMP format");
> + return -EINVAL;
> + }
> +
> + bmp_width = dib_header->width;
> + bmp_height = abs(dib_header->height);
> + bmp_pitch = round_up(3 * bmp_width, 4);
> + bmp_invert_y = (dib_header->height > 0);
> +
> + if ((file_header->bitmap_offset + bmp_pitch * bmp_height) > data_len) {
> + drm_err(client->dev, "splash: invalid BMP size");
> + return -EINVAL;
> + }
> +
> + if (bmp_width > scanout->width || bmp_height > scanout->height) {
> + drm_err(client->dev, "splash: BMP image is too big for the screen");
> + return -EINVAL;
> + }
> +
> + image_data = data + file_header->bitmap_offset;
> +
> + ret = drm_client_buffer_vmap_local(buffer, &map);
> + if (ret) {
> + drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
> + return ret;
> + }
> +
> + /* Center X and Y */
> + x_pad = (scanout->width - bmp_width) / 2;
> + y_pad = (scanout->height - bmp_height) / 2;
> + r = DRM_RECT_INIT(x_pad, y_pad, bmp_width, bmp_height);
> +
> + /* In case the target format is RGB888, source data can be copied to
> + * the video buffer line by line, avoiding some overhead.
> + */
> + if (scanout->format == DRM_FORMAT_RGB888) {
> + drm_splash_blit_rgb888(&map, fb->pitches[0], x_pad, y_pad,
> + image_data, bmp_pitch, bmp_width,
> + bmp_height, bmp_invert_y);
> + } else {
> + switch (px_width) {
> + case 2:
> + drm_splash_blit_pix16(&map, fb->pitches[0], x_pad,
> + y_pad, image_data, bmp_pitch,
> + bmp_width, bmp_height,
> + bmp_invert_y, scanout->format);
> + break;
> + case 3:
> + drm_splash_blit_pix24(&map, fb->pitches[0], x_pad,
> + y_pad, image_data, bmp_pitch,
> + bmp_width, bmp_height,
> + bmp_invert_y, scanout->format);
> + break;
> + case 4:
> + drm_splash_blit_pix32(&map, fb->pitches[0], x_pad,
> + y_pad, image_data, bmp_pitch,
> + bmp_width, bmp_height,
> + bmp_invert_y, scanout->format);
> + break;
> + default:
> + drm_warn_once(client->dev,
> + "splash: can't blit with pixel width %d",
> + px_width);
> + }
> + }
> +
> + drm_client_buffer_vunmap_local(buffer);
> +
> + return drm_client_buffer_flush(buffer, &r);
> +}
> +#else
> +static inline int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
> + const u8 *data, size_t data_len)
> +{
> + return -EOPNOTSUPP;
> +}
> +#endif
> +
> +static int drm_splash_draw_scanout(struct drm_splash *splash,
> + struct drm_splash_scanout *scanout,
> + const u8 *data, size_t data_len)
> +{
> + if (!scanout->buffer)
> + return -ENODEV;
> +
> + if (!scanout->bg_drawn) {
> + u32 color = drm_draw_color_from_xrgb8888(splash_color,
> + scanout->format);
> + drm_splash_fill_solid_color(scanout->buffer, color);
> + scanout->bg_drawn = true;
> + }
> +
> + if (data != NULL) {
> + /* Ignore the return value, since the solid color has already
> + * been drawn to screen.
> + */
> + drm_splash_bmp_to_scanout(scanout, data, data_len);
> + }
> +
> + return 0;
> +}
> +
> +static int drm_splash_render_thread(void *data)
> +{
> + struct drm_splash *splash = data;
> + struct drm_client_dev *client = &splash->client;
> +
> + while (!kthread_should_stop()) {
> + unsigned int draw_count = 0;
> + drm_splash_data_get_func_t get_fn = NULL;
> + drm_splash_data_release_func_t release_fn = NULL;
> + void *priv = NULL;
> + const u8 *img_data = NULL;
> + size_t img_data_len = 0;
> + int i, ret;
> +
> + drm_splash_data_source_pop(splash, &get_fn, &release_fn, &priv);
> +
> + if (get_fn) {
> + ret = get_fn(priv, &img_data, &img_data_len);
> + if (ret) {
> + drm_err(client->dev,
> + "splash: failed to get image data: %d",
> + ret);
> + }
> + }
> +
> + for (i = 0; i < splash->n_scanout; i++) {
> + ret = drm_splash_draw_scanout(splash,
> + &splash->scanout[i],
> + img_data, img_data_len);
> + if (ret) {
> + drm_err(client->dev,
> + "splash: failed to fill scanout %d: %d",
> + i, ret);
> + continue;
> + }
> +
> + draw_count++;
> + }
> +
> + if (release_fn)
> + release_fn(priv);
> +
> + if (draw_count > 0) {
> + ret = drm_client_modeset_commit(client);
> + /* If commit returns EBUSY, another master showed up.
> + * This means that the splash is no more required.
> + */
> + if (ret == -EBUSY) {
> + drm_info(client->dev,
> + "splash: not master anymore, exiting");
> + break;
> + }
> + }
> +
> + /* If no changes arrived in the mean time, wait to be awaken,
> + * e.g.by a firmware callback.
> + */
> + if (atomic_xchg(&splash->pending, 0) == 0)
> + set_current_state(TASK_UNINTERRUPTIBLE);
> +
> + schedule();
> + }
> +
> + return 0;
> +}
> +
> +static inline void drm_splash_wake_render_thread(struct drm_splash *splash)
> +{
> + atomic_set(&splash->pending, 1);
> + wake_up_process(splash->thread);
> +}
> +
> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
> +static int drm_splash_fw_get(void *priv, const u8 **data, size_t *size)
> +{
> + const struct firmware *fw = priv;
> +
> + if (!fw)
> + return -ENODATA;
> +
> + *data = fw->data;
> + *size = fw->size;
> +
> + return 0;
> +}
> +
> +static void drm_splash_fw_release(void *priv)
> +{
> + const struct firmware *fw = priv;
> +
> + if (fw)
> + release_firmware(fw);
> +}
> +
> +static void drm_splash_fw_callback(const struct firmware *fw, void *context)
> +{
> + struct drm_splash *splash = context;
> + struct drm_client_dev *client = &splash->client;
> +
> + if (!fw || !fw->data) {
> + drm_err(client->dev, "splash: no firmware");
> + return;
> + }
> +
> + drm_splash_data_source_push(splash, drm_splash_fw_get,
> + drm_splash_fw_release, (void *)fw);
> +
> + /* Wake the render thread */
> + drm_dbg(client->dev, "splash: firmware loaded, wake up drawing thread");
> + drm_splash_wake_render_thread(splash);
> +}
> +
> +static int drm_splash_fw_request_bmp(struct drm_splash *splash)
> +{
> + struct drm_client_dev *client = &splash->client;
> +
> + drm_info(client->dev, "splash: request %s as firmware", splash_bmp);
> +
> + return request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
> + splash_bmp, client->dev->dev, GFP_KERNEL,
> + splash, drm_splash_fw_callback);
> +}
> +#else
> +static inline int drm_splash_fw_request_bmp(struct drm_splash *splash)
> +{
> + return -EOPNOTSUPP;
> +}
> +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
> +
> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT)
> +static int drm_splash_bgrt_get_data(void *priv, const u8 **data, size_t *size)
> +{
> + void *bgrt_image = priv;
> +
> + *data = bgrt_image;
> + *size = bgrt_image_size;
> +
> + return 0;
> +}
> +
> +static void drm_splash_bgrt_release(void *priv)
> +{
> + void *bgrt_image = priv;
> +
> + if (bgrt_image)
> + memunmap(bgrt_image);
> +}
> +
> +static int drm_splash_bgrt_load(struct drm_splash *splash)
> +{
> + struct drm_client_dev *client = &splash->client;
> + void *bgrt_image = NULL;
> +
> + drm_info(client->dev, "splash: using EFI BGRT");
> +
> + if (!bgrt_tab.image_address) {
> + drm_info(client->dev, "splash: no BGRT found");
> + return -ENOENT;
> + }
> +
> + if (bgrt_tab.status & 0x06) {
> + drm_info(client->dev, "splash: BGRT rotation bits set, skipping");
> + return -EOPNOTSUPP;
> + }
> +
> + drm_dbg(client->dev, "splash: BGRT image is at 0x%016llx, size=%zX",
> + bgrt_tab.image_address, bgrt_image_size);
> +
> + bgrt_image = memremap(bgrt_tab.image_address, bgrt_image_size,
> + MEMREMAP_WB);
> + if (!bgrt_image) {
> + drm_warn(client->dev, "splash: failed to map BGRT image memory");
> + return -ENOMEM;
> + }
> +
> + drm_splash_data_source_push(splash, drm_splash_bgrt_get_data,
> + drm_splash_bgrt_release, bgrt_image);
> +
> + drm_splash_wake_render_thread(splash);
> +
> + return 0;
> +}
> +#else
> +static inline int drm_splash_bgrt_load(struct drm_splash *splash)
> +{
> + return -EOPNOTSUPP;
> +}
> +#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT
> +
> +static int drm_splash_init_client(struct drm_splash *splash)
> +{
> + struct drm_client_dev *client = &splash->client;
> + struct drm_mode_set *modeset;
> + unsigned int modeset_mask = 0;
> + unsigned int fb_count = 0;
> + int j;
> +
> + if (drm_client_modeset_probe(client, 0, 0))
> + return -1;
> +
> + j = 0;
> + drm_client_for_each_modeset(modeset, client) {
> + struct drm_splash_scanout *tmp;
> + struct drm_splash_scanout *scanout;
> + u32 format;
> + int id = -1;
> +
> + /* Skip modesets without a mode */
> + if (!modeset->mode)
> + continue;
> +
> + if (modeset->connectors[0]->has_tile) {
> + struct drm_splash_scanout *tiled;
> + int new_id = modeset->connectors[0]->tile_group->id;
> +
> + /* Tiled modesets contribute to a single framebuffer,
> + * check if this tiled group has already been seen.
> + */
> + tiled = get_scanout_from_tile_group(splash, new_id);
> + if (tiled != NULL) {
> + if (!modeset->x)
> + tiled->width += modeset->mode->vdisplay;
> + if (!modeset->y)
> + tiled->height += modeset->mode->hdisplay;
> + modeset->fb = tiled->buffer->fb;
> + continue;
> + }
> +
> + /* New tile group, save its ID for later */
> + id = new_id;
> + }
> +
> + format = drm_splash_find_usable_format(modeset->crtc->primary,
> + splash->preferred_format);
> + if (format == DRM_FORMAT_INVALID) {
> + drm_warn(client->dev,
> + "splash: can't find a usable format for modeset");
> + continue;
> + }
> +
> + tmp = krealloc(splash->scanout,
> + (splash->n_scanout + 1) * sizeof(*splash->scanout),
> + GFP_KERNEL);
> + if (!tmp) {
> + drm_warn(client->dev,
> + "splash: can't reallocate the scanout array");
> + break;
> + }
> +
> + splash->scanout = tmp;
> + scanout = &splash->scanout[splash->n_scanout];
> + splash->n_scanout++;
> +
> + memset(scanout, 0, sizeof(*scanout));
> + scanout->id = id;
> + scanout->format = format;
> + scanout->width = modeset->mode->hdisplay;
> + scanout->height = modeset->mode->vdisplay;
> +
> + modeset_mask |= BIT(j);
> + j++;
> + }
> +
> + /* Now that all sensible modesets have been collected, allocate buffers */
> + j = 0;
> + drm_client_for_each_modeset(modeset, client) {
> + struct drm_splash_scanout *scanout;
> +
> + if (!(modeset_mask & BIT(j)))
> + continue;
> +
> + scanout = &splash->scanout[j];
> + j++;
> +
> + scanout->buffer = drm_client_buffer_create_dumb(client,
> + scanout->width,
> + scanout->height,
> + scanout->format);
> + if (IS_ERR(scanout->buffer)) {
> + drm_warn(client->dev,
> + "splash: can't create dumb buffer %d %d %p4cc",
> + scanout->width, scanout->height, &scanout->format);
> + continue;
> + }
> +
> + drm_info(client->dev, "splash: created dumb buffer %d %d %p4cc",
> + scanout->width, scanout->height, &scanout->format);
> +
> + modeset->fb = scanout->buffer->fb;
> + fb_count++;
> + }
> +
> + return (fb_count == 0) ? -ENODEV : 0;
> +}
> +
> +static void drm_splash_free_scanout(struct drm_client_dev *client)
> +{
> + struct drm_splash *splash = client_to_drm_splash(client);
> + int i;
> +
> + if (splash->n_scanout) {
> + for (i = 0; i < splash->n_scanout; i++)
> + drm_client_buffer_delete(splash->scanout[i].buffer);
> +
> + splash->n_scanout = 0;
> + kfree(splash->scanout);
> + splash->scanout = NULL;
> + }
> +}
> +
> +static int drm_splash_client_hotplug(struct drm_client_dev *client)
> +{
> + struct drm_splash *splash = client_to_drm_splash(client);
> + int ret;
> +
> + guard(mutex)(&splash->hotplug_lock);
> +
> + /* The modesets that get a splash are defined at first hotplug event */
> + if (splash->initialized)
> + return 0;
> +
> + ret = drm_splash_init_client(splash);
> + if (ret == -ENODEV) {
> + drm_info(client->dev, "splash: no modeset found");
> + return 0;
> + } else if (ret) {
> + drm_err(client->dev,
> + "splash: failed to init client: %d", ret);
> + return ret;
> + }
> +
> + /* Create the render thread, waken later */
> + splash->thread = kthread_create(drm_splash_render_thread,
> + splash, "drm_splash_%s",
> + client->dev->unique);
> + if (IS_ERR(splash->thread)) {
> + ret = PTR_ERR(splash->thread);
> + drm_err(client->dev, "splash: failed to create render thread: %d", ret);
> + drm_splash_free_scanout(client);
> + return ret;
> + }
> +
> + if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP))
> + ret = drm_splash_fw_request_bmp(splash);
> + else if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT))
> + ret = drm_splash_bgrt_load(splash);
I'm not sure this logic will actually work in this order.
CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT selects
CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
So won't the BGRT path never run?
> + else
> + ret = 0;
> +
> + if (ret) {
> + drm_err(client->dev, "splash: failed to kick image load: %d", ret);
> + kthread_stop(splash->thread);
> + drm_splash_free_scanout(client);
> + return ret;
> + }
> +
> + /* Wake the render thread to show initial contents */
> + drm_splash_wake_render_thread(splash);
> +
> + splash->initialized = true;
> +
> + return 0;
> +}
> +
> +static int drm_splash_client_restore(struct drm_client_dev *client, bool force)
> +{
> + int ret;
> +
> + if (force)
> + ret = drm_client_modeset_commit_locked(client);
> + else
> + ret = drm_client_modeset_commit(client);
> +
> + return ret;
> +}
> +
> +static void drm_splash_client_unregister(struct drm_client_dev *client)
> +{
> + struct drm_splash *splash = client_to_drm_splash(client);
> +
> + kthread_stop(splash->thread);
> + drm_splash_free_scanout(client);
> + drm_client_release(client);
> +
> + if (splash->data_release)
> + splash->data_release(splash->data_priv);
> +}
> +
> +static void drm_splash_client_free(struct drm_client_dev *client)
> +{
> + struct drm_splash *splash = client_to_drm_splash(client);
> + struct drm_device *dev = client->dev;
> +
> + mutex_destroy(&splash->hotplug_lock);
> + kfree(splash);
> +
> + drm_dbg(dev, "Unregistered with drm splash");
> +}
> +
> +static const struct drm_client_funcs drm_splash_client_funcs = {
> + .owner = THIS_MODULE,
> + .hotplug = drm_splash_client_hotplug,
> + .restore = drm_splash_client_restore,
> + .unregister = drm_splash_client_unregister,
> + .free = drm_splash_client_free,
> +};
> +
> +/**
> + * drm_splash_register() - Register a drm device to drm_splash
> + * @dev: the drm device to register.
> + * @format: drm device preferred format.
> + */
> +void drm_splash_register(struct drm_device *dev,
> + const struct drm_format_info *format)
> +{
> + struct drm_splash *splash;
> +
> + splash = kzalloc(sizeof(*splash), GFP_KERNEL);
> + if (!splash)
> + goto err_warn;
> +
> + mutex_init(&splash->hotplug_lock);
> + spin_lock_init(&splash->data_lock);
> +
> + if (format && format->num_planes == 1)
> + splash->preferred_format = format->format;
> + else
> + splash->preferred_format = DRM_FORMAT_RGB888;
> +
> + if (drm_client_init(dev, &splash->client, "drm_splash",
> + &drm_splash_client_funcs))
> + goto err_free;
> +
> + drm_client_register(&splash->client);
> + drm_dbg(dev, "Registered with drm splash");
> +
> + return;
> +
> +err_free:
> + kfree(splash);
> +err_warn:
> + drm_warn(dev, "Failed to register with drm splash");
> +}
>
^ permalink raw reply
* [PATCH RFC v2 0/3] Add splash DRM client
From: Francesco Valla @ 2026-01-06 14:25 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas
Cc: Sam Ravnborg, Mario Limonciello, linux-kernel, dri-devel,
linux-doc, linux-embedded, Francesco Valla
Hello,
after quite some time, this is the second RFC version for the DRM-based
splash screen. Motivation behind the work can be found in v1 [0].
Following the advice received on the v1, I stripped away all of the
user-driven functionalities (i.e.: configurable message and progress
bar), which can be provided by existing userspace tools, and decided to
use 24-bits bitmap as image format instead of raw dumps.
With the addition of EFI BGRT as a new source, this new DRM client is
now able to draw to screen one of the following:
- a colored background;
- a BMP image loaded as firmware (either built-in or loaded from the
filesystem);
- the BMP image supplied by the EFI BGRT.
Once compiled inside the kernel, the client can be enabled through the
command line specifying the drm_client_lib.active=splash parameter.
Two additional command-line parameters can be specified:
- drm_client_lib.splash_color=0xRRGGBB to modify the default
background color (which can in turn be set through a build-time
option);
- drm_client_lib.splash_bmp=<image.bmp> to set the BMP image loaded in
case this is the chosen source (with a fixed default of
drm_splash.bmp.
These two parameters were kept against some of the received feedback
because they fit a specific embedded usecase I want to cover (i.e.:
hardware model detected and set by the bootloader, along with a custom
splash for each model).
Additional notes:
- Rotation is still not managed.
- As for v1, support for tiled screens is untested.
- Plain color and BMP sources were tested both on QEMU and on a
Beagleplay.
- EFI BGRT support was tested using QEMU+OVMF.
Thank you in advance for any feedback you want to leave.
Best regards,
Francesco
[0] https://lore.kernel.org/all/20251027-drm_client_splash-v1-0-00698933b34a@valla.it
Signed-off-by: Francesco Valla <francesco@valla.it>
---
Changes in v2:
- Moved from raw dump to BMP format for static image source
- Removed support for configurable message
- Removed support for progress bar
- Added EFI BGRT as image source
Link to v1: https://lore.kernel.org/r/20251027-drm_client_splash-v1-0-00698933b34a@valla.it
---
Francesco Valla (3):
drm: client: add splash client
MAINTAINERS: add entry for DRM splash client
drm: docs: remove bootsplash from TODO
Documentation/gpu/todo.rst | 17 -
MAINTAINERS | 7 +
drivers/gpu/drm/clients/Kconfig | 79 ++-
drivers/gpu/drm/clients/Makefile | 1 +
drivers/gpu/drm/clients/drm_client_internal.h | 9 +
drivers/gpu/drm/clients/drm_client_setup.c | 8 +
drivers/gpu/drm/clients/drm_splash.c | 883 ++++++++++++++++++++++++++
7 files changed, 986 insertions(+), 18 deletions(-)
---
base-commit: 7f98ab9da046865d57c102fd3ca9669a29845f67
change-id: 20251026-drm_client_splash-e10d7d663e7f
Best regards,
--
Francesco Valla <francesco@valla.it>
^ permalink raw reply
* Re: [PATCH RFC v2 1/3] drm: client: add splash client
From: Geert Uytterhoeven @ 2026-01-06 14:47 UTC (permalink / raw)
To: Francesco Valla
Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas, Sam Ravnborg, Mario Limonciello,
linux-kernel, dri-devel, linux-doc, linux-embedded
In-Reply-To: <20260106-drm_client_splash-v2-1-6e86a7434b59@valla.it>
Hi Franceso,
On Tue, 6 Jan 2026 at 15:26, Francesco Valla <francesco@valla.it> wrote:
> Add a DRM client that draws a simple splash, with possibility to show:
>
> - a colored background;
> - a static BMP image (loaded as firmware);
> - the logo provided by EFI BGRT.
>
> The client is not meant to replace a full-featured bootsplash, but
> rather to remove some complexity (and hopefully boot time) on small
> embedded platforms or on systems with a limited scope (e.g: recovery
> or manufacturing images).
>
> The background color can be set either at build time from a dedicated
> config option or at runtime through the drm_client_lib.splash_color
> command line parameter. Any color in RGB888 format can be used.
>
> If enabled, the static BMP image is loaded using the kernel firmware
> infrastructure; a valid BMP image with 24bpp color and no compression
> is expected. The name of the image can be set through the
> drm_client_lib.splash_bmp kernel command line parameter, with the
> default being 'drm_splash.bmp'.
>
> Just like the existing DRM clients, the splash can be enabled from the
> kernel command line using drm_client_lib.active=splash.
>
> Signed-off-by: Francesco Valla <francesco@valla.it>
Thanks for your patch!
> --- /dev/null
> +++ b/drivers/gpu/drm/clients/drm_splash.c
> +#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
There is no need to protect this block with #if, as it does not generate
any code.
> +#define BMP_FILE_MAGIC_ID 0x4d42
> +
> +/* BMP header structures copied from drivers/video/fbdev/efifb.c */
> +struct bmp_file_header {
> + u16 id;
> + u32 file_size;
> + u32 reserved;
> + u32 bitmap_offset;
> +} __packed;
> +
> +struct bmp_dib_header {
> + u32 dib_header_size;
> + s32 width;
> + s32 height;
> + u16 planes;
> + u16 bpp;
> + u32 compression;
> + u32 bitmap_size;
> + u32 horz_resolution;
> + u32 vert_resolution;
> + u32 colors_used;
> + u32 colors_important;
> +} __packed;
> +#endif // CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT
As per [1], all these values are little-endian. Hence they should
be declared as such using le16 or le32, and accessed using
get_unalined_le{16,32}().
[1] https://en.wikipedia.org/wiki/BMP_file_format#File_structure
Gr{oetje,eeting}s,
Geert
--
Geert Uytterhoeven -- There's lots of Linux beyond ia32 -- geert@linux-m68k.org
In personal conversations with technical people, I call myself a hacker. But
when I'm talking to journalists I just say "programmer" or something like that.
-- Linus Torvalds
^ permalink raw reply
* [PATCH RFC v2 3/3] drm: docs: remove bootsplash from TODO
From: Francesco Valla @ 2026-01-06 14:25 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas
Cc: Sam Ravnborg, Mario Limonciello, linux-kernel, dri-devel,
linux-doc, linux-embedded, Francesco Valla
In-Reply-To: <20260106-drm_client_splash-v2-0-6e86a7434b59@valla.it>
Now that a splash client exists, remove the bootsplash task from the
TODO list for the DRM subsystem.
Signed-off-by: Francesco Valla <francesco@valla.it>
---
Documentation/gpu/todo.rst | 17 -----------------
1 file changed, 17 deletions(-)
diff --git a/Documentation/gpu/todo.rst b/Documentation/gpu/todo.rst
index 9013ced318cb97d6895752d4cfe21c94d0000973..e32b303cca26fa284f795e30dab18cf9e9496a9e 100644
--- a/Documentation/gpu/todo.rst
+++ b/Documentation/gpu/todo.rst
@@ -754,23 +754,6 @@ See drivers/gpu/drm/amd/display/TODO for tasks.
Contact: Harry Wentland, Alex Deucher
-Bootsplash
-==========
-
-There is support in place now for writing internal DRM clients making it
-possible to pick up the bootsplash work that was rejected because it was written
-for fbdev.
-
-- [v6,8/8] drm/client: Hack: Add bootsplash example
- https://patchwork.freedesktop.org/patch/306579/
-
-- [RFC PATCH v2 00/13] Kernel based bootsplash
- https://lore.kernel.org/r/20171213194755.3409-1-mstaudt@suse.de
-
-Contact: Sam Ravnborg
-
-Level: Advanced
-
Brightness handling on devices with multiple internal panels
============================================================
--
2.52.0
^ permalink raw reply related
* [PATCH RFC v2 2/3] MAINTAINERS: add entry for DRM splash client
From: Francesco Valla @ 2026-01-06 14:25 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas
Cc: Sam Ravnborg, Mario Limonciello, linux-kernel, dri-devel,
linux-doc, linux-embedded, Francesco Valla
In-Reply-To: <20260106-drm_client_splash-v2-0-6e86a7434b59@valla.it>
Add myself as maintainer for the DRM splash client.
Signed-off-by: Francesco Valla <francesco@valla.it>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index a0dd762f5648b7e4e6fc62560662e43720422e01..35032cd6fddf4828906ca30924c5322949551ddf 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8758,6 +8758,13 @@ T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
F: drivers/gpu/drm/drm_privacy_screen*
F: include/drm/drm_privacy_screen*
+DRM SPLASH
+M: Francesco Valla <francesco@valla.it>
+L: dri-devel@lists.freedesktop.org
+S: Maintained
+T: git https://gitlab.freedesktop.org/drm/misc/kernel.git
+F: drivers/gpu/drm/clients/drm_splash.c
+
DRM TTM SUBSYSTEM
M: Christian Koenig <christian.koenig@amd.com>
M: Huang Rui <ray.huang@amd.com>
--
2.52.0
^ permalink raw reply related
* [PATCH RFC v2 1/3] drm: client: add splash client
From: Francesco Valla @ 2026-01-06 14:25 UTC (permalink / raw)
To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter, Jonathan Corbet, Jocelyn Falempe,
Javier Martinez Canillas
Cc: Sam Ravnborg, Mario Limonciello, linux-kernel, dri-devel,
linux-doc, linux-embedded, Francesco Valla
In-Reply-To: <20260106-drm_client_splash-v2-0-6e86a7434b59@valla.it>
Add a DRM client that draws a simple splash, with possibility to show:
- a colored background;
- a static BMP image (loaded as firmware);
- the logo provided by EFI BGRT.
The client is not meant to replace a full-featured bootsplash, but
rather to remove some complexity (and hopefully boot time) on small
embedded platforms or on systems with a limited scope (e.g: recovery
or manufacturing images).
The background color can be set either at build time from a dedicated
config option or at runtime through the drm_client_lib.splash_color
command line parameter. Any color in RGB888 format can be used.
If enabled, the static BMP image is loaded using the kernel firmware
infrastructure; a valid BMP image with 24bpp color and no compression
is expected. The name of the image can be set through the
drm_client_lib.splash_bmp kernel command line parameter, with the
default being 'drm_splash.bmp'.
Just like the existing DRM clients, the splash can be enabled from the
kernel command line using drm_client_lib.active=splash.
Signed-off-by: Francesco Valla <francesco@valla.it>
---
drivers/gpu/drm/clients/Kconfig | 79 ++-
drivers/gpu/drm/clients/Makefile | 1 +
drivers/gpu/drm/clients/drm_client_internal.h | 9 +
drivers/gpu/drm/clients/drm_client_setup.c | 8 +
drivers/gpu/drm/clients/drm_splash.c | 883 ++++++++++++++++++++++++++
5 files changed, 979 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
index 6096c623d9d5b1a3d4a40d986c45aad2f8277767..dd8cd6cacd1166932eb3890dd816b9ae2d26330f 100644
--- a/drivers/gpu/drm/clients/Kconfig
+++ b/drivers/gpu/drm/clients/Kconfig
@@ -12,6 +12,7 @@ config DRM_CLIENT_LIB
config DRM_CLIENT_SELECTION
tristate
depends on DRM
+ select DRM_CLIENT_LIB if DRM_CLIENT_SPLASH
select DRM_CLIENT_LIB if DRM_CLIENT_LOG
select DRM_CLIENT_LIB if DRM_FBDEV_EMULATION
help
@@ -85,10 +86,79 @@ config DRM_CLIENT_LOG
If you only need logs, but no terminal, or if you prefer userspace
terminal, say "Y".
+config DRM_CLIENT_SPLASH
+ bool "Display graphic splash"
+ depends on DRM_CLIENT_SELECTION
+ select DRM_CLIENT
+ select DRM_CLIENT_SETUP
+ select DRM_DRAW
+ help
+ This enables a splash drm client, able to display either a plain
+ color or a static image until the userspace is ready to take over.
+ The splash will be displayed on all screens available at boot, if
+ any, or on the ones part of the first hotplug event.
+
+config DRM_CLIENT_SPLASH_BACKGROUND_COLOR
+ hex "Splash background color"
+ depends on DRM_CLIENT_SPLASH
+ default 0x000000
+ help
+ The default splash background color, in RGB888 format.
+
+ The color can be overridden through the drm_client_lib.splash_color
+ kernel command line parameter.
+
+config DRM_CLIENT_SPLASH_BMP_SUPPORT
+ bool
+
+choice
+ prompt "Splash source"
+ depends on DRM_CLIENT_SPLASH
+ default DRM_CLIENT_SPLASH_SRC_COLOR
+ help
+ Selects the source for the splash graphic.
+
+config DRM_CLIENT_SPLASH_SRC_COLOR
+ bool "Solid color"
+ help
+ Use a solid color as splash. The color is selected through the
+ DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option.
+
+ The image will be loaded using the firmware loading facility the
+ kernel provides.
+
+config DRM_CLIENT_SPLASH_SRC_BMP
+ bool "BMP image"
+ select DRM_CLIENT_SPLASH_BMP_SUPPORT
+ select FW_LOADER
+ help
+ Use a BMP (bitmap) image as splash. If the image is smaller than the
+ display(s), it will be centered and the color specified through the
+ DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option will be used as
+ background.
+
+ The image will be loaded using the firmware loading facility the
+ kernel provides; it shall use 24 bits per pixel and shall not be
+ compressed. The name of the file can be set through the
+ drm_client_lib.splash_bmp command line parameter, with the default
+ being 'drm_splash.bmp'.
+
+config DRM_CLIENT_SPLASH_SRC_BGRT
+ bool "EFI BGRT"
+ select DRM_CLIENT_SPLASH_BMP_SUPPORT
+ depends on EFI
+ help
+ Use the BGRT image provided by the EFI bootloader. If the image is
+ smaller than the display(s), it will be centered and the color
+ specified through the DRM_CLIENT_SPLASH_BACKGROUND_COLOR config
+ option will be used as background.
+
+endchoice
+
choice
prompt "Default DRM Client"
depends on DRM_CLIENT_SELECTION
- depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG
+ depends on DRM_FBDEV_EMULATION || DRM_CLIENT_LOG || DRM_CLIENT_SPLASH
default DRM_CLIENT_DEFAULT_FBDEV
help
Selects the default drm client.
@@ -111,6 +181,12 @@ config DRM_CLIENT_DEFAULT_LOG
screen, but doesn't implement a full terminal. For that you will need
a userspace terminal using drm/kms.
+config DRM_CLIENT_DEFAULT_SPLASH
+ bool "splash"
+ depends on DRM_CLIENT_SPLASH
+ help
+ Use splash as default drm client.
+
endchoice
config DRM_CLIENT_DEFAULT
@@ -118,6 +194,7 @@ config DRM_CLIENT_DEFAULT
depends on DRM_CLIENT
default "fbdev" if DRM_CLIENT_DEFAULT_FBDEV
default "log" if DRM_CLIENT_DEFAULT_LOG
+ default "splash" if DRM_CLIENT_DEFAULT_SPLASH
default ""
endmenu
diff --git a/drivers/gpu/drm/clients/Makefile b/drivers/gpu/drm/clients/Makefile
index c16addbc327f09572aa3142cbf0d1d13f172a9e9..3df02d10cd18a47d7e8d7cee70163b0ef0129b51 100644
--- a/drivers/gpu/drm/clients/Makefile
+++ b/drivers/gpu/drm/clients/Makefile
@@ -5,4 +5,5 @@ subdir-ccflags-y += -I$(src)/..
drm_client_lib-y := drm_client_setup.o
drm_client_lib-$(CONFIG_DRM_CLIENT_LOG) += drm_log.o
drm_client_lib-$(CONFIG_DRM_FBDEV_EMULATION) += drm_fbdev_client.o
+drm_client_lib-$(CONFIG_DRM_CLIENT_SPLASH) += drm_splash.o
obj-$(CONFIG_DRM_CLIENT_LIB) += drm_client_lib.o
diff --git a/drivers/gpu/drm/clients/drm_client_internal.h b/drivers/gpu/drm/clients/drm_client_internal.h
index 6dc078bf6503b902cbb3267b64ea42d9f1c23375..48ee0c1c2529882b2bf5dc786788390823e25cd6 100644
--- a/drivers/gpu/drm/clients/drm_client_internal.h
+++ b/drivers/gpu/drm/clients/drm_client_internal.h
@@ -22,4 +22,13 @@ void drm_log_register(struct drm_device *dev);
static inline void drm_log_register(struct drm_device *dev) {}
#endif
+#ifdef CONFIG_DRM_CLIENT_SPLASH
+void drm_splash_register(struct drm_device *dev,
+ const struct drm_format_info *format);
+#else
+static inline void drm_splash_register(struct drm_device *dev,
+ const struct drm_format_info *format)
+{}
+#endif
+
#endif
diff --git a/drivers/gpu/drm/clients/drm_client_setup.c b/drivers/gpu/drm/clients/drm_client_setup.c
index 515aceac22b18161a14bd60be4acedc1ddd05bc5..c19498938ee3ba442f7502b27a9c33dfff45a203 100644
--- a/drivers/gpu/drm/clients/drm_client_setup.c
+++ b/drivers/gpu/drm/clients/drm_client_setup.c
@@ -56,6 +56,14 @@ void drm_client_setup(struct drm_device *dev, const struct drm_format_info *form
return;
}
#endif
+
+#ifdef CONFIG_DRM_CLIENT_SPLASH
+ if (!strcmp(drm_client_default, "splash")) {
+ drm_splash_register(dev, format);
+ return;
+ }
+#endif
+
if (strcmp(drm_client_default, ""))
drm_warn(dev, "Unknown DRM client %s\n", drm_client_default);
}
diff --git a/drivers/gpu/drm/clients/drm_splash.c b/drivers/gpu/drm/clients/drm_splash.c
new file mode 100644
index 0000000000000000000000000000000000000000..204a5256ef03b6edb81e5dc8c49b5929cb51ff92
--- /dev/null
+++ b/drivers/gpu/drm/clients/drm_splash.c
@@ -0,0 +1,883 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+/*
+ * Copyright (c) 2025-2026 Francesco Valla <francesco@valla.it>
+ *
+ */
+
+#include <linux/atomic.h>
+#include <linux/device.h>
+#include <linux/efi-bgrt.h>
+#include <linux/firmware.h>
+#include <linux/init.h>
+#include <linux/iosys-map.h>
+#include <linux/kthread.h>
+#include <linux/module.h>
+#include <linux/types.h>
+
+#include <drm/drm_client.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_fourcc.h>
+#include <drm/drm_framebuffer.h>
+#include <drm/drm_plane.h>
+#include <drm/drm_print.h>
+
+#include "drm_client_internal.h"
+#include "drm_draw_internal.h"
+#include "drm_internal.h"
+
+/**
+ * DOC: overview
+ *
+ * This is a simple graphic bootsplash, able to display either a plain color or
+ * a static image.
+ */
+
+static unsigned int splash_color = CONFIG_DRM_CLIENT_SPLASH_BACKGROUND_COLOR;
+module_param(splash_color, uint, 0400);
+MODULE_PARM_DESC(splash_color, "Splash background color (RGB888)");
+
+#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
+#define DEFAULT_SPLASH_BMP "drm_splash.bmp"
+static char *splash_bmp = DEFAULT_SPLASH_BMP;
+module_param(splash_bmp, charp, 0400);
+MODULE_PARM_DESC(splash_bmp, "Name of splash image (default: \"" DEFAULT_SPLASH_BMP "\")");
+#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
+
+#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
+#define BMP_FILE_MAGIC_ID 0x4d42
+
+/* BMP header structures copied from drivers/video/fbdev/efifb.c */
+struct bmp_file_header {
+ u16 id;
+ u32 file_size;
+ u32 reserved;
+ u32 bitmap_offset;
+} __packed;
+
+struct bmp_dib_header {
+ u32 dib_header_size;
+ s32 width;
+ s32 height;
+ u16 planes;
+ u16 bpp;
+ u32 compression;
+ u32 bitmap_size;
+ u32 horz_resolution;
+ u32 vert_resolution;
+ u32 colors_used;
+ u32 colors_important;
+} __packed;
+#endif // CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT
+
+typedef int (*drm_splash_data_get_func_t)(void *priv, const u8 **data, size_t *size);
+typedef void (*drm_splash_data_release_func_t)(void *priv);
+
+struct drm_splash_scanout {
+ int id;
+ u32 format;
+ unsigned int width;
+ unsigned int height;
+ struct drm_client_buffer *buffer;
+ bool bg_drawn;
+};
+
+struct drm_splash {
+ struct drm_client_dev client;
+ u32 preferred_format;
+ struct device dev;
+
+ struct task_struct *thread;
+ atomic_t pending;
+
+ struct mutex hotplug_lock;
+ bool initialized;
+
+ u32 n_scanout;
+ struct drm_splash_scanout *scanout;
+
+ spinlock_t data_lock;
+ drm_splash_data_get_func_t data_get;
+ drm_splash_data_release_func_t data_release;
+ void *data_priv;
+};
+
+static struct drm_splash *client_to_drm_splash(struct drm_client_dev *client)
+{
+ return container_of_const(client, struct drm_splash, client);
+}
+
+static void __maybe_unused
+drm_splash_data_source_push(struct drm_splash *splash,
+ drm_splash_data_get_func_t get,
+ drm_splash_data_release_func_t release,
+ void *priv)
+{
+ guard(spinlock)(&splash->data_lock);
+
+ /* Release previous data */
+ if (splash->data_release)
+ splash->data_release(splash->data_priv);
+
+ splash->data_get = get;
+ splash->data_release = release;
+ splash->data_priv = priv;
+}
+
+static void drm_splash_data_source_pop(struct drm_splash *splash,
+ drm_splash_data_get_func_t *get,
+ drm_splash_data_release_func_t *release,
+ void **priv)
+{
+ guard(spinlock)(&splash->data_lock);
+
+ *get = splash->data_get;
+ splash->data_get = NULL;
+
+ *release = splash->data_release;
+ splash->data_release = NULL;
+
+ *priv = splash->data_priv;
+ splash->data_priv = NULL;
+}
+
+static struct drm_splash_scanout *
+get_scanout_from_tile_group(struct drm_splash *splash, int id)
+{
+ int j;
+
+ for (j = 0; j < splash->n_scanout; j++)
+ if (splash->scanout[j].id == id)
+ return &splash->scanout[j];
+
+ return NULL;
+}
+
+static u32 drm_splash_find_usable_format(struct drm_plane *plane,
+ u32 preferred_format)
+{
+ int i;
+
+ /* Check if the preferred format can be used */
+ for (i = 0; i < plane->format_count; i++)
+ if (plane->format_types[i] == preferred_format)
+ return preferred_format;
+
+ /* Otherwise, find the first format that can be converted from XRGB8888 */
+ for (i = 0; i < plane->format_count; i++)
+ if (drm_draw_color_from_xrgb8888(0xffffffff, plane->format_types[i]) != 0)
+ return plane->format_types[i];
+
+ return DRM_FORMAT_INVALID;
+}
+
+static void drm_splash_fill(struct iosys_map *map, unsigned int dst_pitch,
+ unsigned int height, unsigned int width,
+ u32 px_width, u32 color)
+{
+ switch (px_width) {
+ case 2:
+ drm_draw_fill16(map, dst_pitch, height, width, color);
+ break;
+ case 3:
+ drm_draw_fill24(map, dst_pitch, height, width, color);
+ break;
+ case 4:
+ drm_draw_fill32(map, dst_pitch, height, width, color);
+ break;
+ default:
+ WARN_ONCE(1, "Can't fill with pixel width %d", px_width);
+ }
+}
+
+static int drm_splash_fill_solid_color(struct drm_client_buffer *buffer, u32 color)
+{
+ struct drm_client_dev *client = buffer->client;
+ struct drm_framebuffer *fb = buffer->fb;
+ struct drm_rect r = DRM_RECT_INIT(0, 0, fb->width, fb->height);
+ u32 px_width = fb->format->cpp[0];
+ struct iosys_map map;
+ int ret;
+
+ ret = drm_client_buffer_vmap_local(buffer, &map);
+ if (ret) {
+ drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
+ return ret;
+ }
+
+ drm_splash_fill(&map, fb->pitches[0], drm_rect_height(&r),
+ drm_rect_width(&r), px_width, color);
+
+ drm_client_buffer_vunmap_local(buffer);
+
+ return drm_client_buffer_flush(buffer, &r);
+}
+
+#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
+static void drm_splash_blit_pix16(struct iosys_map *map, unsigned int dpitch,
+ unsigned int x_pad, unsigned int y_pad,
+ const u8 *sbuf8, unsigned int spitch,
+ unsigned int width, unsigned int height,
+ bool invert_y, u32 format)
+{
+ unsigned int x, y, src_offset, dst_offset;
+ u32 scolor, dcolor, wr_off;
+
+ for (y = 0; y < height; y++) {
+ src_offset = (invert_y ? (height - y - 1) : y) * spitch;
+ dst_offset = (y_pad + y) * dpitch;
+
+ for (x = 0; x < width; x++) {
+ scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
+ dcolor = drm_draw_color_from_xrgb8888(scolor, format);
+ wr_off = dst_offset + (x_pad + x) * sizeof(u16);
+
+ iosys_map_wr(map, wr_off, u16, dcolor);
+ }
+ }
+}
+
+static void drm_splash_blit_pix24(struct iosys_map *map, unsigned int dpitch,
+ unsigned int x_pad, unsigned int y_pad,
+ const u8 *sbuf8, unsigned int spitch,
+ unsigned int width, unsigned int height,
+ bool invert_y, u32 format)
+{
+ unsigned int x, y, src_offset, dst_offset;
+ u32 scolor, dcolor, wr_off;
+
+ for (y = 0; y < height; y++) {
+ src_offset = (invert_y ? (height - y - 1) : y) * spitch;
+ dst_offset = (y_pad + y) * dpitch;
+
+ for (x = 0; x < width; x++) {
+ scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
+ dcolor = drm_draw_color_from_xrgb8888(scolor, format);
+ wr_off = dst_offset + (x_pad + x) * 3;
+
+ iosys_map_wr(map, wr_off, u8, (dcolor & 0x000000FF) >> 0);
+ iosys_map_wr(map, wr_off + 1, u8, (dcolor & 0x0000FF00) >> 8);
+ iosys_map_wr(map, wr_off + 2, u8, (dcolor & 0x00FF0000) >> 16);
+ }
+ }
+}
+
+static void drm_splash_blit_pix32(struct iosys_map *map, unsigned int dpitch,
+ unsigned int x_pad, unsigned int y_pad,
+ const u8 *sbuf8, unsigned int spitch,
+ unsigned int width, unsigned int height,
+ bool invert_y, u32 format)
+{
+ unsigned int x, y, src_offset, dst_offset;
+ u32 scolor, dcolor, wr_off;
+
+ for (y = 0; y < height; y++) {
+ src_offset = (invert_y ? (height - y - 1) : y) * spitch;
+ dst_offset = (y_pad + y) * dpitch;
+
+ for (x = 0; x < width; x++) {
+ scolor = *(const u32 *)(&sbuf8[src_offset + 3 * x]);
+ dcolor = drm_draw_color_from_xrgb8888(scolor, format);
+ wr_off = dst_offset + (x_pad + x) * sizeof(u32);
+
+ iosys_map_wr(map, wr_off, u32, dcolor);
+ }
+ }
+}
+
+static void drm_splash_blit_rgb888(struct iosys_map *map, unsigned int dpitch,
+ unsigned int x_pad, unsigned int y_pad,
+ const u8 *sbuf8, unsigned int spitch,
+ unsigned int width, unsigned int height,
+ bool invert_y)
+{
+ unsigned int y, src_offset, dst_offset;
+
+ for (y = 0; y < height; y++) {
+ src_offset = (invert_y ? (height - y - 1) : y) * spitch;
+ dst_offset = (y_pad + y) * dpitch + x_pad * 3;
+
+ iosys_map_memcpy_to(map, dst_offset, &sbuf8[src_offset], width * 3);
+ }
+}
+
+static int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
+ const u8 *data, size_t data_len)
+
+{
+ struct drm_client_buffer *buffer = scanout->buffer;
+ struct drm_client_dev *client = buffer->client;
+ struct drm_framebuffer *fb = buffer->fb;
+ u32 px_width = fb->format->cpp[0];
+ const struct bmp_file_header *file_header;
+ const struct bmp_dib_header *dib_header;
+ u32 bmp_width, bmp_height, bmp_pitch;
+ bool bmp_invert_y;
+ unsigned int x_pad, y_pad;
+ const u8 *image_data;
+ struct iosys_map map;
+ struct drm_rect r;
+ int ret;
+
+ if (data_len < (sizeof(*file_header) + sizeof(*dib_header))) {
+ drm_err(client->dev, "splash: BMP file too short");
+ return -EINVAL;
+ }
+
+ file_header = (const struct bmp_file_header *)data;
+ if (file_header->id != BMP_FILE_MAGIC_ID) {
+ drm_err(client->dev, "splash: invalid BMP magic 0x%04X",
+ file_header->id);
+ return -EINVAL;
+ }
+
+ dib_header = (const struct bmp_dib_header *)(data + sizeof(*file_header));
+
+ /* Restrict supported format to uncompressed, 24bit RGB888 */
+ if (dib_header->dib_header_size != 40 || dib_header->width < 0 ||
+ dib_header->planes != 1 || dib_header->compression != 0 ||
+ dib_header->bpp != 24) {
+ drm_err(client->dev, "splash: invalid BMP format");
+ return -EINVAL;
+ }
+
+ bmp_width = dib_header->width;
+ bmp_height = abs(dib_header->height);
+ bmp_pitch = round_up(3 * bmp_width, 4);
+ bmp_invert_y = (dib_header->height > 0);
+
+ if ((file_header->bitmap_offset + bmp_pitch * bmp_height) > data_len) {
+ drm_err(client->dev, "splash: invalid BMP size");
+ return -EINVAL;
+ }
+
+ if (bmp_width > scanout->width || bmp_height > scanout->height) {
+ drm_err(client->dev, "splash: BMP image is too big for the screen");
+ return -EINVAL;
+ }
+
+ image_data = data + file_header->bitmap_offset;
+
+ ret = drm_client_buffer_vmap_local(buffer, &map);
+ if (ret) {
+ drm_err(client->dev, "splash: cannot vmap buffer: %d", ret);
+ return ret;
+ }
+
+ /* Center X and Y */
+ x_pad = (scanout->width - bmp_width) / 2;
+ y_pad = (scanout->height - bmp_height) / 2;
+ r = DRM_RECT_INIT(x_pad, y_pad, bmp_width, bmp_height);
+
+ /* In case the target format is RGB888, source data can be copied to
+ * the video buffer line by line, avoiding some overhead.
+ */
+ if (scanout->format == DRM_FORMAT_RGB888) {
+ drm_splash_blit_rgb888(&map, fb->pitches[0], x_pad, y_pad,
+ image_data, bmp_pitch, bmp_width,
+ bmp_height, bmp_invert_y);
+ } else {
+ switch (px_width) {
+ case 2:
+ drm_splash_blit_pix16(&map, fb->pitches[0], x_pad,
+ y_pad, image_data, bmp_pitch,
+ bmp_width, bmp_height,
+ bmp_invert_y, scanout->format);
+ break;
+ case 3:
+ drm_splash_blit_pix24(&map, fb->pitches[0], x_pad,
+ y_pad, image_data, bmp_pitch,
+ bmp_width, bmp_height,
+ bmp_invert_y, scanout->format);
+ break;
+ case 4:
+ drm_splash_blit_pix32(&map, fb->pitches[0], x_pad,
+ y_pad, image_data, bmp_pitch,
+ bmp_width, bmp_height,
+ bmp_invert_y, scanout->format);
+ break;
+ default:
+ drm_warn_once(client->dev,
+ "splash: can't blit with pixel width %d",
+ px_width);
+ }
+ }
+
+ drm_client_buffer_vunmap_local(buffer);
+
+ return drm_client_buffer_flush(buffer, &r);
+}
+#else
+static inline int drm_splash_bmp_to_scanout(struct drm_splash_scanout *scanout,
+ const u8 *data, size_t data_len)
+{
+ return -EOPNOTSUPP;
+}
+#endif
+
+static int drm_splash_draw_scanout(struct drm_splash *splash,
+ struct drm_splash_scanout *scanout,
+ const u8 *data, size_t data_len)
+{
+ if (!scanout->buffer)
+ return -ENODEV;
+
+ if (!scanout->bg_drawn) {
+ u32 color = drm_draw_color_from_xrgb8888(splash_color,
+ scanout->format);
+ drm_splash_fill_solid_color(scanout->buffer, color);
+ scanout->bg_drawn = true;
+ }
+
+ if (data != NULL) {
+ /* Ignore the return value, since the solid color has already
+ * been drawn to screen.
+ */
+ drm_splash_bmp_to_scanout(scanout, data, data_len);
+ }
+
+ return 0;
+}
+
+static int drm_splash_render_thread(void *data)
+{
+ struct drm_splash *splash = data;
+ struct drm_client_dev *client = &splash->client;
+
+ while (!kthread_should_stop()) {
+ unsigned int draw_count = 0;
+ drm_splash_data_get_func_t get_fn = NULL;
+ drm_splash_data_release_func_t release_fn = NULL;
+ void *priv = NULL;
+ const u8 *img_data = NULL;
+ size_t img_data_len = 0;
+ int i, ret;
+
+ drm_splash_data_source_pop(splash, &get_fn, &release_fn, &priv);
+
+ if (get_fn) {
+ ret = get_fn(priv, &img_data, &img_data_len);
+ if (ret) {
+ drm_err(client->dev,
+ "splash: failed to get image data: %d",
+ ret);
+ }
+ }
+
+ for (i = 0; i < splash->n_scanout; i++) {
+ ret = drm_splash_draw_scanout(splash,
+ &splash->scanout[i],
+ img_data, img_data_len);
+ if (ret) {
+ drm_err(client->dev,
+ "splash: failed to fill scanout %d: %d",
+ i, ret);
+ continue;
+ }
+
+ draw_count++;
+ }
+
+ if (release_fn)
+ release_fn(priv);
+
+ if (draw_count > 0) {
+ ret = drm_client_modeset_commit(client);
+ /* If commit returns EBUSY, another master showed up.
+ * This means that the splash is no more required.
+ */
+ if (ret == -EBUSY) {
+ drm_info(client->dev,
+ "splash: not master anymore, exiting");
+ break;
+ }
+ }
+
+ /* If no changes arrived in the mean time, wait to be awaken,
+ * e.g.by a firmware callback.
+ */
+ if (atomic_xchg(&splash->pending, 0) == 0)
+ set_current_state(TASK_UNINTERRUPTIBLE);
+
+ schedule();
+ }
+
+ return 0;
+}
+
+static inline void drm_splash_wake_render_thread(struct drm_splash *splash)
+{
+ atomic_set(&splash->pending, 1);
+ wake_up_process(splash->thread);
+}
+
+#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP)
+static int drm_splash_fw_get(void *priv, const u8 **data, size_t *size)
+{
+ const struct firmware *fw = priv;
+
+ if (!fw)
+ return -ENODATA;
+
+ *data = fw->data;
+ *size = fw->size;
+
+ return 0;
+}
+
+static void drm_splash_fw_release(void *priv)
+{
+ const struct firmware *fw = priv;
+
+ if (fw)
+ release_firmware(fw);
+}
+
+static void drm_splash_fw_callback(const struct firmware *fw, void *context)
+{
+ struct drm_splash *splash = context;
+ struct drm_client_dev *client = &splash->client;
+
+ if (!fw || !fw->data) {
+ drm_err(client->dev, "splash: no firmware");
+ return;
+ }
+
+ drm_splash_data_source_push(splash, drm_splash_fw_get,
+ drm_splash_fw_release, (void *)fw);
+
+ /* Wake the render thread */
+ drm_dbg(client->dev, "splash: firmware loaded, wake up drawing thread");
+ drm_splash_wake_render_thread(splash);
+}
+
+static int drm_splash_fw_request_bmp(struct drm_splash *splash)
+{
+ struct drm_client_dev *client = &splash->client;
+
+ drm_info(client->dev, "splash: request %s as firmware", splash_bmp);
+
+ return request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
+ splash_bmp, client->dev->dev, GFP_KERNEL,
+ splash, drm_splash_fw_callback);
+}
+#else
+static inline int drm_splash_fw_request_bmp(struct drm_splash *splash)
+{
+ return -EOPNOTSUPP;
+}
+#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BMP
+
+#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT)
+static int drm_splash_bgrt_get_data(void *priv, const u8 **data, size_t *size)
+{
+ void *bgrt_image = priv;
+
+ *data = bgrt_image;
+ *size = bgrt_image_size;
+
+ return 0;
+}
+
+static void drm_splash_bgrt_release(void *priv)
+{
+ void *bgrt_image = priv;
+
+ if (bgrt_image)
+ memunmap(bgrt_image);
+}
+
+static int drm_splash_bgrt_load(struct drm_splash *splash)
+{
+ struct drm_client_dev *client = &splash->client;
+ void *bgrt_image = NULL;
+
+ drm_info(client->dev, "splash: using EFI BGRT");
+
+ if (!bgrt_tab.image_address) {
+ drm_info(client->dev, "splash: no BGRT found");
+ return -ENOENT;
+ }
+
+ if (bgrt_tab.status & 0x06) {
+ drm_info(client->dev, "splash: BGRT rotation bits set, skipping");
+ return -EOPNOTSUPP;
+ }
+
+ drm_dbg(client->dev, "splash: BGRT image is at 0x%016llx, size=%zX",
+ bgrt_tab.image_address, bgrt_image_size);
+
+ bgrt_image = memremap(bgrt_tab.image_address, bgrt_image_size,
+ MEMREMAP_WB);
+ if (!bgrt_image) {
+ drm_warn(client->dev, "splash: failed to map BGRT image memory");
+ return -ENOMEM;
+ }
+
+ drm_splash_data_source_push(splash, drm_splash_bgrt_get_data,
+ drm_splash_bgrt_release, bgrt_image);
+
+ drm_splash_wake_render_thread(splash);
+
+ return 0;
+}
+#else
+static inline int drm_splash_bgrt_load(struct drm_splash *splash)
+{
+ return -EOPNOTSUPP;
+}
+#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT
+
+static int drm_splash_init_client(struct drm_splash *splash)
+{
+ struct drm_client_dev *client = &splash->client;
+ struct drm_mode_set *modeset;
+ unsigned int modeset_mask = 0;
+ unsigned int fb_count = 0;
+ int j;
+
+ if (drm_client_modeset_probe(client, 0, 0))
+ return -1;
+
+ j = 0;
+ drm_client_for_each_modeset(modeset, client) {
+ struct drm_splash_scanout *tmp;
+ struct drm_splash_scanout *scanout;
+ u32 format;
+ int id = -1;
+
+ /* Skip modesets without a mode */
+ if (!modeset->mode)
+ continue;
+
+ if (modeset->connectors[0]->has_tile) {
+ struct drm_splash_scanout *tiled;
+ int new_id = modeset->connectors[0]->tile_group->id;
+
+ /* Tiled modesets contribute to a single framebuffer,
+ * check if this tiled group has already been seen.
+ */
+ tiled = get_scanout_from_tile_group(splash, new_id);
+ if (tiled != NULL) {
+ if (!modeset->x)
+ tiled->width += modeset->mode->vdisplay;
+ if (!modeset->y)
+ tiled->height += modeset->mode->hdisplay;
+ modeset->fb = tiled->buffer->fb;
+ continue;
+ }
+
+ /* New tile group, save its ID for later */
+ id = new_id;
+ }
+
+ format = drm_splash_find_usable_format(modeset->crtc->primary,
+ splash->preferred_format);
+ if (format == DRM_FORMAT_INVALID) {
+ drm_warn(client->dev,
+ "splash: can't find a usable format for modeset");
+ continue;
+ }
+
+ tmp = krealloc(splash->scanout,
+ (splash->n_scanout + 1) * sizeof(*splash->scanout),
+ GFP_KERNEL);
+ if (!tmp) {
+ drm_warn(client->dev,
+ "splash: can't reallocate the scanout array");
+ break;
+ }
+
+ splash->scanout = tmp;
+ scanout = &splash->scanout[splash->n_scanout];
+ splash->n_scanout++;
+
+ memset(scanout, 0, sizeof(*scanout));
+ scanout->id = id;
+ scanout->format = format;
+ scanout->width = modeset->mode->hdisplay;
+ scanout->height = modeset->mode->vdisplay;
+
+ modeset_mask |= BIT(j);
+ j++;
+ }
+
+ /* Now that all sensible modesets have been collected, allocate buffers */
+ j = 0;
+ drm_client_for_each_modeset(modeset, client) {
+ struct drm_splash_scanout *scanout;
+
+ if (!(modeset_mask & BIT(j)))
+ continue;
+
+ scanout = &splash->scanout[j];
+ j++;
+
+ scanout->buffer = drm_client_buffer_create_dumb(client,
+ scanout->width,
+ scanout->height,
+ scanout->format);
+ if (IS_ERR(scanout->buffer)) {
+ drm_warn(client->dev,
+ "splash: can't create dumb buffer %d %d %p4cc",
+ scanout->width, scanout->height, &scanout->format);
+ continue;
+ }
+
+ drm_info(client->dev, "splash: created dumb buffer %d %d %p4cc",
+ scanout->width, scanout->height, &scanout->format);
+
+ modeset->fb = scanout->buffer->fb;
+ fb_count++;
+ }
+
+ return (fb_count == 0) ? -ENODEV : 0;
+}
+
+static void drm_splash_free_scanout(struct drm_client_dev *client)
+{
+ struct drm_splash *splash = client_to_drm_splash(client);
+ int i;
+
+ if (splash->n_scanout) {
+ for (i = 0; i < splash->n_scanout; i++)
+ drm_client_buffer_delete(splash->scanout[i].buffer);
+
+ splash->n_scanout = 0;
+ kfree(splash->scanout);
+ splash->scanout = NULL;
+ }
+}
+
+static int drm_splash_client_hotplug(struct drm_client_dev *client)
+{
+ struct drm_splash *splash = client_to_drm_splash(client);
+ int ret;
+
+ guard(mutex)(&splash->hotplug_lock);
+
+ /* The modesets that get a splash are defined at first hotplug event */
+ if (splash->initialized)
+ return 0;
+
+ ret = drm_splash_init_client(splash);
+ if (ret == -ENODEV) {
+ drm_info(client->dev, "splash: no modeset found");
+ return 0;
+ } else if (ret) {
+ drm_err(client->dev,
+ "splash: failed to init client: %d", ret);
+ return ret;
+ }
+
+ /* Create the render thread, waken later */
+ splash->thread = kthread_create(drm_splash_render_thread,
+ splash, "drm_splash_%s",
+ client->dev->unique);
+ if (IS_ERR(splash->thread)) {
+ ret = PTR_ERR(splash->thread);
+ drm_err(client->dev, "splash: failed to create render thread: %d", ret);
+ drm_splash_free_scanout(client);
+ return ret;
+ }
+
+ if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BMP))
+ ret = drm_splash_fw_request_bmp(splash);
+ else if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT))
+ ret = drm_splash_bgrt_load(splash);
+ else
+ ret = 0;
+
+ if (ret) {
+ drm_err(client->dev, "splash: failed to kick image load: %d", ret);
+ kthread_stop(splash->thread);
+ drm_splash_free_scanout(client);
+ return ret;
+ }
+
+ /* Wake the render thread to show initial contents */
+ drm_splash_wake_render_thread(splash);
+
+ splash->initialized = true;
+
+ return 0;
+}
+
+static int drm_splash_client_restore(struct drm_client_dev *client, bool force)
+{
+ int ret;
+
+ if (force)
+ ret = drm_client_modeset_commit_locked(client);
+ else
+ ret = drm_client_modeset_commit(client);
+
+ return ret;
+}
+
+static void drm_splash_client_unregister(struct drm_client_dev *client)
+{
+ struct drm_splash *splash = client_to_drm_splash(client);
+
+ kthread_stop(splash->thread);
+ drm_splash_free_scanout(client);
+ drm_client_release(client);
+
+ if (splash->data_release)
+ splash->data_release(splash->data_priv);
+}
+
+static void drm_splash_client_free(struct drm_client_dev *client)
+{
+ struct drm_splash *splash = client_to_drm_splash(client);
+ struct drm_device *dev = client->dev;
+
+ mutex_destroy(&splash->hotplug_lock);
+ kfree(splash);
+
+ drm_dbg(dev, "Unregistered with drm splash");
+}
+
+static const struct drm_client_funcs drm_splash_client_funcs = {
+ .owner = THIS_MODULE,
+ .hotplug = drm_splash_client_hotplug,
+ .restore = drm_splash_client_restore,
+ .unregister = drm_splash_client_unregister,
+ .free = drm_splash_client_free,
+};
+
+/**
+ * drm_splash_register() - Register a drm device to drm_splash
+ * @dev: the drm device to register.
+ * @format: drm device preferred format.
+ */
+void drm_splash_register(struct drm_device *dev,
+ const struct drm_format_info *format)
+{
+ struct drm_splash *splash;
+
+ splash = kzalloc(sizeof(*splash), GFP_KERNEL);
+ if (!splash)
+ goto err_warn;
+
+ mutex_init(&splash->hotplug_lock);
+ spin_lock_init(&splash->data_lock);
+
+ if (format && format->num_planes == 1)
+ splash->preferred_format = format->format;
+ else
+ splash->preferred_format = DRM_FORMAT_RGB888;
+
+ if (drm_client_init(dev, &splash->client, "drm_splash",
+ &drm_splash_client_funcs))
+ goto err_free;
+
+ drm_client_register(&splash->client);
+ drm_dbg(dev, "Registered with drm splash");
+
+ return;
+
+err_free:
+ kfree(splash);
+err_warn:
+ drm_warn(dev, "Failed to register with drm splash");
+}
--
2.52.0
^ permalink raw reply related
* hello
From: zh yon @ 2025-12-27 11:55 UTC (permalink / raw)
To: linux-embedded
subscribe linux-embedded
^ permalink raw reply
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox