diff --git a/MAINTAINERS b/MAINTAINERS
index b5633b56391e..5c237445761e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -2712,6 +2712,7 @@ S:	Maintained
 F:	drivers/video/fbdev/core/bootsplash*.*
 F:	drivers/video/fbdev/core/dummycon.c
 F:	include/linux/bootsplash.h
+F:	include/uapi/linux/bootsplash_file.h
 
 BPF (Safe dynamic programs and tools)
 M:	Alexei Starovoitov <ast@kernel.org>
diff --git a/drivers/video/fbdev/core/Makefile b/drivers/video/fbdev/core/Makefile
index 66895321928e..6a8d1bab8a01 100644
--- a/drivers/video/fbdev/core/Makefile
+++ b/drivers/video/fbdev/core/Makefile
@@ -31,4 +31,4 @@ obj-$(CONFIG_FB_SVGALIB)       += svgalib.o
 obj-$(CONFIG_FB_DDC)           += fb_ddc.o
 
 obj-$(CONFIG_BOOTSPLASH)       += bootsplash.o bootsplash_render.o \
-                                  dummyblit.o
+                                  bootsplash_load.o dummyblit.o
diff --git a/drivers/video/fbdev/core/bootsplash.c b/drivers/video/fbdev/core/bootsplash.c
index e449755af268..843c5400fefc 100644
--- a/drivers/video/fbdev/core/bootsplash.c
+++ b/drivers/video/fbdev/core/bootsplash.c
@@ -32,6 +32,7 @@
 #include <linux/workqueue.h>
 
 #include "bootsplash_internal.h"
+#include "uapi/linux/bootsplash_file.h"
 
 
 /*
@@ -102,10 +103,17 @@ static bool is_fb_compatible(const struct fb_info *info)
  */
 void bootsplash_render_full(struct fb_info *info)
 {
+	mutex_lock(&splash_state.data_lock);
+
 	if (!is_fb_compatible(info))
-		return;
+		goto out;
+
+	bootsplash_do_render_background(info, splash_state.file);
+
+	bootsplash_do_render_pictures(info, splash_state.file);
 
-	bootsplash_do_render_background(info);
+out:
+	mutex_unlock(&splash_state.data_lock);
 }
 
 
@@ -116,6 +124,7 @@ bool bootsplash_would_render_now(void)
 {
 	return !oops_in_progress
 		&& !console_blanked
+		&& splash_state.file
 		&& bootsplash_is_enabled();
 }
 
@@ -252,6 +261,7 @@ static struct platform_driver splash_driver = {
 void bootsplash_init(void)
 {
 	int ret;
+	struct splash_file_priv *fp;
 
 	/* Initialized already? */
 	if (splash_state.splash_device)
@@ -280,8 +290,26 @@ void bootsplash_init(void)
 	}
 
 
+	mutex_init(&splash_state.data_lock);
+	set_bit(0, &splash_state.enabled);
+
 	INIT_WORK(&splash_state.work_redraw_vc, splash_callback_redraw_vc);
 
+
+	if (!splash_state.bootfile || !strlen(splash_state.bootfile))
+		return;
+
+	fp = bootsplash_load_firmware(&splash_state.splash_device->dev,
+				      splash_state.bootfile);
+
+	if (!fp)
+		goto err;
+
+	mutex_lock(&splash_state.data_lock);
+	splash_state.splash_fb = NULL;
+	splash_state.file = fp;
+	mutex_unlock(&splash_state.data_lock);
+
 	return;
 
 err_device:
@@ -292,3 +320,7 @@ void bootsplash_init(void)
 err:
 	pr_err("Failed to initialize.\n");
 }
+
+
+module_param_named(bootfile, splash_state.bootfile, charp, 0444);
+MODULE_PARM_DESC(bootfile, "Bootsplash file to load on boot");
diff --git a/drivers/video/fbdev/core/bootsplash_internal.h b/drivers/video/fbdev/core/bootsplash_internal.h
index b11da5cb90bf..71e2a27ac0b8 100644
--- a/drivers/video/fbdev/core/bootsplash_internal.h
+++ b/drivers/video/fbdev/core/bootsplash_internal.h
@@ -15,15 +15,43 @@
 
 #include <linux/types.h>
 #include <linux/fb.h>
