Linux Documentation
 help / color / mirror / Atom feed
* [PATCH v7 0/6] Add splash DRM client
@ 2026-11-16  0:21 Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 1/6] drm/clients: Kconfig: indent DRM_CLIENT_DEFAULT with tabs Màxim Pedraza Padilla
                   ` (5 more replies)
  0 siblings, 6 replies; 7+ messages in thread
From: Màxim Pedraza Padilla @ 2026-11-16  0:21 UTC (permalink / raw)
  To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Ard Biesheuvel, Jonathan Corbet, Helge Deller,
	Peter Jones
  Cc: Francesco Valla, Jani Nikula, Mario Limonciello,
	Javier Martinez Canillas, Jocelyn Falempe, Sam Ravnborg,
	Ilias Apalodimas, Shuah Khan, Randy Dunlap, dri-devel, linux-efi,
	linux-fbdev, linux-doc, linux-embedded, linux-kernel

This is the seventh version of the splash client, which I took over from
Francesco with v4. As Thomas suggested [1], it holds only the part that
can go in now: patches 1 to 6 and 8 of v6, with the MAINTAINERS entry
squashed into the patch that adds drm_splash.c. The device tree source
and its binding, the placement and rotation, and the connector
orientation will follow as a separate series on top of this one.

The client draws a splash on every output, including those plugged in
later, until userspace takes over. The image comes from the EFI BGRT or,
failing that, from a BMP loaded as firmware, named at build time
("drm_splash.bmp" by default); with neither, only the background is
drawn, in a colour set at build time. A BGRT image is drawn at the table
offsets, a BMP loaded as firmware is centred. A BGRT whose ACPI 6.2
orientation bits are set is skipped for now; turning it comes with the
rotation in the later series.

Changes since v6
----------------

From the reviews of v6 [1]-[6]:

  - MAINTAINERS: I am listed as M: and Francesco as R:, in patch 4
    (Thomas [2], Francesco [3]).
  - The firmware source is DRM_SPLASH_SRC_FW, and its Kconfig options
    DRM_CLIENT_SPLASH_SRC_FW and DRM_CLIENT_SPLASH_FW_NAME, since the
    BGRT is a BMP too (Thomas [4]).
  - The drm_splash_fw_load() stub returns 0 (Thomas [5]), and
    drm_splash_image_load() does not call it when the firmware source is
    disabled.
  - Thomas, I have kept your Reviewed-by on patch 4, as you said [6].

From sashiko's review of v6 [7], and further review:

  - Patch 4: restore leaves the commit to the work, which draws whatever
    is missing first, so a restore that comes while the device is
    suspended is no longer lost [7]; a forced restore stays forced across
    suspend, and the work commits even when no scanout is left.
  - Patch 4: a hotplug while userspace has the display waits for the
    next restore, as fbdev does; only a lack of memory makes the image be
    drawn again; unregister cancels the work before it looks for a
    firmware request, which the work can now make too; the tile group id
    is read under mode_config.mutex, which an EDID update holds while it
    changes or frees the group.
  - Patch 2: the image address and size are 0 when efi_bgrt_has_image()
    says there is no image, so that they never disagree on kernels with
    32-bit physical addresses.
  - Patch 3: the comment on the DIB header says that a negative width is
    invalid.

Testing
-------

  - QEMU versatilepb (pl111, 16 bpp): no image, and the BMP loaded as
    firmware, compared pixel by pixel.
  - x86-64 with OVMF: the BGRT through the new accessors; with
    CONFIG_DRM_CLIENT_SPLASH_SRC_FW=n, with and without a BGRT
    (bgrt_disable), the background alone and no error.
  - i386 without PAE on 32-bit OVMF, with the BGRT replaced from the
    initrd so that it points above 4 GiB: the background alone.
  - QEMU virt arm64 (virtio-gpu, 32 bpp) with KASAN and kmemleak: the
    driver bound and unbound repeatedly, a crafted bitmap offset, and
    the firmware arriving only after the unbind; no reports. With
    lockdep too, over several outputs, tiled displays and the bind
    cycles: no reports.
  - Several outputs, one of them off, and tiled displays from 2x1 up to
    3x3, 4x2 and 2x4 tiles, in several CRTC orders, simulated with
    DisplayID tiled topology EDIDs on virtio-gpu heads. The layouts with
    more than one row and column need a fix to the client modeset code
    that is not part of this series.
  - The error paths, with a debug patch forcing them: buffers and formats
    that fail on one output, work queued while suspended, a restore
    during suspend or with a master, a hotplug with a master, a fill that
    fails, a firmware request made by the work and raced against the
    unbind; no WARN or KASAN report in any of them.
  - An AM335x board (tilcdc, 16 bpp), booting over TFTP and NFS, with
    the BMP built in and from an initramfs, an 8 bpp BMP that is
    rejected, and 5 unbind and bind cycles of tilcdc, checked by reading
    the scanout buffer back.
  - W=1 and sparse clean; every patch builds on its own on arm and
    x86-64; checkpatch --strict clean apart from a quoted modpost line in
    patch 2 and the MAINTAINERS reminder on patch 3, whose new header
    FRAMEBUFFER LAYER already covers.

Open questions
--------------

  - Unbinding waits for a pending firmware request, as it cannot be
    cancelled and its callback uses the splash, and it waits under the
    device's clientlist_mutex. Normally the request is long over; with
    the sysfs fallback it can take up to the loading timeout. Not
    waiting would mean giving the callback its own refcounted object;
    I can do that as a follow-up.
  - The BGRT offsets are for the mode the firmware drew in. When KMS
    picks another one, the x86 check taken from efifb leaves the screen
    without a logo, and elsewhere the logo lands off centre. The offsets
    could be used only when the scanout matches the firmware's
    framebuffer, and the image centred otherwise.
  - The splash is a DRM client, so a device using it has no fbdev
    emulation and no /dev/fb0.

Based on drm-misc-next-2026-10-23 (cf5d8e390e2a).

[1] https://lore.kernel.org/all/066197a1-aa2b-455b-84d8-4e2215fad764@suse.de/
[2] https://lore.kernel.org/all/ed28619d-6ca7-46aa-82c0-c83f30a836e3@suse.de/
[3] https://lore.kernel.org/all/avTjrfIEPgyvPDti@bywater/
[4] https://lore.kernel.org/all/8eaf9955-485f-4908-bb65-1e0c3184fb36@suse.de/
[5] https://lore.kernel.org/all/c2cbc13a-a58d-4c0b-87ac-62daa6de6fba@suse.de/
[6] https://lore.kernel.org/all/4762a951-5b37-4e7a-8569-f55a4683c912@suse.de/
[7] https://lore.kernel.org/all/sashiko-outbox-186719@kernel.org/

v6: https://lore.kernel.org/r/20261027135342.2003632-1-maximpedraza@gmail.com
v5: https://lore.kernel.org/r/20261013093243.505776-1-maximpedraza@gmail.com
v4: https://lore.kernel.org/r/20261001195847.141192-1-maximpedraza@gmail.com
v3: https://lore.kernel.org/r/20260510-drm_client_splash-v3-0-a9aee9f0b2fc@valla.it
v2: https://lore.kernel.org/r/20260106-drm_client_splash-v2-0-6e86a7434b59@valla.it
v1: https://lore.kernel.org/r/20251027-drm_client_splash-v1-0-00698933b34a@valla.it

Màxim

Francesco Valla (2):
  drm: client: add splash client
  drm: docs: remove bootsplash from TODO

Màxim Pedraza Padilla (4):
  drm/clients: Kconfig: indent DRM_CLIENT_DEFAULT with tabs
  efi: bgrt: add accessors for the boot graphics
  fbdev: efifb: move the BMP headers to <video/bmp.h>
  drm/client: splash: pick the image source in one place

 Documentation/gpu/todo.rst                    |   17 -
 MAINTAINERS                                   |    8 +
 drivers/firmware/efi/efi-bgrt.c               |   76 +-
 drivers/gpu/drm/clients/Kconfig               |   80 +-
 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          | 1060 +++++++++++++++++
 drivers/video/fbdev/efifb.c                   |   49 +-
 include/linux/efi-bgrt.h                      |   39 +-
 include/video/bmp.h                           |   40 +
 11 files changed, 1330 insertions(+), 57 deletions(-)
 create mode 100644 drivers/gpu/drm/clients/drm_splash.c
 create mode 100644 include/video/bmp.h


base-commit: cf5d8e390e2ac2cfa24a50645efdbb9f0eebe65f
-- 
2.39.5


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v7 1/6] drm/clients: Kconfig: indent DRM_CLIENT_DEFAULT with tabs
  2026-11-16  0:21 [PATCH v7 0/6] Add splash DRM client Màxim Pedraza Padilla
@ 2026-11-16  0:21 ` Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 2/6] efi: bgrt: add accessors for the boot graphics Màxim Pedraza Padilla
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Màxim Pedraza Padilla @ 2026-11-16  0:21 UTC (permalink / raw)
  To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Ard Biesheuvel, Jonathan Corbet, Helge Deller,
	Peter Jones
  Cc: Francesco Valla, Jani Nikula, Mario Limonciello,
	Javier Martinez Canillas, Jocelyn Falempe, Sam Ravnborg,
	Ilias Apalodimas, Shuah Khan, Randy Dunlap, dri-devel, linux-efi,
	linux-fbdev, linux-doc, linux-embedded, linux-kernel

The body of DRM_CLIENT_DEFAULT is indented with spaces, unlike every
other entry in the file. Use tabs, so that the next client added to the
list does not have to pick one or the other.

No functional change.

Reviewed-by: Thomas Zimmermann <tzimmermann@suse.de>
Signed-off-by: Màxim Pedraza Padilla <maximpedraza@gmail.com>
---
 drivers/gpu/drm/clients/Kconfig | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
index 6096c623d9d5..8d8bc37ec9f2 100644
--- a/drivers/gpu/drm/clients/Kconfig
+++ b/drivers/gpu/drm/clients/Kconfig
@@ -114,10 +114,10 @@ config DRM_CLIENT_DEFAULT_LOG
 endchoice
 
 config DRM_CLIENT_DEFAULT
-       string
-       depends on DRM_CLIENT
-       default "fbdev" if DRM_CLIENT_DEFAULT_FBDEV
-       default "log" if DRM_CLIENT_DEFAULT_LOG
-       default ""
+	string
+	depends on DRM_CLIENT
+	default "fbdev" if DRM_CLIENT_DEFAULT_FBDEV
+	default "log" if DRM_CLIENT_DEFAULT_LOG
+	default ""
 
 endmenu
-- 
2.39.5


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v7 2/6] efi: bgrt: add accessors for the boot graphics
  2026-11-16  0:21 [PATCH v7 0/6] Add splash DRM client Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 1/6] drm/clients: Kconfig: indent DRM_CLIENT_DEFAULT with tabs Màxim Pedraza Padilla
@ 2026-11-16  0:21 ` Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 3/6] fbdev: efifb: move the BMP headers to <video/bmp.h> Màxim Pedraza Padilla
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Màxim Pedraza Padilla @ 2026-11-16  0:21 UTC (permalink / raw)
  To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Ard Biesheuvel, Jonathan Corbet, Helge Deller,
	Peter Jones
  Cc: Francesco Valla, Jani Nikula, Mario Limonciello,
	Javier Martinez Canillas, Jocelyn Falempe, Sam Ravnborg,
	Ilias Apalodimas, Shuah Khan, Randy Dunlap, dri-devel, linux-efi,
	linux-fbdev, linux-doc, linux-embedded, linux-kernel

The DRM splash client shows the boot graphics the firmware left behind,
and needs the image address and size, its offsets and the status bits.
Reading them from bgrt_tab and bgrt_image_size works with drm_client_lib
built in, but it can be a module, and neither is exported:

  ERROR: modpost: drivers/gpu/drm/clients/drm_client_lib.ko: symbol 'bgrt_tab' undefined!
  ERROR: modpost: drivers/gpu/drm/clients/drm_client_lib.ko: symbol 'bgrt_image_size' undefined!

Rather than export the data, add an accessor for each of them, exported
GPL only, with stubs in efi-bgrt.h for CONFIG_ACPI_BGRT=n so that users
need no #ifdef of their own. The variables stay global for
drivers/acpi/bgrt.c, and are only written by efi_bgrt_init(): mark them
__ro_after_init.

The table gives the image address as a u64. efi_bgrt_has_image() only
says yes when phys_addr_t can hold it, and the address and size are 0
when it says no, so that they never disagree on kernels with 32-bit
physical addresses.

Reported-by: Mario Limonciello <mario.limonciello@amd.com>
Closes: https://lore.kernel.org/dri-devel/5d7067de-97b7-4232-9cf6-e4b978696482@amd.com/
Suggested-by: Jani Nikula <jani.nikula@linux.intel.com>
Reviewed-by: Thomas Zimmermann <tzimmermann@suse.de>
Signed-off-by: Màxim Pedraza Padilla <maximpedraza@gmail.com>
---
 drivers/firmware/efi/efi-bgrt.c | 76 ++++++++++++++++++++++++++++++++-
 include/linux/efi-bgrt.h        | 39 ++++++++++++++++-
 2 files changed, 112 insertions(+), 3 deletions(-)

diff --git a/drivers/firmware/efi/efi-bgrt.c b/drivers/firmware/efi/efi-bgrt.c
index 1da451582812..8f5f5edf5ede 100644
--- a/drivers/firmware/efi/efi-bgrt.c
+++ b/drivers/firmware/efi/efi-bgrt.c
@@ -16,8 +16,8 @@
 #include <linux/efi.h>
 #include <linux/efi-bgrt.h>
 
-struct acpi_table_bgrt bgrt_tab;
-size_t bgrt_image_size;
+struct acpi_table_bgrt bgrt_tab __ro_after_init;
+size_t bgrt_image_size __ro_after_init;
 
 struct bmp_header {
 	u16 id;
@@ -89,3 +89,75 @@ void __init efi_bgrt_init(struct acpi_table_header *table)
 out:
 	memset(bgrt, 0, sizeof(bgrt_tab));
 }
+
+/**
+ * efi_bgrt_has_image() - Whether the firmware left a boot graphic
+ *
+ * Return: true if the BGRT points at a BMP image, which efi_bgrt_init()
+ * checked and reserved, at an address that phys_addr_t can hold. The
+ * other accessors only mean something when it returns true.
+ */
+bool efi_bgrt_has_image(void)
+{
+	return bgrt_tab.image_address &&
+	       bgrt_tab.image_address == (phys_addr_t)bgrt_tab.image_address;
+}
+EXPORT_SYMBOL_GPL(efi_bgrt_has_image);
+
+/**
+ * efi_bgrt_image_address() - Physical address of the boot graphic
+ *
+ * Return: the address of the BMP image, or 0 if there is none.
+ */
+phys_addr_t efi_bgrt_image_address(void)
+{
+	return efi_bgrt_has_image() ? bgrt_tab.image_address : 0;
+}
+EXPORT_SYMBOL_GPL(efi_bgrt_image_address);
+
+/**
+ * efi_bgrt_image_size() - Size of the boot graphic
+ *
+ * Return: the size of the BMP image, as its header gives it, or 0 if there
+ * is none.
+ */
+size_t efi_bgrt_image_size(void)
+{
+	return efi_bgrt_has_image() ? bgrt_image_size : 0;
+}
+EXPORT_SYMBOL_GPL(efi_bgrt_image_size);
+
+/**
+ * efi_bgrt_status() - Status field of the BGRT
+ *
+ * Return: the status byte. Bit 0 tells whether the image is still on
+ * screen and, since ACPI 6.2, bits 1-2 how far it has to be turned to show
+ * upright (ACPI_BGRT_ORIENTATION_OFFSET).
+ */
+u8 efi_bgrt_status(void)
+{
+	return bgrt_tab.status;
+}
+EXPORT_SYMBOL_GPL(efi_bgrt_status);
+
+/**
+ * efi_bgrt_image_offset_x() - Horizontal position of the boot graphic
+ *
+ * Return: the X offset, in pixels, at which the firmware drew the image.
+ */
+u32 efi_bgrt_image_offset_x(void)
+{
+	return bgrt_tab.image_offset_x;
+}
+EXPORT_SYMBOL_GPL(efi_bgrt_image_offset_x);
+
+/**
+ * efi_bgrt_image_offset_y() - Vertical position of the boot graphic
+ *
+ * Return: the Y offset, in pixels, at which the firmware drew the image.
+ */
+u32 efi_bgrt_image_offset_y(void)
+{
+	return bgrt_tab.image_offset_y;
+}
+EXPORT_SYMBOL_GPL(efi_bgrt_image_offset_y);
diff --git a/include/linux/efi-bgrt.h b/include/linux/efi-bgrt.h
index e6cd51005633..1c287e5fd388 100644
--- a/include/linux/efi-bgrt.h
+++ b/include/linux/efi-bgrt.h
@@ -9,7 +9,14 @@
 void efi_bgrt_init(struct acpi_table_header *table);
 int __init acpi_parse_bgrt(struct acpi_table_header *table);
 
-/* The BGRT data itself; only valid if bgrt_image != NULL. */
+bool efi_bgrt_has_image(void);
+phys_addr_t efi_bgrt_image_address(void);
+size_t efi_bgrt_image_size(void);
+u8 efi_bgrt_status(void);
+u32 efi_bgrt_image_offset_x(void);
+u32 efi_bgrt_image_offset_y(void);
+
+/* The BGRT data itself; only valid if efi_bgrt_has_image(). */
 extern size_t bgrt_image_size;
 extern struct acpi_table_bgrt bgrt_tab;
 
@@ -21,6 +28,36 @@ static inline int __init acpi_parse_bgrt(struct acpi_table_header *table)
 	return 0;
 }
 
+static inline bool efi_bgrt_has_image(void)
+{
+	return false;
+}
+
+static inline phys_addr_t efi_bgrt_image_address(void)
+{
+	return 0;
+}
+
+static inline size_t efi_bgrt_image_size(void)
+{
+	return 0;
+}
+
+static inline u8 efi_bgrt_status(void)
+{
+	return 0;
+}
+
+static inline u32 efi_bgrt_image_offset_x(void)
+{
+	return 0;
+}
+
+static inline u32 efi_bgrt_image_offset_y(void)
+{
+	return 0;
+}
+
 #endif /* !CONFIG_ACPI_BGRT */
 
 #endif /* _LINUX_EFI_BGRT_H */
-- 
2.39.5


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v7 3/6] fbdev: efifb: move the BMP headers to <video/bmp.h>
  2026-11-16  0:21 [PATCH v7 0/6] Add splash DRM client Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 1/6] drm/clients: Kconfig: indent DRM_CLIENT_DEFAULT with tabs Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 2/6] efi: bgrt: add accessors for the boot graphics Màxim Pedraza Padilla
@ 2026-11-16  0:21 ` Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 4/6] drm: client: add splash client Màxim Pedraza Padilla
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 7+ messages in thread
From: Màxim Pedraza Padilla @ 2026-11-16  0:21 UTC (permalink / raw)
  To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Ard Biesheuvel, Jonathan Corbet, Helge Deller,
	Peter Jones
  Cc: Francesco Valla, Jani Nikula, Mario Limonciello,
	Javier Martinez Canillas, Jocelyn Falempe, Sam Ravnborg,
	Ilias Apalodimas, Shuah Khan, Randy Dunlap, dri-devel, linux-efi,
	linux-fbdev, linux-doc, linux-embedded, linux-kernel