+#include <linux/firmware.h>
 #include <linux/kernel.h>
 #include <linux/mutex.h>
 #include <linux/spinlock.h>
 
+#include "uapi/linux/bootsplash_file.h"
+
 
 /*
  * Runtime types
  */
+struct splash_blob_priv {
+	struct splash_blob_header *blob_header;
+	const void *data;
+};
+
+
+struct splash_pic_priv {
+	const struct splash_pic_header *pic_header;
+
+	struct splash_blob_priv *blobs;
+	u16 blobs_loaded;
+};
+
+
+struct splash_file_priv {
+	const struct firmware *fw;
+	const struct splash_file_header *header;
+
+	struct splash_pic_priv *pics;
+};
+
+
 struct splash_priv {
+	/* Bootup and runtime state */
+	char *bootfile;
+
 	/*
 	 * Enabled/disabled state, to be used with atomic bit operations.
 	 *   Bit 0: 0 = Splash hidden
@@ -43,6 +71,13 @@ struct splash_priv {
 	struct platform_device *splash_device;
 
 	struct work_struct work_redraw_vc;
+
+	/* Splash data structures including lock for everything below */
+	struct mutex data_lock;
+
+	struct fb_info *splash_fb;
+
+	struct splash_file_priv *file;
 };
 
 
@@ -50,6 +85,14 @@ struct splash_priv {
 /*
  * Rendering functions
  */
-void bootsplash_do_render_background(struct fb_info *info);
+void bootsplash_do_render_background(struct fb_info *info,
+				     const struct splash_file_priv *fp);
+void bootsplash_do_render_pictures(struct fb_info *info,
+				   const struct splash_file_priv *fp);
+
+
+void bootsplash_free_file(struct splash_file_priv *fp);
+struct splash_file_priv *bootsplash_load_firmware(struct device *device,
+						  const char *path);
 
 #endif
diff --git a/drivers/video/fbdev/core/bootsplash_load.c b/drivers/video/fbdev/core/bootsplash_load.c
new file mode 100644
index 000000000000..fd807571ab7d
--- /dev/null
+++ b/drivers/video/fbdev/core/bootsplash_load.c
@@ -0,0 +1,225 @@
+/*
+ * Kernel based bootsplash.
+ *
+ * (Loading and freeing functions)
+ *
+ * Authors:
+ * Max Staudt <mstaudt@suse.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0
+ */
+
+#define pr_fmt(fmt) "bootsplash: " fmt
+
+
+#include <linux/bootsplash.h>
+#include <linux/fb.h>
+#include <linux/firmware.h>
+#include <linux/kernel.h>
+#include <linux/mutex.h>
+#include <linux/printk.h>
+#include <linux/types.h>
+#include <linux/vmalloc.h>
+
+#include "bootsplash_internal.h"
+#include "uapi/linux/bootsplash_file.h"
+
+
+
+
+/*
+ * Free all vmalloc()'d resources describing a splash file.
+ */
+void bootsplash_free_file(struct splash_file_priv *fp)
+{
+	if (!fp)
+		return;
+
+	if (fp->pics) {
+		unsigned int i;
+
+		for (i = 0; i < fp->header->num_pics; i++) {
+			struct splash_pic_priv *pp = &fp->pics[i];
+
+			if (pp->blobs)
+				vfree(pp->blobs);
+		}
+
+		vfree(fp->pics);
+	}
+
+	release_firmware(fp->fw);
+	vfree(fp);
+}
+
+
+
+
+/*
+ * Load a splash screen from a "firmware" file.
+ *
+ * Parsing, and sanity checks.
+ */
+#ifdef __BIG_ENDIAN
+	#define BOOTSPLASH_MAGIC BOOTSPLASH_MAGIC_BE
+#else
+	#define BOOTSPLASH_MAGIC BOOTSPLASH_MAGIC_LE
+#endif
+
+struct splash_file_priv *bootsplash_load_firmware(struct device *device,
+						  const char *path)
+{
+	const struct firmware *fw;
+	struct splash_file_priv *fp;
+	unsigned int i;
+	const u8 *walker;
+
+	if (request_firmware(&fw, path, device))
+		return NULL;
+
+	if (fw->size < sizeof(struct splash_file_header)
+	    || memcmp(fw->data, BOOTSPLASH_MAGIC, sizeof(fp->header->id))) {
+		pr_err("Not a bootsplash file.\n");
+
+		release_firmware(fw);
+		return NULL;
+	}
+
+	fp = vzalloc(sizeof(struct splash_file_priv));
+	if (!fp) {
+		release_firmware(fw);
+		return NULL;
+	}
+
+	pr_info("Loading splash file (%li bytes)\n", fw->size);
+
+	fp->fw = fw;
+	fp->header = (struct splash_file_header *)fw->data;
+
+	/* Sanity checks */
+	if (fp->header->version != BOOTSPLASH_VERSION) {
+		pr_err("Loaded v%d file, but we only support version %d\n",
+			fp->header->version,
+			BOOTSPLASH_VERSION);
+
+		goto err;
+	}
+
+	if (fw->size < sizeof(struct splash_file_header)
+		+ fp->header->num_pics
+			* sizeof(struct splash_pic_header)
+		+ fp->header->num_blobs
+			* sizeof(struct splash_blob_header)) {
+		pr_err("File incomplete.\n");
+
+		goto err;
+	}
+
+	/* Read picture headers */
+	if (fp->header->num_pics) {
+		fp->pics = vzalloc(fp->header->num_pics
+				   * sizeof(struct splash_pic_priv));
+		if (!fp->pics)
+			goto err;
+	}
+
+	walker = fw->data + sizeof(struct splash_file_header);
+	for (i = 0; i < fp->header->num_pics; i++) {
+		struct splash_pic_priv *pp = &fp->pics[i];
+		struct splash_pic_header *ph = (void *)walker;
+
+		pr_debug("Picture %u: Size %ux%u\n", i, ph->width, ph->height);
+
+		if (ph->num_blobs < 1) {
+			pr_err("Picture %u: Zero blobs? Aborting load.\n", i);
+			goto err;
+		}
+
+		pp->pic_header = ph;
+		pp->blobs = vzalloc(ph->num_blobs
+					* sizeof(struct splash_blob_priv));
+		if (!pp->blobs)
+			goto err;
+
+		walker += sizeof(struct splash_pic_header);
+	}
+
+	/* Read blob headers */
+	for (i = 0; i < fp->header->num_blobs; i++) {
+		struct splash_blob_header *bh = (void *)walker;
+		struct splash_pic_priv *pp;
+
+		if (walker + sizeof(struct splash_blob_header)
+		    > fw->data + fw->size)
+			goto err;
+
+		walker += sizeof(struct splash_blob_header);
+
+		if (walker + bh->length > fw->data + fw->size)
+			goto err;
+
+		if (bh->picture_id >= fp->header->num_pics)
+			goto nextblob;
+
+		pp = &fp->pics[bh->picture_id];
+
+		pr_debug("Blob %u, pic %u, blobs_loaded %u, num_blobs %u.\n",
+			 i, bh->picture_id,
+			 pp->blobs_loaded, pp->pic_header->num_blobs);
+
+		if (pp->blobs_loaded >= pp->pic_header->num_blobs)
+			goto nextblob;
+
+		switch (bh->type) {
+		case 0:
+			/* Raw 24-bit packed pixels */
+			if (bh->length != pp->pic_header->width
+					* pp->pic_header->height * 3) {
+				pr_err("Blob %u, type 1: Length doesn't match picture.\n",
+				       i);
+
+				goto err;
+			}
+			break;
+		default:
+			pr_warn("Blob %u, unknown type %u.\n", i, bh->type);
+			goto nextblob;
+		}
+
+		pp->blobs[pp->blobs_loaded].blob_header = bh;
+		pp->blobs[pp->blobs_loaded].data = walker;
+		pp->blobs_loaded++;
+
+nextblob:
+		walker += bh->length;
+		if (bh->length % 16)
+			walker += 16 - (bh->length % 16);
+	}
+
+	if (walker != fw->data + fw->size)
+		pr_warn("Trailing data in splash file.\n");
+
+	/* Walk over pictures and ensure all blob slots are filled */
+	for (i = 0; i < fp->header->num_pics; i++) {
+		struct splash_pic_priv *pp = &fp->pics[i];
+
+		if (pp->blobs_loaded != pp->pic_header->num_blobs) {
+			pr_err("Picture %u doesn't have all blob slots filled.\n",
+			       i);
+
+			goto err;
+		}
+	}
+
+	pr_info("Loaded (%ld bytes, %u pics, %u blobs).\n",
+		fw->size,
+		fp->header->num_pics,
+		fp->header->num_blobs);
+
+	return fp;
+
+
+err:
+	bootsplash_free_file(fp);
+	return NULL;
+}
diff --git a/drivers/video/fbdev/core/bootsplash_render.c b/drivers/video/fbdev/core/bootsplash_render.c
index 4d7e0117f653..2ae36949d0e3 100644
--- a/drivers/video/fbdev/core/bootsplash_render.c
+++ b/drivers/video/fbdev/core/bootsplash_render.c
@@ -19,6 +19,7 @@
 #include <linux/types.h>
 
 #include "bootsplash_internal.h"
+#include "uapi/linux/bootsplash_file.h"
 
 
 
@@ -70,16 +71,69 @@ static inline u32 pack_pixel(const struct fb_var_screeninfo *dst_var,
 }
 
 
-void bootsplash_do_render_background(struct fb_info *info)
+/*
+ * Copy from source and blend into the destination picture.
+ * Currently assumes that the source picture is 24bpp.
+ * Currently assumes that the destination is <= 32bpp.
+ */
+static int splash_convert_to_fb(u8 *dst,
+				const struct fb_var_screeninfo *dst_var,
+				unsigned int dst_stride,
+				unsigned int dst_xoff,
+				unsigned int dst_yoff,
+				const u8 *src,
+				unsigned int src_width,
+				unsigned int src_height)
+{
+	unsigned int x, y;
+	unsigned int src_stride = 3 * src_width; /* Assume 24bpp packed */
+	u32 dst_octpp = dst_var->bits_per_pixel / 8;
+
+	dst_xoff += dst_var->xoffset;
+	dst_yoff += dst_var->yoffset;
+
+	/* Copy with stride and pixel size adjustment */
+	for (y = 0;
+	     y < src_height && y + dst_yoff < dst_var->yres_virtual;
+	     y++) {
+		const u8 *srcline = src + (y * src_stride);
+		u8 *dstline = dst + ((y + dst_yoff) * dst_stride)
+				  + (dst_xoff * dst_octpp);
+
+		for (x = 0;
+		     x < src_width && x + dst_xoff < dst_var->xres_virtual;
+		     x++) {
+			u8 red, green, blue;
+			u32 dstpix;
+
+			/* Read pixel */
+			red = *srcline++;
+			green = *srcline++;
+			blue = *srcline++;
+
+			/* Write pixel */
+			dstpix = pack_pixel(dst_var, red, green, blue);
+			memcpy(dstline, &dstpix, dst_octpp);
+
+			dstline += dst_octpp;
+		}
+	}
+
+	return 0;
+}
+
+
+void bootsplash_do_render_background(struct fb_info *info,
+				     const struct splash_file_priv *fp)
 {
 	unsigned int x, y;
 	u32 dstpix;
 	u32 dst_octpp = info->var.bits_per_pixel / 8;
 
 	dstpix = pack_pixel(&info->var,
-			    0,
-			    0,
-			    0);
+			    fp->header->bg_red,
+			    fp->header->bg_green,
+			    fp->header->bg_blue);
 
 	for (y = 0; y < info->var.yres_virtual; y++) {
 		u8 *dstline = info->screen_buffer + (y * info->fix.line_length);
@@ -91,3 +145,44 @@ void bootsplash_do_render_background(struct fb_info *info)
 		}
 	}
 }