The DRM splash client parses the same BMP headers as efifb. Rather than
keep a second copy of the structures, move them to include/video/bmp.h
with the magic number.

BMP is little endian whatever the CPU, so the header gives the fields
as __le16 and __le32, and efifb now reads them with
get_unaligned_le16() and get_unaligned_le32(). Width and height are
read signed, as the format has them. No change on the little endian
systems EFI runs on.

Suggested-by: Thomas Zimmermann <tzimmermann@suse.de>
Reviewed-by: Thomas Zimmermann <tzimmermann@suse.de>
Reviewed-by: Javier Martinez Canillas <javierm@redhat.com>
Signed-off-by: Màxim Pedraza Padilla <maximpedraza@gmail.com>
---
 drivers/video/fbdev/efifb.c | 49 ++++++++++++++-----------------------
 include/video/bmp.h         | 40 ++++++++++++++++++++++++++++++
 2 files changed, 58 insertions(+), 31 deletions(-)
 create mode 100644 include/video/bmp.h

diff --git a/drivers/video/fbdev/efifb.c b/drivers/video/fbdev/efifb.c
index 3411be06d3e6..fd348a72f780 100644
--- a/drivers/video/fbdev/efifb.c
+++ b/drivers/video/fbdev/efifb.c
@@ -16,32 +16,13 @@
 #include <linux/platform_device.h>
 #include <linux/printk.h>
 #include <linux/sysfb.h>
+#include <linux/unaligned.h>
+#include <video/bmp.h>
 #include <video/vga.h>
 #include <asm/efi.h>
 #include <drm/drm_connector.h>  /* For DRM_MODE_PANEL_ORIENTATION_* */
 #include <drm/drm_panel_quirks.h> /* For drm_get_panel_orientation_quirk */
 
-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;
-
 static bool use_bgrt = true;
 static bool request_mem_succeeded = false;
 static u64 mem_flags = EFI_MEMORY_WC | EFI_MEMORY_UC;