+
+
+void bootsplash_do_render_pictures(struct fb_info *info,
+				   const struct splash_file_priv *fp)
+{
+	unsigned int i;
+
+	for (i = 0; i < fp->header->num_pics; i++) {
+		struct splash_blob_priv *bp;
+		struct splash_pic_priv *pp = &fp->pics[i];
+		long dst_xoff, dst_yoff;
+
+		if (pp->blobs_loaded < 1)
+			continue;
+
+		bp = &pp->blobs[0];
+
+		if (!bp || bp->blob_header->type != 0)
+			continue;
+
+		dst_xoff = (info->var.xres - pp->pic_header->width) / 2;
+		dst_yoff = (info->var.yres - pp->pic_header->height) / 2;
+
+		if (dst_xoff < 0
+		    || dst_yoff < 0
+		    || dst_xoff + pp->pic_header->width > info->var.xres
+		    || dst_yoff + pp->pic_header->height > info->var.yres) {
+			pr_info_once("Picture %u is out of bounds at current resolution: %dx%d\n"
+				     "(this will only be printed once every reboot)\n",
+				     i, info->var.xres, info->var.yres);
+
+			continue;
+		}
+
+		/* Draw next splash frame */
+		splash_convert_to_fb(info->screen_buffer, &info->var,
+				info->fix.line_length, dst_xoff, dst_yoff,
+				bp->data,
+				pp->pic_header->width, pp->pic_header->height);
+	}
+}
diff --git a/include/uapi/linux/bootsplash_file.h b/include/uapi/linux/bootsplash_file.h
new file mode 100644
index 000000000000..89dc9cca8f0c
--- /dev/null
+++ b/include/uapi/linux/bootsplash_file.h
@@ -0,0 +1,118 @@
+/*
+ * Kernel based bootsplash.
+ *
+ * (File format)
+ *
+ * Authors:
+ * Max Staudt <mstaudt@suse.de>
+ *
+ * SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note
+ */
+
+#ifndef __BOOTSPLASH_FILE_H
+#define __BOOTSPLASH_FILE_H
+
+
+#define BOOTSPLASH_VERSION 55561
+
+
+#include <linux/kernel.h>
+#include <linux/types.h>
+
+
+/*
+ * On-disk types
+ *
+ * A splash file consists of:
+ *  - One single 'struct splash_file_header'
+ *  - An array of 'struct splash_pic_header'
+ *  - An array of raw data blocks, each padded to 16 bytes and
+ *    preceded by a 'struct splash_blob_header'
+ *
+ * A single-frame splash may look like this:
+ *
+ * +--------------------+
+ * |                    |
+ * | splash_file_header |
+ * |  -> num_blobs = 1  |
+ * |  -> num_pics = 1   |
+ * |                    |
+ * +--------------------+
+ * |                    |
+ * | splash_pic_header  |
+ * |                    |
+ * +--------------------+
+ * |                    |
+ * | splash_blob_header |
+ * |  -> type = 0       |
+ * |  -> picture_id = 0 |
+ * |                    |
+ * | (raw RGB data)     |
+ * | (pad to 16 bytes)  |
+ * |                    |
+ * +--------------------+
+ *
+ * All multi-byte values are stored on disk in the native format
+ * expected by the system the file will be used on.
+ */
+#define BOOTSPLASH_MAGIC_BE "Linux bootsplash"
+#define BOOTSPLASH_MAGIC_LE "hsalpstoob xuniL"
+
+struct splash_file_header {
+	uint8_t  id[16]; /* "Linux bootsplash" (no trailing NUL) */
+
+	/* Splash file format version to avoid clashes */
+	uint16_t version;
+
+	/* The background color */
+	uint8_t bg_red;
+	uint8_t bg_green;
+	uint8_t bg_blue;
+	uint8_t bg_reserved;
+
+	/*
+	 * Number of pic/blobs so we can allocate memory for internal
+	 * structures ahead of time when reading the file
+	 */
+	uint16_t num_blobs;
+	uint8_t num_pics;
+
+	uint8_t padding[103];
+} __attribute__((__packed__));
+
+
+struct splash_pic_header {
+	uint16_t width;
+	uint16_t height;
+
+	/*
+	 * Number of data packages associated with this picture.
+	 * Currently, the only use for more than 1 is for animations.
+	 */
+	uint8_t num_blobs;
+
+	uint8_t padding[27];
+} __attribute__((__packed__));
+
+
+struct splash_blob_header {
+	/* Length of the data block in bytes. */
+	uint32_t length;
+
+	/*
+	 * Type of the contents.
+	 *  0 - Raw RGB data.
+	 */
+	uint16_t type;
+
+	/*
+	 * Picture this blob is associated with.
+	 * Blobs will be added to a picture in the order they are
+	 * found in the file.
+	 */
+	uint8_t picture_id;
+
+	uint8_t padding[9];
+} __attribute__((__packed__));
+
+#endif