@@ -145,9 +126,10 @@ static bool efifb_bgrt_sanity_check(const struct screen_info *si, u32 bmp_width)
 
 static void efifb_show_boot_graphics(struct fb_info *info, const struct screen_info *si)
 {
-	u32 bmp_width, bmp_height, bmp_pitch, dst_x, y, src_y;
+	u32 bmp_width, bmp_height, bmp_pitch, bitmap_offset, dst_x, y, src_y;
 	struct bmp_file_header *file_header;
 	struct bmp_dib_header *dib_header;
+	s32 width, height;
 	void *bgrt_image = NULL;
 	u8 *dst = info->screen_base;
 
@@ -186,20 +168,25 @@ static void efifb_show_boot_graphics(struct fb_info *info, const struct screen_i
 		goto error;
 
 	file_header = bgrt_image;
-	if (file_header->id != 0x4d42 || file_header->reserved != 0)
+	if (get_unaligned_le16(&file_header->id) != BMP_FILE_MAGIC_ID ||
+	    get_unaligned_le32(&file_header->reserved) != 0)
 		goto error;
+	bitmap_offset = get_unaligned_le32(&file_header->bitmap_offset);
 
 	dib_header = bgrt_image + sizeof(*file_header);
-	if (dib_header->dib_header_size != 40 || dib_header->width < 0 ||
-	    dib_header->planes != 1 || dib_header->bpp != 24 ||
-	    dib_header->compression != 0)
+	width = get_unaligned_le32(&dib_header->width);
+	height = get_unaligned_le32(&dib_header->height);
+	if (get_unaligned_le32(&dib_header->dib_header_size) != 40 ||
+	    width < 0 || get_unaligned_le16(&dib_header->planes) != 1 ||
+	    get_unaligned_le16(&dib_header->bpp) != 24 ||
+	    get_unaligned_le32(&dib_header->compression) != 0)
 		goto error;
 
-	bmp_width = dib_header->width;
-	bmp_height = abs(dib_header->height);
+	bmp_width = width;
+	bmp_height = abs(height);
 	bmp_pitch = round_up(3 * bmp_width, 4);
 
-	if ((file_header->bitmap_offset + bmp_pitch * bmp_height) >
+	if ((bitmap_offset + bmp_pitch * bmp_height) >
 				bgrt_image_size)
 		goto error;
 
@@ -222,12 +209,12 @@ static void efifb_show_boot_graphics(struct fb_info *info, const struct screen_i
 
 		src_y = y - bgrt_tab.image_offset_y;
 		/* Positive header height means upside down row order */
-		if (dib_header->height > 0)
+		if (height > 0)
 			src_y = (bmp_height - 1) - src_y;
 
 		memset(dst, 0, bgrt_tab.image_offset_x * 4);
 		dst_x = bgrt_tab.image_offset_x;
-		efifb_copy_bmp(bgrt_image + file_header->bitmap_offset +
+		efifb_copy_bmp(bgrt_image + bitmap_offset +
 					    src_y * bmp_pitch,
 			       (u32 *)dst + dst_x, bmp_width, si);
 		dst_x += bmp_width;
diff --git a/include/video/bmp.h b/include/video/bmp.h
new file mode 100644
index 000000000000..8e97ded98dfb
--- /dev/null
+++ b/include/video/bmp.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef _VIDEO_BMP_H
+#define _VIDEO_BMP_H
+
+#include <linux/types.h>
+
+/* "BM", the first two bytes of a BMP file */
+#define BMP_FILE_MAGIC_ID 0x4d42
+
+/*
+ * The headers at the start of a BMP file. Every field is little endian,
+ * whatever the CPU, and the headers need not be aligned in memory: read them
+ * with get_unaligned_le16() and get_unaligned_le32().
+ */
+struct bmp_file_header {
+	__le16 id;
+	__le32 file_size;
+	__le32 reserved;
+	__le32 bitmap_offset;
+} __packed;
+
+/*
+ * BITMAPINFOHEADER. Width and height are signed: a negative width is
+ * invalid, and a negative height means the rows go top to bottom.
+ */
+struct bmp_dib_header {
+	__le32 dib_header_size;
+	__le32 width;
+	__le32 height;
+	__le16 planes;
+	__le16 bpp;
+	__le32 compression;
+	__le32 bitmap_size;
+	__le32 horz_resolution;
+	__le32 vert_resolution;
+	__le32 colors_used;
+	__le32 colors_important;
+} __packed;
+
+#endif /* _VIDEO_BMP_H */
-- 
2.39.5


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v7 4/6] drm: client: add splash client
  2026-11-16  0:21 [PATCH v7 0/6] Add splash DRM client Màxim Pedraza Padilla
                   ` (2 preceding siblings ...)
  2026-11-16  0:21 ` [PATCH v7 3/6] fbdev: efifb: move the BMP headers to <video/bmp.h> Màxim Pedraza Padilla
@ 2026-11-16  0:21 ` Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 5/6] drm: docs: remove bootsplash from TODO Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 6/6] drm/client: splash: pick the image source in one place Màxim Pedraza Padilla
  5 siblings, 0 replies; 7+ messages in thread
From: Màxim Pedraza Padilla @ 2026-11-16  0:21 UTC (permalink / raw)
  To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Ard Biesheuvel, Jonathan Corbet, Helge Deller,
	Peter Jones
  Cc: Francesco Valla, Jani Nikula, Mario Limonciello,
	Javier Martinez Canillas, Jocelyn Falempe, Sam Ravnborg,
	Ilias Apalodimas, Shuah Khan, Randy Dunlap, dri-devel, linux-efi,
	linux-fbdev, linux-doc, linux-embedded, linux-kernel

From: Francesco Valla <francesco@valla.it>

Add a DRM client that draws a simple splash, with possibility to show:

  - the logo provided by EFI BGRT;
  - a static BMP image (loaded as firmware);
  - a colored background.

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 is set at build time from a dedicated config
option. 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 is set at build time, 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.

v7:
- restore leaves the commit to the work, which draws what is missing
  first, commits even with no scanouts left, and keeps a forced restore
  forced across suspend
- a hotplug while userspace has the display waits for the next restore,
  as fbdev does
- only a lack of memory makes the image be drawn again
- unregister cancels the work before it looks for a firmware request,
  which a restore can now make too
- the MAINTAINERS entry comes with this patch, with Màxim as maintainer
  and Francesco as reviewer
- rename the BMP source to FW, which is where it comes from: the BGRT is
  a BMP too
- fw_load() returns 0 when the firmware source is disabled, and
  image_load() does not call it then
- read the tile group id under mode_config.mutex, which an EDID update
  holds while it changes or frees the group

v6:
- draw from a work item on system_power_efficient_wq instead of a
  render thread
- set the scanouts up again on every hotplug, and keep the image for
  the outputs plugged in later
- keep the scanouts on screen until the new ones are committed, so that
  a hotplug does not blank every output
- also when the commit or the hotplug fails; restore retires them too
- redraw the image after a background that is drawn again
- accept the V4 and V5 BMP headers
- limit the background color to RGB888
- point no modeset at the framebuffer it had, which a probe without
  connectors leaves in place
- leave restore to the work while the device is suspended
- say in DRM_CLIENT_SPLASH_BMP_NAME that the BMP is requested only once
- don't fail a hotplug, which would stop the later ones for good
- restore commits whatever the modesets hold: an output without a
  buffer has no mode either, and is switched off
- take the BMP headers from <video/bmp.h>
- draw the image in two steps: the BMP upright in XRGB8888, then into
  the buffer's format, taken from its drm_format_info
- drop the splash_color and splash_bmp parameters: the color is set at
  build time only, and so is the BMP's name, with
  DRM_CLIENT_SPLASH_BMP_NAME
- read the 24 bpp source pixels with get_unaligned_le24() instead of a
  u32 load that runs past the pixel
- check the BGRT offsets without overflowing
- size a tiled framebuffer from the tile offsets, whatever order the
  CRTCs come in, and leave out a tile whose plane cannot take the
  format of its group
- don't pick a preferred format that cannot be drawn to, such as C8;
  without one from the driver, pick the one fbdev would
- only pick formats the plane takes with the linear modifier, the
  layout of a dumb buffer
- switch off a modeset left without a buffer instead of failing the
  commit for every output
- mark the background drawn only once it is
- hold the work back while the device is suspended, drawing included,
  and queue it on resume
- drop the fallback to a BMP that was never requested when the BGRT
  fails to load, and the unused struct device
- log with drm_dbg() rather than drm_info(), and end the messages with
  a newline
- destroy the mutex at unregister, and when registering fails
- sort the selects of DRM_CLIENT_SELECTION, use unsigned loop counters,
  and reword the comment on the x86 BGRT sanity check

v5:
- clear the modesets' framebuffers when the scanouts are freed
- free the scanouts when setting up fails
- release a firmware that lands after the drawing is done
- read the BGRT through the efi_bgrt_*() accessors
- drop "static inline" from the .c file

v4:
- fix the render thread never exiting when the BMP firmware is missing,
  a lost wake-up, and stopping a thread that had exited, never existed
  or failed to start
- fix buffers going to the wrong modesets and tiled groups in
  drm_splash_init_client(), and take modeset_mutex there
- don't keep an ERR_PTR as a buffer
- unmap the image only once
- fix use-after-frees on unbind and in the firmware callback
- check the BMP size without overflowing
- indent the Kconfig entry with tabs

Signed-off-by: Francesco Valla <francesco@valla.it>
Co-developed-by: Màxim Pedraza Padilla <maximpedraza@gmail.com>
Signed-off-by: Màxim Pedraza Padilla <maximpedraza@gmail.com>
Reviewed-by: Thomas Zimmermann <tzimmermann@suse.de>
---
 MAINTAINERS                                   |    8 +
 drivers/gpu/drm/clients/Kconfig               |   70 +-
 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          | 1032 +++++++++++++++++
 6 files changed, 1127 insertions(+), 1 deletion(-)
 create mode 100644 drivers/gpu/drm/clients/drm_splash.c

diff --git a/MAINTAINERS b/MAINTAINERS
index a08087fb6e67..a97e17509451 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9168,6 +9168,14 @@ F:	Documentation/gpu/drm-ras.rst
 F:	drivers/gpu/drm/drm_ras*
 F:	include/drm/drm_ras*
 
+DRM SPLASH
+M:	Màxim Pedraza Padilla <maximpedraza@gmail.com>
+R:	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:	Natalie Vock <nat@pixelcluster.dev>
 M:	Arun Pravin <arunpravin.paneerselvam@amd.com>
diff --git a/drivers/gpu/drm/clients/Kconfig b/drivers/gpu/drm/clients/Kconfig
index 8d8bc37ec9f2..96ce58dda569 100644
--- a/drivers/gpu/drm/clients/Kconfig
+++ b/drivers/gpu/drm/clients/Kconfig
@@ -13,6 +13,7 @@ config DRM_CLIENT_SELECTION
 	tristate
 	depends on DRM
 	select DRM_CLIENT_LIB if DRM_CLIENT_LOG
+	select DRM_CLIENT_LIB if DRM_CLIENT_SPLASH
 	select DRM_CLIENT_LIB if DRM_FBDEV_EMULATION
 	help
 	  Drivers that support in-kernel DRM clients have to select this
@@ -85,10 +86,70 @@ 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 is displayed on every screen, including those plugged in
+	  later.
+
+config DRM_CLIENT_SPLASH_BACKGROUND_COLOR
+	hex "Splash background color"
+	depends on DRM_CLIENT_SPLASH
+	default 0x000000
+	range 0x000000 0xffffff
+	help
+	  The splash background color, in RGB888 format.
+
+config DRM_CLIENT_SPLASH_SRC_BGRT
+	bool "EFI BGRT as splash source"
+	depends on DRM_CLIENT_SPLASH
+	depends on ACPI_BGRT
+	select DRM_CLIENT_SPLASH_BMP_SUPPORT
+	help
+	  Use the BGRT image provided by the EFI bootloader, drawn where the
+	  firmware drew it. The color specified through the
+	  DRM_CLIENT_SPLASH_BACKGROUND_COLOR config option will be used as
+	  background.
+
+config DRM_CLIENT_SPLASH_SRC_FW
+	bool "BMP image loaded as firmware as splash source"
+	depends on DRM_CLIENT_SPLASH
+	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.
+
+config DRM_CLIENT_SPLASH_FW_NAME
+	string "Firmware name of the BMP image"
+	depends on DRM_CLIENT_SPLASH_SRC_FW
+	default "drm_splash.bmp"
+	help
+	  The file the BMP image is loaded from, looked up like any other
+	  firmware: built into the kernel through EXTRA_FIRMWARE, in the
+	  initramfs, or under /lib/firmware. It is requested only once, when
+	  the splash first sets up: for a driver that probes before the root
+	  file system is mounted, it has to be built in or in the initramfs.
+
+config DRM_CLIENT_SPLASH_BMP_SUPPORT
+	bool
+
 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 +172,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 +185,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 1304ff358509..b43b1e86fe8e 100644
--- a/drivers/gpu/drm/clients/Makefile
+++ b/drivers/gpu/drm/clients/Makefile
@@ -6,4 +6,5 @@ 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_fbdev_helper.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 6dc078bf6503..48ee0c1c2529 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 515aceac22b1..c19498938ee3 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 000000000000..d54cf628cf17
--- /dev/null
+++ b/drivers/gpu/drm/clients/drm_splash.c
@@ -0,0 +1,1032 @@
+// SPDX-License-Identifier: GPL-2.0 or MIT
+/*
+ * Copyright (c) 2025-2026 Francesco Valla <francesco@valla.it>
+ */
+
+#include <linux/completion.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/module.h>
+#include <linux/mutex.h>
+#include <linux/overflow.h>
+#include <linux/slab.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#include <acpi/actbl1.h>
+
+#include <video/bmp.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.
+ */
+
+struct drm_splash_scanout {
+	int id;
+	/* Modesets scanning this buffer out, by position in the list */
+	u32 modesets;
+	u32 format;
+	unsigned int width;
+	unsigned int height;
+	struct drm_client_buffer *buffer;
+	bool bg_drawn;
+	bool img_drawn;
+};
+
+struct drm_splash {
+	struct drm_client_dev client;
+	u32 preferred_format;
+
+	/* Draws what is missing and shows it, queued when there is news */
+	struct work_struct work;
+
+	/*
+	 * Serialises the work against hotplug events, which set the scanouts
+	 * up again, suspend and resume, and the firmware callback
+	 */
+	struct mutex lock;
+	/* Suspended, and whether the work was held back meanwhile */
+	bool suspended, work_skipped;
+	/* A restore for the work to do, and whether it is forced */
+	bool restore_pending, restore_force;
+	/* A hotplug held back while userspace had the display, for restore */
+	bool hotplug_pending;
+	/* The source is picked at the first hotplug that finds an output */
+	bool source_picked;
+
+	u32 n_scanout;
+	struct drm_splash_scanout *scanout;
+	/*
+	 * The scanouts a hotplug replaced, kept on screen until the work has
+	 * committed the new ones, so that the outputs do not go dark meanwhile
+	 */
+	u32 n_old_scanout;
+	struct drm_splash_scanout *old_scanout;
+
+	/* A request was made, and fw_done tells when its callback is over */
+	bool fw_requested;
+	struct completion fw_done;
+	/*
+	 * The image is kept for as long as the splash is there, so that an
+	 * output plugged in later gets it too
+	 */
+	const struct firmware *fw;
+	void *map_data;
+
+	bool use_bgrt;
+};
+
+static struct drm_splash *client_to_drm_splash(struct drm_client_dev *client)
+{
+	return container_of_const(client, struct drm_splash, client);
+}
+
+/* An EDID update can change or free the tile group under mode_config.mutex */
+static int drm_splash_tile_group_id(struct drm_connector *connector)
+{
+	guard(mutex)(&connector->dev->mode_config.mutex);
+	return connector->has_tile ? connector->tile_group->id : -1;
+}
+
+static struct drm_splash_scanout *
+get_scanout_from_tile_group(struct drm_splash *splash, int id)
+{
+	unsigned int j;
+
+	for (j = 0; j < splash->n_scanout; j++)
+		if (splash->scanout[j].id == id)
+			return &splash->scanout[j];
+
+	return NULL;
+}
+
+static void drm_splash_queue(struct drm_splash *splash)
+{
+	queue_work(system_power_efficient_wq, &splash->work);
+}
+
+#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_FW)
+static int drm_splash_fw_load(struct drm_splash *splash, const u8 **data,
+			      size_t *size)
+{
+	const struct firmware *fw = splash->fw;
+
+	/* Under the lock, which the firmware callback takes to hand it over */
+	if (!fw)
+		return -ENOENT;
+
+	*data = fw->data;
+	*size = fw->size;
+
+	return 0;
+}
+
+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) {
+		/* The firmware loader has said why already */
+		drm_dbg(client->dev, "splash: no firmware\n");
+		release_firmware(fw);
+		fw = NULL;
+	}
+
+	scoped_guard(mutex, &splash->lock)
+		splash->fw = fw;
+
+	/* Even without it: a background that failed to draw gets another go */
+	drm_splash_queue(splash);
+
+	/* Last: once this is seen, unregister may free the splash */
+	complete(&splash->fw_done);
+}
+
+static int drm_splash_fw_request(struct drm_splash *splash)
+{
+	struct drm_client_dev *client = &splash->client;
+
+	drm_dbg(client->dev, "splash: request %s as firmware\n",
+		CONFIG_DRM_CLIENT_SPLASH_FW_NAME);
+
+	return request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT,
+				       CONFIG_DRM_CLIENT_SPLASH_FW_NAME,
+				       client->dev->dev, GFP_KERNEL,
+				       splash, drm_splash_fw_callback);
+}
+#else
+static int drm_splash_fw_load(struct drm_splash *splash, const u8 **data,
+			      size_t *size)
+{
+	return 0;
+}
+
+static int drm_splash_fw_request(struct drm_splash *splash)
+{
+	return -EOPNOTSUPP;
+}
+#endif // CONFIG_DRM_CLIENT_SPLASH_SRC_FW
+
+static bool drm_splash_bgrt_available(struct drm_splash *splash)
+{
+	struct drm_client_dev *client = &splash->client;
+
+	if (!IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_BGRT))
+		return false;
+
+	if (!efi_bgrt_has_image()) {
+		drm_dbg(client->dev, "splash: no BGRT found\n");
+		return false;
+	}
+
+	if (efi_bgrt_status() & ACPI_BGRT_ORIENTATION_OFFSET) {
+		drm_dbg(client->dev, "splash: BGRT rotation bits set, skipping\n");
+		return false;
+	}
+
+	return true;
+}
+
+static int drm_splash_bgrt_load(struct drm_splash *splash, const u8 **data,
+				size_t *size)
+{
+	struct drm_client_dev *client = &splash->client;
+	phys_addr_t addr = efi_bgrt_image_address();
+	size_t len = efi_bgrt_image_size();
+
+	if (!drm_splash_bgrt_available(splash))
+		return -ENOENT;
+
+	/* Mapped once, and kept for the outputs plugged in later */
+	if (!splash->map_data) {
+		drm_dbg(client->dev, "splash: BGRT image is at %pa, size=%zX\n",
+			&addr, len);
+
+		splash->map_data = memremap(addr, len, MEMREMAP_WB);
+		if (!splash->map_data) {
+			drm_warn(client->dev,
+				 "splash: failed to map BGRT image memory\n");
+			return -ENOMEM;
+		}
+	}
+
+	*data = splash->map_data;
+	*size = len;
+
+	return 0;
+}
+
+static u32 drm_splash_find_usable_format(struct drm_plane *plane,
+					 u32 preferred_format)
+{
+	unsigned int i;
+
+	/* Dumb buffers are linear: the plane must take the format so */
+	if (drm_draw_can_convert_from_xrgb8888(preferred_format) &&
+	    drm_plane_has_format(plane, preferred_format, DRM_FORMAT_MOD_LINEAR))
+		return preferred_format;
+
+	/* Otherwise, find the first format that can be converted from XRGB8888 */
+	for (i = 0; i < plane->format_count; i++) {
+		u32 format = plane->format_types[i];
+
+		if (drm_draw_can_convert_from_xrgb8888(format) &&
+		    drm_plane_has_format(plane, format, DRM_FORMAT_MOD_LINEAR))
+			return format;
+	}
+
+	return DRM_FORMAT_INVALID;
+}
+
+static void drm_splash_fill(struct iosys_map *map, unsigned int dst_pitch,
+			    unsigned int height, unsigned int width,
+			    const struct drm_format_info *format, u32 color)
+{
+	switch (drm_format_info_bpp(format, 0)) {
+	case 16:
+		drm_draw_fill16(map, dst_pitch, height, width, color);
+		break;
+	case 24:
+		drm_draw_fill24(map, dst_pitch, height, width, color);
+		break;
+	case 32:
+		drm_draw_fill32(map, dst_pitch, height, width, color);
+		break;
+	default:
+		WARN_ONCE(1, "Can't fill %p4cc\n", &format->format);
+	}
+}
+
+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);
+	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\n", ret);
+		return ret;
+	}
+
+	drm_splash_fill(&map, fb->pitches[0], drm_rect_height(&r),
+			drm_rect_width(&r), fb->format, color);
+
+	drm_client_buffer_vunmap_local(buffer);
+
+	return drm_client_buffer_flush(buffer, &r);
+}
+
+#if IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_BMP_SUPPORT)
+/* The rows of a 24 bpp BMP, bottom up or top down, upright in XRGB8888 */
+static void drm_splash_bmp_decode(u32 *dst, const u8 *src, unsigned int spitch,
+				  unsigned int width, unsigned int height,
+				  bool invert_y)
+{
+	unsigned int x, y;
+
+	for (y = 0; y < height; y++) {
+		const u8 *p = src + (invert_y ? height - 1 - y : y) * spitch;
+
+		for (x = 0; x < width; x++, p += 3)
+			*dst++ = get_unaligned_le24(p);
+	}
+}
+
+/* An XRGB8888 image into a buffer of another format, at x0, y0 */
+static void drm_splash_blit(struct iosys_map *map, unsigned int dpitch,
+			    const struct drm_format_info *format,
+			    unsigned int x0, unsigned int y0, const u32 *src,
+			    unsigned int width, unsigned int height)
+{
+	unsigned int bpp = drm_format_info_bpp(format, 0);
+	unsigned int x, y, off;
+	u32 c;
+
+	for (y = 0; y < height; y++) {
+		off = (y0 + y) * dpitch + x0 * bpp / 8;
+
+		switch (bpp) {
+		case 16:
+			for (x = 0; x < width; x++, off += 2) {
+				c = drm_draw_color_from_xrgb8888(*src++, format->format);
+				iosys_map_wr(map, off, u16, c);
+			}
+			break;
+		case 24:
+			for (x = 0; x < width; x++, off += 3) {
+				c = drm_draw_color_from_xrgb8888(*src++, format->format);
+				iosys_map_wr(map, off, u8, c & 0xff);
+				iosys_map_wr(map, off + 1, u8, (c >> 8) & 0xff);
+				iosys_map_wr(map, off + 2, u8, (c >> 16) & 0xff);
+			}
+			break;
+		case 32:
+			for (x = 0; x < width; x++, off += 4) {
+				c = drm_draw_color_from_xrgb8888(*src++, format->format);
+				iosys_map_wr(map, off, u32, c);
+			}
+			break;
+		}
+	}
+}
+
+static int drm_splash_bmp_to_scanout(struct drm_splash *splash,
+				     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;
+	const struct bmp_file_header *file_header;
+	const struct bmp_dib_header *dib_header;
+	u32 dib_header_size;
+	u16 bmp_id, bmp_bpp, bmp_planes;
+	u32 bmp_compression, bmp_pitch;
+	s32 bmp_width, bmp_height;
+	bool bmp_invert_y;
+	u32 bitmap_offset;
+	size_t row_len, pixels_len, end;
+	unsigned int x_pad, y_pad;
+	const u8 *image_data;
+	struct iosys_map map;
+	struct drm_rect r;
+	u32 *img;
+	int ret;
+
+	if (data_len < (sizeof(*file_header) + sizeof(*dib_header))) {
+		drm_err(client->dev, "splash: BMP file too short\n");
+		return -EINVAL;
+	}
+
+	file_header = (const struct bmp_file_header *)data;
+
+	bmp_id = get_unaligned_le16(&file_header->id);
+	if (bmp_id != BMP_FILE_MAGIC_ID) {
+		drm_err(client->dev, "splash: invalid BMP magic 0x%04X\n", bmp_id);
+		return -EINVAL;
+	}
+
+	bitmap_offset = get_unaligned_le32(&file_header->bitmap_offset);
+
+	dib_header = (const struct bmp_dib_header *)(data + sizeof(*file_header));
+
+	dib_header_size = get_unaligned_le32(&dib_header->dib_header_size);
+
+	bmp_width = (s32)get_unaligned_le32(&dib_header->width);
+	bmp_height = (s32)get_unaligned_le32(&dib_header->height);
+	bmp_planes = get_unaligned_le16(&dib_header->planes);
+	bmp_bpp = get_unaligned_le16(&dib_header->bpp);
+	bmp_compression = get_unaligned_le32(&dib_header->compression);
+
+	/*
+	 * Restrict supported format to uncompressed, 24bit RGB888. The V4 and
+	 * V5 headers only add fields after these, and bitmap_offset finds the
+	 * pixels.
+	 */
+	if (dib_header_size < 40 || bmp_width <= 0 || bmp_height == 0 ||
+	    bmp_height == S32_MIN || bmp_planes != 1 ||
+	    bmp_compression != 0 || bmp_bpp != 24) {
+		drm_err(client->dev, "splash: invalid BMP format\n");
+		return -EINVAL;
+	}
+
+	/* A positive height means bottom-to-top scan direction */
+	bmp_invert_y = (bmp_height > 0);
+	bmp_height = abs(bmp_height);
+
+	/*
+	 * Every field comes from the image, which the firmware supplied, so
+	 * check the arithmetic before trusting the result: rows are padded to
+	 * four bytes, and the pixels must end within the data.
+	 */
+	if (check_mul_overflow((size_t)bmp_width, (size_t)3, &row_len) ||
+	    check_add_overflow(row_len, (size_t)3, &row_len) ||
+	    check_mul_overflow(row_len & ~(size_t)3, (size_t)bmp_height,
+			       &pixels_len) ||
+	    check_add_overflow((size_t)bitmap_offset, pixels_len, &end) ||
+	    end > data_len) {
+		drm_err(client->dev, "splash: invalid BMP size\n");
+		return -EINVAL;
+	}
+	bmp_pitch = row_len & ~(size_t)3;
+
+	if (bmp_width > scanout->width || bmp_height > scanout->height) {
+		drm_err(client->dev, "splash: BMP image is too big for the screen\n");
+		return -EINVAL;
+	}
+
+	if (splash->use_bgrt) {
+		x_pad = efi_bgrt_image_offset_x();
+		y_pad = efi_bgrt_image_offset_y();
+
+		if (x_pad > scanout->width - bmp_width ||
+		    y_pad > scanout->height - bmp_height) {
+			drm_err(client->dev, "splash: BGRT image would overflow\n");
+			return -EINVAL;
+		}
+
+#ifdef CONFIG_X86
+		/*
+		 * BGRT sanity check, taken from efifb.c:
+		 *
+		 * Some firmware on x86 uses a non-native resolution for the
+		 * display if it has shown some text messages, while the BGRT
+		 * still describes the image for the native resolution. If
+		 * that image still fits, it ends up very close to the right
+		 * edge of the display, looking quite bad.
+		 */
+
+		if (x_pad != (scanout->width - bmp_width) / 2) {
+			drm_err(client->dev, "splash: BGRT sanity check failed\n");
+			return -EINVAL;
+		}
+#endif
+	} else {
+		/* Center X and Y */
+		x_pad = (scanout->width - bmp_width) / 2;
+		y_pad = (scanout->height - bmp_height) / 2;
+	}
+
+	image_data = data + bitmap_offset;
+
+	/* First the image upright in XRGB8888, then into the buffer's format */
+	img = kvmalloc_array(bmp_width, bmp_height * sizeof(*img), GFP_KERNEL);
+	if (!img)
+		return -ENOMEM;
+	drm_splash_bmp_decode(img, image_data, bmp_pitch, bmp_width, bmp_height,
+			      bmp_invert_y);
+
+	ret = drm_client_buffer_vmap_local(buffer, &map);
+	if (ret) {
+		drm_err(client->dev, "splash: cannot vmap buffer: %d\n", ret);
+		kvfree(img);
+		/* Like the allocation above, this may work on the next pass */
+		return -ENOMEM;
+	}
+
+	r = DRM_RECT_INIT(x_pad, y_pad, bmp_width, bmp_height);
+	drm_splash_blit(&map, fb->pitches[0], fb->format, x_pad, y_pad, img,
+			bmp_width, bmp_height);
+
+	kvfree(img);
+	drm_client_buffer_vunmap_local(buffer);
+
+	return drm_client_buffer_flush(buffer, &r);
+}
+#else
+static int drm_splash_bmp_to_scanout(struct drm_splash *splash,
+				     struct drm_splash_scanout *scanout,
+				     const u8 *data, size_t data_len)
+{
+	return -EOPNOTSUPP;
+}
+#endif
+
+static int drm_splash_image_load(struct drm_splash *splash, const u8 **img_data,
+				 size_t *img_data_len)
+{
+	if (splash->use_bgrt)
+		return drm_splash_bgrt_load(splash, img_data, img_data_len);
+
+	if (!IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_FW))
+		return -ENOENT;
+
+	return drm_splash_fw_load(splash, img_data, img_data_len);
+}
+
+/* At unregister, once nothing else can use the image */
+static void drm_splash_image_cleanup(struct drm_splash *splash)
+{
+	memunmap(splash->map_data);
+	splash->map_data = NULL;
+	release_firmware(splash->fw);
+	splash->fw = NULL;
+}
+
+/* Delete the buffers of an array of scanouts, and the array */
+static void drm_splash_delete_scanouts(struct drm_splash_scanout **scanout,
+				       u32 *n_scanout)
+{
+	unsigned int i;
+
+	for (i = 0; i < *n_scanout; i++)
+		drm_client_buffer_delete((*scanout)[i].buffer);
+
+	kfree(*scanout);
+	*scanout = NULL;
+	*n_scanout = 0;
+}
+
+static int drm_splash_commit(struct drm_splash *splash, bool force)
+{
+	struct drm_client_dev *client = &splash->client;
+	int ret;
+
+	if (force)
+		ret = drm_client_modeset_commit_locked(client);
+	else
+		ret = drm_client_modeset_commit(client);
+
+	/* Whatever a hotplug replaced is no longer on screen */
+	if (!ret)
+		drm_splash_delete_scanouts(&splash->old_scanout,
+					   &splash->n_old_scanout);
+
+	return ret;
+}
+
+static void drm_splash_rebuild(struct drm_splash *splash);
+
+static void drm_splash_work(struct work_struct *work)
+{
+	struct drm_splash *splash = container_of(work, struct drm_splash, work);
+	struct drm_client_dev *client = &splash->client;
+	const u8 *img_data = NULL;
+	size_t img_data_len = 0;
+	bool img_loaded, restore, force;
+	unsigned int i;
+	int ret;
+
+	guard(mutex)(&splash->lock);
+
+	/*
+	 * Not while the device sleeps, not even drawing, which flushes the
+	 * buffers to the hardware: resume queues the work again
+	 */
+	if (splash->suspended) {
+		splash->work_skipped = true;
+		return;
+	}
+
+	restore = splash->restore_pending;
+	force = splash->restore_force;
+	splash->restore_pending = false;
+	splash->restore_force = false;
+
+	/* The outputs a hotplug found while userspace had the display */
+	if (restore && splash->hotplug_pending) {
+		splash->hotplug_pending = false;
+		drm_splash_rebuild(splash);
+	}
+
+	/* A restore commits even with no scanouts: it switches the outputs off */
+	if (!splash->n_scanout && !restore)
+		return;
+
+	/* A BMP loaded as firmware may not be there yet: its callback requeues */
+	img_loaded = !drm_splash_image_load(splash, &img_data, &img_data_len);
+
+	for (i = 0; i < splash->n_scanout; i++) {
+		struct drm_splash_scanout *scanout = &splash->scanout[i];
+
+		/* Its outputs are off: init_client could not create it */
+		if (!scanout->buffer)
+			continue;
+
+		if (!scanout->bg_drawn) {
+			u32 rgb = CONFIG_DRM_CLIENT_SPLASH_BACKGROUND_COLOR;
+			u32 color = drm_draw_color_from_xrgb8888(rgb, scanout->format);
+
+			/* Only success marks it done: a later pass retries */
+			ret = drm_splash_fill_solid_color(scanout->buffer, color);
+			scanout->bg_drawn = !ret;
+			/* The fill covers the image too */
+			scanout->img_drawn = false;
+		}
+
+		if (img_loaded && !scanout->img_drawn) {
+			/*
+			 * A failure leaves the background alone. Only a lack of
+			 * memory can go away: anything else, the image or where
+			 * it goes, would fail the same way on every pass.
+			 */
+			ret = drm_splash_bmp_to_scanout(splash, scanout, img_data,
+							img_data_len);
+			scanout->img_drawn = ret != -ENOMEM;
+		}
+	}
+
+	/*
+	 * With another master, the display is not the splash's to show; the
+	 * buffers stay drawn for when restore hands it back.
+	 */
+	ret = drm_splash_commit(splash, force);
+	/* What a hotplug replaced is then still on screen: it stays */
+	if (ret)
+		drm_dbg(client->dev, "splash: commit: %d\n", ret);
+}
+
+/* No mode and no connectors: a commit switches the output off */
+static void drm_splash_modeset_off(struct drm_device *dev,
+				   struct drm_mode_set *modeset)
+{
+	unsigned int k;
+
+	for (k = 0; k < modeset->num_connectors; k++) {
+		drm_connector_put(modeset->connectors[k]);
+		modeset->connectors[k] = NULL;
+	}
+	modeset->num_connectors = 0;
+	drm_mode_destroy(dev, modeset->mode);
+	modeset->mode = NULL;
+	modeset->fb = NULL;
+}
+
+static int drm_splash_init_client(struct drm_splash *splash)
+{
+	struct drm_client_dev *client = &splash->client;
+	struct drm_mode_set *modeset;
+	unsigned int fb_count = 0;
+	unsigned int i, k;
+	int err = -ENODEV;
+
+	if (drm_client_modeset_probe(client, 0, 0))
+		return -EIO;
+
+	mutex_lock(&client->modeset_mutex);
+
+	/*
+	 * Collect one scanout per usable modeset, or per tile group. Each
+	 * scanout records the modesets it feeds by their position in the
+	 * list, so that the second pass below can find them again whatever
+	 * was skipped in between.
+	 */
+	i = 0;
+	drm_client_for_each_modeset(modeset, client) {
+		struct drm_splash_scanout *tmp;
+		struct drm_splash_scanout *scanout;
+		unsigned int idx = i++;
+		u32 format;
+		int id;
+
+		if (WARN_ON_ONCE(idx >= 32))
+			break;
+
+		/* Skip modesets without a mode */
+		if (!modeset->mode)
+			continue;
+
+		id = drm_splash_tile_group_id(modeset->connectors[0]);
+		if (id >= 0) {
+			struct drm_splash_scanout *tiled;
+
+			/*
+			 * Tiled modesets contribute to a single framebuffer,
+			 * which reaches the far edge of the last tile, whatever
+			 * order the tiles come in.
+			 */
+			tiled = get_scanout_from_tile_group(splash, id);
+			if (tiled) {
+				/* Left off unless its plane takes the group's format */
+				if (!drm_plane_has_format(modeset->crtc->primary,
+							  tiled->format,
+							  DRM_FORMAT_MOD_LINEAR)) {
+					drm_warn(client->dev,
+						 "splash: tile plane can't take %p4cc\n",
+						 &tiled->format);
+					continue;
+				}
+				tiled->width = max(tiled->width, modeset->x +
+						   modeset->mode->hdisplay);
+				tiled->height = max(tiled->height, modeset->y +
+						    modeset->mode->vdisplay);
+				tiled->modesets |= BIT(idx);
+				continue;
+			}
+		}
+
+		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\n");
+			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\n");
+			break;
+		}
+
+		splash->scanout = tmp;
+		scanout = &splash->scanout[splash->n_scanout];
+		splash->n_scanout++;
+
+		memset(scanout, 0, sizeof(*scanout));
+		scanout->id = id;
+		scanout->modesets = BIT(idx);
+		scanout->format = format;
+		scanout->width = modeset->x + modeset->mode->hdisplay;
+		scanout->height = modeset->y + modeset->mode->vdisplay;
+	}
+
+	/* Now that the sizes are known, allocate one buffer per scanout */
+	for (k = 0; k < splash->n_scanout; k++) {
+		struct drm_splash_scanout *scanout = &splash->scanout[k];
+		struct drm_client_buffer *buffer;
+
+		buffer = drm_client_buffer_create_dumb(client, scanout->width,
+						       scanout->height,
+						       scanout->format);
+		if (IS_ERR(buffer)) {
+			err = PTR_ERR(buffer);
+			drm_warn(client->dev,
+				 "splash: can't create dumb buffer %d %d %p4cc: %pe\n",
+				 scanout->width, scanout->height, &scanout->format,
+				 buffer);
+			continue;
+		}
+
+		drm_dbg(client->dev, "splash: created dumb buffer %d %d %p4cc\n",
+			scanout->width, scanout->height, &scanout->format);
+
+		scanout->buffer = buffer;
+		fb_count++;
+	}
+
+	/*
+	 * And point every modeset at the buffer of its scanout. One left
+	 * without, whatever the reason, is switched off: a mode without a
+	 * framebuffer fails the commit, and with it every other output.
+	 * Never the old one: a probe without connectors leaves it there.
+	 */
+	i = 0;
+	drm_client_for_each_modeset(modeset, client) {
+		unsigned int idx = i++;
+
+		modeset->fb = NULL;
+		for (k = 0; idx < 32 && k < splash->n_scanout; k++) {
+			struct drm_splash_scanout *scanout = &splash->scanout[k];
+
+			if (scanout->buffer && (scanout->modesets & BIT(idx))) {
+				modeset->fb = scanout->buffer->fb;
+				break;
+			}
+		}
+
+		if (!modeset->fb)
+			drm_splash_modeset_off(client->dev, modeset);
+	}
+
+	mutex_unlock(&client->modeset_mutex);
+
+	/* -ENODEV without a usable modeset, which hotplug only logs */
+	return fb_count ? 0 : err;
+}
+
+static void drm_splash_free_scanout(struct drm_client_dev *client)
+{
+	struct drm_splash *splash = client_to_drm_splash(client);
+	struct drm_mode_set *modeset;
+
+	/*
+	 * The modesets must not point at the framebuffers freed below, nor
+	 * keep a mode without one, which would fail every later commit
+	 */
+	mutex_lock(&client->modeset_mutex);
+	drm_client_for_each_modeset(modeset, client)
+		drm_splash_modeset_off(client->dev, modeset);
+	mutex_unlock(&client->modeset_mutex);
+
+	drm_splash_delete_scanouts(&splash->scanout, &splash->n_scanout);
+}
+
+/* Under the lock: set the scanouts up again, for the work to draw */
+static void drm_splash_rebuild(struct drm_splash *splash)
+{
+	struct drm_client_dev *client = &splash->client;
+	int ret;
+
+	/*
+	 * Every hotplug starts over, with the outputs there are now. The
+	 * scanouts on screen stay there until the work or restore has
+	 * committed the new ones; with some still waiting for that, the
+	 * current ones were never committed and can go.
+	 */
+	if (splash->old_scanout) {
+		drm_splash_free_scanout(client);
+	} else {
+		splash->old_scanout = splash->scanout;
+		splash->n_old_scanout = splash->n_scanout;
+		splash->scanout = NULL;
+		splash->n_scanout = 0;
+	}
+
+	ret = drm_splash_init_client(splash);
+	if (ret) {
+		/*
+		 * It may have set up scanouts before failing to get buffers.
+		 * Those on screen stay there, for the next hotplug to replace.
+		 */
+		drm_splash_free_scanout(client);
+		if (ret == -ENODEV)
+			drm_dbg(client->dev, "splash: no usable modeset\n");
+		else
+			drm_err(client->dev,
+				"splash: failed to init client: %d\n", ret);
+
+		return;
+	}
+
+	if (!splash->source_picked) {
+		splash->source_picked = true;
+		splash->use_bgrt = drm_splash_bgrt_available(splash);
+
+		/* If no other image has been loaded, try to load a BMP as firmware */
+		if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_FW) && !splash->use_bgrt) {
+			ret = drm_splash_fw_request(splash);
+			splash->fw_requested = !ret;
+			if (ret)
+				drm_err(client->dev,
+					"splash: failed to kick image load: %d\n", ret);
+		}
+	}
+}
+
+static int drm_splash_client_hotplug(struct drm_client_dev *client)
+{
+	struct drm_splash *splash = client_to_drm_splash(client);
+
+	guard(mutex)(&splash->lock);
+
+	/*
+	 * As fbdev: with another master, nothing the splash draws can be
+	 * shown, so the outputs are set up again only at the next restore
+	 */
+	if (!drm_master_internal_acquire(client->dev)) {
+		splash->hotplug_pending = true;
+		return 0;
+	}
+	drm_master_internal_release(client->dev);
+
+	splash->hotplug_pending = false;
+	drm_splash_rebuild(splash);
+	drm_splash_queue(splash);
+
+	/* An error would stop the hotplug events: let the next one retry */
+	return 0;
+}
+
+/*
+ * The work draws whatever is missing and commits, but not while the
+ * device sleeps. An output without a buffer has no mode either: the commit
+ * switches it off.
+ */
+static int drm_splash_client_restore(struct drm_client_dev *client, bool force)
+{
+	struct drm_splash *splash = client_to_drm_splash(client);
+
+	guard(mutex)(&splash->lock);
+	splash->restore_pending = true;
+	splash->restore_force |= force;
+	drm_splash_queue(splash);
+
+	return 0;
+}
+
+static void drm_splash_client_unregister(struct drm_client_dev *client)
+{
+	struct drm_splash *splash = client_to_drm_splash(client);
+
+	/*
+	 * Hotplug, restore and resume no longer come. The work can make a
+	 * firmware request too, so it goes first.
+	 */
+	cancel_work_sync(&splash->work);
+
+	/*
+	 * A firmware request cannot be cancelled, and its callback uses the
+	 * splash and queues the work: let it finish first. It is usually over
+	 * already; at worst this waits for the firmware fallback timeout,
+	 * with clientlist_mutex held.
+	 */
+	if (splash->fw_requested) {
+		wait_for_completion(&splash->fw_done);
+		cancel_work_sync(&splash->work);
+	}
+	drm_splash_free_scanout(client);
+	drm_splash_delete_scanouts(&splash->old_scanout, &splash->n_old_scanout);
+	drm_splash_image_cleanup(splash);
+	mutex_destroy(&splash->lock);
+
+	/* Last: it ends up in drm_splash_client_free(), which frees splash */
+	drm_client_release(client);
+}
+
+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;
+
+	kfree(splash);
+
+	drm_dbg(dev, "Freed drm splash\n");
+}
+
+static int drm_splash_client_suspend(struct drm_client_dev *client)
+{
+	struct drm_splash *splash = client_to_drm_splash(client);
+
+	guard(mutex)(&splash->lock);
+	splash->suspended = true;
+
+	return 0;
+}
+
+static int drm_splash_client_resume(struct drm_client_dev *client)
+{
+	struct drm_splash *splash = client_to_drm_splash(client);
+
+	guard(mutex)(&splash->lock);
+	splash->suspended = false;
+	if (splash->work_skipped) {
+		splash->work_skipped = false;
+		drm_splash_queue(splash);
+	}
+
+	return 0;
+}
+
+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,
+	.suspend	= drm_splash_client_suspend,
+	.resume		= drm_splash_client_resume,
+};
+
+/**
+ * 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_obj(*splash);
+	if (!splash)
+		goto err_warn;
+
+	INIT_WORK(&splash->work, drm_splash_work);
+	mutex_init(&splash->lock);
+	init_completion(&splash->fw_done);
+
+	if (format && format->num_planes == 1) {
+		splash->preferred_format = format->format;
+	} else {
+		unsigned int mode = dev->mode_config.preferred_depth;
+
+		/* As fbdev: 32 bits per pixel, unless the driver wants fewer */
+		if (!mode || mode == 24)
+			mode = 32;
+		splash->preferred_format =
+			drm_driver_color_mode_format(dev, mode);
+	}
+
+	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\n");
+
+	return;
+
+err_free:
+	mutex_destroy(&splash->lock);
+	kfree(splash);
+err_warn:
+	drm_warn(dev, "Failed to register with drm splash\n");
+}
-- 
2.39.5


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v7 5/6] drm: docs: remove bootsplash from TODO
  2026-11-16  0:21 [PATCH v7 0/6] Add splash DRM client Màxim Pedraza Padilla
                   ` (3 preceding siblings ...)
  2026-11-16  0:21 ` [PATCH v7 4/6] drm: client: add splash client Màxim Pedraza Padilla
@ 2026-11-16  0:21 ` Màxim Pedraza Padilla
  2026-11-16  0:21 ` [PATCH v7 6/6] drm/client: splash: pick the image source in one place Màxim Pedraza Padilla
  5 siblings, 0 replies; 7+ messages in thread
From: Màxim Pedraza Padilla @ 2026-11-16  0:21 UTC (permalink / raw)
  To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Ard Biesheuvel, Jonathan Corbet, Helge Deller,
	Peter Jones
  Cc: Francesco Valla, Jani Nikula, Mario Limonciello,
	Javier Martinez Canillas, Jocelyn Falempe, Sam Ravnborg,
	Ilias Apalodimas, Shuah Khan, Randy Dunlap, dri-devel, linux-efi,
	linux-fbdev, linux-doc, linux-embedded, linux-kernel

From: Francesco Valla <francesco@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>
Reviewed-by: Thomas Zimmermann <tzimmermann@suse.de>
Signed-off-by: Màxim Pedraza Padilla <maximpedraza@gmail.com>
---
 Documentation/gpu/todo.rst | 17 -----------------
 1 file changed, 17 deletions(-)

diff --git a/Documentation/gpu/todo.rst b/Documentation/gpu/todo.rst
index e6d36f8da086..5e987e18bfcd 100644
--- a/Documentation/gpu/todo.rst
+++ b/Documentation/gpu/todo.rst
@@ -756,23 +756,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.39.5


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v7 6/6] drm/client: splash: pick the image source in one place
  2026-11-16  0:21 [PATCH v7 0/6] Add splash DRM client Màxim Pedraza Padilla
                   ` (4 preceding siblings ...)
  2026-11-16  0:21 ` [PATCH v7 5/6] drm: docs: remove bootsplash from TODO Màxim Pedraza Padilla
@ 2026-11-16  0:21 ` Màxim Pedraza Padilla
  5 siblings, 0 replies; 7+ messages in thread
From: Màxim Pedraza Padilla @ 2026-11-16  0:21 UTC (permalink / raw)
  To: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, Ard Biesheuvel, Jonathan Corbet, Helge Deller,
	Peter Jones
  Cc: Francesco Valla, Jani Nikula, Mario Limonciello,
	Javier Martinez Canillas, Jocelyn Falempe, Sam Ravnborg,
	Ilias Apalodimas, Shuah Khan, Randy Dunlap, dri-devel, linux-efi,
	linux-fbdev, linux-doc, linux-embedded, linux-kernel

The client chooses between two sources with a boolean: the BGRT if the
firmware left one, otherwise the BMP loaded as firmware. A third one is
coming, and with it an order of preference that a boolean cannot hold.

Name the source in use with an enum, picked once by
drm_splash_pick_source(), and have drm_splash_image_load() switch on
it. No functional change.

v7:
- DRM_SPLASH_SRC_FW for DRM_SPLASH_SRC_BMP: the BGRT is a BMP too

Suggested-by: Thomas Zimmermann <tzimmermann@suse.de>
Signed-off-by: Màxim Pedraza Padilla <maximpedraza@gmail.com>
---
 drivers/gpu/drm/clients/drm_splash.c | 46 ++++++++++++++++++++++------
 1 file changed, 37 insertions(+), 9 deletions(-)

diff --git a/drivers/gpu/drm/clients/drm_splash.c b/drivers/gpu/drm/clients/drm_splash.c
index d54cf628cf17..fbbdc034e417 100644
--- a/drivers/gpu/drm/clients/drm_splash.c
+++ b/drivers/gpu/drm/clients/drm_splash.c
@@ -39,6 +39,16 @@
  * a static image.
  */
 
+/*
+ * Where the image comes from. drm_splash_pick_source() sets the order of
+ * preference; NONE is zero, so that a new splash starts without a source.
+ */
+enum drm_splash_source {
+	DRM_SPLASH_SRC_NONE,
+	DRM_SPLASH_SRC_BGRT,
+	DRM_SPLASH_SRC_FW,
+};
+
 struct drm_splash_scanout {
 	int id;
 	/* Modesets scanning this buffer out, by position in the list */
@@ -91,7 +101,7 @@ struct drm_splash {
 	const struct firmware *fw;
 	void *map_data;
 
-	bool use_bgrt;
+	enum drm_splash_source src;
 };
 
 static struct drm_splash *client_to_drm_splash(struct drm_client_dev *client)
@@ -439,7 +449,7 @@ static int drm_splash_bmp_to_scanout(struct drm_splash *splash,
 		return -EINVAL;
 	}
 
-	if (splash->use_bgrt) {
+	if (splash->src == DRM_SPLASH_SRC_BGRT) {
 		x_pad = efi_bgrt_image_offset_x();
 		y_pad = efi_bgrt_image_offset_y();
 
@@ -509,13 +519,31 @@ static int drm_splash_bmp_to_scanout(struct drm_splash *splash,
 static int drm_splash_image_load(struct drm_splash *splash, const u8 **img_data,
 				 size_t *img_data_len)
 {
-	if (splash->use_bgrt)
+	switch (splash->src) {
+	case DRM_SPLASH_SRC_BGRT:
 		return drm_splash_bgrt_load(splash, img_data, img_data_len);
+	case DRM_SPLASH_SRC_FW:
+		return drm_splash_fw_load(splash, img_data, img_data_len);
+	case DRM_SPLASH_SRC_NONE:
+		break;
+	}
 
-	if (!IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_FW))
-		return -ENOENT;
+	return -ENOENT;
+}
+
+/*
+ * Pick the one source the image will come from: the BGRT is whatever the
+ * firmware shows, and the BMP loaded as firmware is the fallback. With
+ * neither, only the background colour is drawn.
+ */
+static enum drm_splash_source drm_splash_pick_source(struct drm_splash *splash)
+{
+	if (drm_splash_bgrt_available(splash))
+		return DRM_SPLASH_SRC_BGRT;
+	if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_FW))
+		return DRM_SPLASH_SRC_FW;
 
-	return drm_splash_fw_load(splash, img_data, img_data_len);
+	return DRM_SPLASH_SRC_NONE;
 }
 
 /* At unregister, once nothing else can use the image */
@@ -858,10 +886,10 @@ static void drm_splash_rebuild(struct drm_splash *splash)
 
 	if (!splash->source_picked) {
 		splash->source_picked = true;
-		splash->use_bgrt = drm_splash_bgrt_available(splash);
+		splash->src = drm_splash_pick_source(splash);
 
-		/* If no other image has been loaded, try to load a BMP as firmware */
-		if (IS_ENABLED(CONFIG_DRM_CLIENT_SPLASH_SRC_FW) && !splash->use_bgrt) {
+		/* The BMP loaded as firmware is the only source that arrives later */
+		if (splash->src == DRM_SPLASH_SRC_FW) {
 			ret = drm_splash_fw_request(splash);
 			splash->fw_requested = !ret;
 			if (ret)
-- 
2.39.5


^ permalink raw reply related	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-11-15 23:22 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-16  0:21 [PATCH v7 0/6] Add splash DRM client Màxim Pedraza Padilla
2026-11-16  0:21 ` [PATCH v7 1/6] drm/clients: Kconfig: indent DRM_CLIENT_DEFAULT with tabs Màxim Pedraza Padilla
2026-11-16  0:21 ` [PATCH v7 2/6] efi: bgrt: add accessors for the boot graphics Màxim Pedraza Padilla
2026-11-16  0:21 ` [PATCH v7 3/6] fbdev: efifb: move the BMP headers to <video/bmp.h> Màxim Pedraza Padilla
2026-11-16  0:21 ` [PATCH v7 4/6] drm: client: add splash client Màxim Pedraza Padilla
2026-11-16  0:21 ` [PATCH v7 5/6] drm: docs: remove bootsplash from TODO Màxim Pedraza Padilla
2026-11-16  0:21 ` [PATCH v7 6/6] drm/client: splash: pick the image source in one place Màxim Pedraza Padilla

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox