* [PATCH v3 0/7] Boot logo supplied by the device tree
@ 2026-09-23 20:10 Max Pedraza
2026-09-23 20:10 ` [PATCH v3 1/7] fbdev: describe where the boot logo goes in one place Max Pedraza
` (6 more replies)
0 siblings, 7 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
Embedded products routinely need their own boot logo. Today that means
pointing CONFIG_LOGO_LINUX_CLUT224_FILE at a different image, which bakes
it into the kernel image. Two products that share a board support package
but differ in branding therefore need two kernel builds, and rebranding an
existing product means rebuilding and requalifying a kernel for what is
purely a cosmetic change.
This series lets the logo be described by the device tree instead: a node
compatible with "boot-logo-clut224" under /chosen supplies the image in
the same paletted format the built-in CLUT224 logos already use, and the
kernel prefers it over the built-in ones when it is present and enabled.
If the node is absent or disabled, nothing changes.
The image can come from the node itself (patches 2-3) or from a reserved
memory region the bootloader filled in (patches 5-6), because the image
and its placement are independent axes of variation. One board sold to
several customers wants several device trees differing in the logo. One
customer with several products built on that board, with different panels,
wants the same logo placed differently on each: there the image belongs in
a shared binary and only the placement belongs in the device tree.
Patch 1 is a cleanup that stands on its own. fb_prepare_logo() works out
how many rows to keep clear for the logo and fb_show_logo_line() works out
where to draw it, and both open code the same decision; nothing makes them
agree, even though fbcon erases whatever falls outside the rows that were
reserved. It gives the position a structure of its own, in linux_logo.h,
with -1 on an axis meaning centre on that axis, so that fb_center_logo
becomes a value rather than a second code path and both callers share one
calculation. No functional change. The device tree placement in patch 4
then only fills the same structure in from the node, which is parsed in
logo.c next to the image that comes from it, so that everything that knows
the binding lives in one place and the frame buffer code only asks for the
result.
We have been carrying a cruder version of this downstream on an AM335x
product since 2020, across a handful of board revisions, and it has
removed a real maintenance burden for us.
Where this fits
---------------
There are already ways to get a picture on the screen early, and this does
not replace any of them. A bootloader splash handed over through a
simple-framebuffer node is the earliest of all. A userspace splash is the
most flexible, and is what most systems end up using. What neither covers
is the case where nothing initialises the display before the kernel does.
That case is not exotic. U-Boot's SPL can boot the kernel directly, and
display initialisation lives in U-Boot proper, which then never runs:
there is no splash to hand over and nothing for a simple-framebuffer node
to point at. Falcon mode exists to cut boot time, which is the same reason
one cares about how early the logo appears, so the two tend to arrive
together. Userspace is far too late to fill that gap: on the board I
tested, the kernel has the panel up at 3.2 seconds.
Rob Herring asked in v2 why a simple-framebuffer handover would not do,
and I answered that it did not work on our hardware. That was wrong, and I
am correcting it here: the node I had measured it with was incomplete, and
with a complete one it does work. What it does not do is help where there
is no splash to hand over in the first place, and it does not survive the
native driver.
simplefb registers fb0 at 1.79 seconds with the bootloader's image; tilcdc
initialises at 3.16 seconds, registers its own fb1 and reprograms the
controller to scan that buffer, which starts empty, so the screen goes
black. Nothing is evicted and nothing is cleared: the two frame buffers
coexist, and cat /dev/fb0 > /dev/fb1 brings the picture straight back. It
is only that nobody does it, and by the time anything could, userspace is
already up, which is the moment a splash was there to cover. With
simpledrm it does not even survive in memory, since it hands its clients
shmem buffers and blits them onto the firmware framebuffer, so the first
frame any client commits overwrites what the bootloader drew. A logo the
kernel draws itself has none of this: there is nothing to carry across.
Notes on the binding
--------------------
- The compatible has no "linux," prefix. Rob asked why it was Linux
specific, and nothing in the node is: it describes an image and where
it goes, which any consumer can read. A bootloader drawing the same
logo before the kernel starts is the obvious other one. The 224 is the
palette limit of the format, which is what lets the kernel use the
image the way it already uses its built-in logos, without converting
it.
- The palette size is derived from the length of the "clut" property
instead of being a separate property, so it cannot disagree with the
palette actually supplied.
- "data" holds plain palette indices. The 32 entry offset the frame
buffer layer reserves for the console is an implementation detail and
is applied by the kernel.
- "logo-position" takes -1 on an axis to mean centre on that axis, which
is what fb_center_logo already meant. A boolean could only centre both
axes or neither, and next to explicit coordinates it would have to
override them silently.
- "logo-rotation" turns the logo, not the screen. "logo-position" and
"logo-offset" are screen pixels whatever the rotation says, and a
quarter turn only changes how much room the logo takes up.
- "logo-position" and "logo-offset" are spelled with the prefix because
plain "position" and "offset" are already used elsewhere in the tree
with an incompatible type, which dtschema rejects.
- The reserved memory path takes a "memory-region" phandle rather than a
bare address. The reservation is what makes the memory safe to read at
all, and it is what gives the kernel a size to bounds check against.
The byte arrays are not written by hand: patch 7 adds ppmtodtlogo, a host
tool along the lines of the existing pnmtologo -- plain C, no
dependencies, no quantization of its own -- that turns a PPM image into
the node or into the memory region blob. It is what produced everything
tested below.
Until chosen.yaml knows about the node, dtbs_check rejects it on any board
that uses it: it allows only ^framebuffer under /chosen. I sent that one
line change as dt-schema pull request 204, and Rob closed it saying he
expects it is either not needed or will change, given this discussion.
That seems like the right order to me, so I am not asking for it again
here: once the shape of the binding is settled, the schema change follows
from it, and I will send it then.
On a system that has DRM but no frame buffer device, nothing draws the
logo at all: fbcon is what draws it today, and without it there is no
consumer. Showing it there needs an in-kernel DRM client, which I have
working on top of this series and will send separately. I mention it
because it is the reason the node is parsed in logo.c rather than in
fb_logo.c: the same node then puts the logo on the same pixel whichever of
the two draws it, rotated or not, which I checked on the same board.
Testing: built for arm with CONFIG_LOGO_DT_CLUT224 both enabled and
disabled, and each of the seven patches builds on its own, with the option
enabled from the patch that introduces it. Also built for x86_64 with
CONFIG_OF=n, where nothing is left unresolved and the placement data is
dropped from the image, and for powerpc Cell -- where SPU_BASE makes
CONFIG_FB_LOGO_EXTRA real -- to a linked vmlinux. No compiler warnings,
W=1 clean on the files touched, checkpatch --strict clean apart from the
MAINTAINERS reminder for the new tool, which the existing drivers/video/
entry already covers. A full dt_binding_check has one complaint for this
binding and none other in the whole tree: the chosen.yaml rejection
described above.
Boot tested under qemu-system-arm -M versatilepb with PL111 and fbcon, at
16bpp, over a 26 case matrix: absolute positions, per axis centring, all
four corners, offsets including negative ones, out of range values, the
three rotations, and each rotation combined with an offset and with an
absolute position, each compared pixel by pixel against the source image
rotated to match. A position out of range on both axes is clamped to the
corner, and the console text then overwrites the rows below what fbcon
reserved. Supplying the same image through a reserved region instead
produces an identical logo area. Blobs with a bad magic, a geometry larger
than the reservation and an out of range pixel are each rejected with a
warning, with no logo drawn and no crash.
Also boot tested on real hardware: an AM335x board (tilcdc) with an
800x480 panel at 16bpp. The product logo comes up where the node asks,
both carried in the device tree and taken from a bootloader-loaded
reserved memory region, and the two produce a frame buffer that is
identical byte for byte. Dumping /dev/fb0 and comparing it against the
source image, the logo lands on exactly the pixel the binding predicts:
16836 of 16836 pixels match, and one pixel of displacement in any
direction drops that to about 90%. With logo-rotation = "ccw" it lands on
the rotated position the same way, every pixel.
Changes since v2:
- Rebased onto current mainline (v7.3-rc3).
- Dropped the "linux," prefix from the compatible, and renamed the
binding to match, per Rob.
- A rotation asked for by the device tree now turns the logo and not the
screen: "logo-position" and "logo-offset" stay in screen pixels, and a
quarter turn only changes how much room the logo takes up. v2 fed the
device tree rotation into the path fbcon uses for a rotated console,
which places the logo in the console's own frame and maps the result
back, so a vertical offset came out horizontal on a screen that was not
rotated. Found by testing it on the panel. fb_rotate_logo() is split
into the part that turns the image and the part that moves the
placement, and the console path is unchanged.
- The placement properties are read in logo.c, next to the image, and the
frame buffer code only asks for the result, so that the binding is
parsed in a single place.
- Added patch 1, which pulls the logo position out into something both
fb_prepare_logo() and fb_show_logo_line() share, after Helge pointed out
that the placement patch was stamped in rather than merged with the
existing code. That turned up a real bug: the reservation took the larger
of the console position and the device tree one while the drawing took
only the device tree one.
- IS_ENABLED() instead of #ifdef, per Helge, so the code is compile checked
whatever the configuration.
- The device tree is read once, from fb_prepare_logo(), rather than from
every accessor.
- Dropped "logo-centered" for -1 in "logo-position", per Helge.
- The position and offset are added in 64 bits and both are bounded in the
binding; in int, a large pair from the device tree wrapped instead of
landing against an edge.
- The copy of the image is no longer freed from a late initcall, which ran
before async_synchronize_full() and so could pull it out from under a
display driver still probing. Pixels are allocated with kvmalloc().
- The example declares compatible and model on the root node, and the
binding no longer requires the image properties unconditionally, which
made the reserved memory form unreachable. Both found by Rob's bot.
- Extra logos are not drawn when the device tree supplied the logo; they
stack up from an arbitrary point once it has been placed.
Max Pedraza (7):
fbdev: describe where the boot logo goes in one place
dt-bindings: display: add a device tree supplied boot logo
video: logo: allow the boot logo to come from the device tree
fbdev: honour the device tree boot logo placement properties
dt-bindings: display: allow the boot logo in a reserved memory region
video: logo: allow the boot logo to come from a reserved memory region
video: logo: add ppmtodtlogo host tool
.../bindings/display/boot-logo-clut224.yaml | 161 +++++++
MAINTAINERS | 1 +
drivers/video/fbdev/core/fb_logo.c | 221 ++++++++--
drivers/video/logo/Kconfig | 12 +
drivers/video/logo/Makefile | 6 +-
drivers/video/logo/logo.c | 332 +++++++++++++-
drivers/video/logo/ppmtodtlogo.c | 416 ++++++++++++++++++
include/linux/linux_logo.h | 59 +++
8 files changed, 1170 insertions(+), 38 deletions(-)
create mode 100644 Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
create mode 100644 drivers/video/logo/ppmtodtlogo.c
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 1/7] fbdev: describe where the boot logo goes in one place
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
@ 2026-09-23 20:10 ` Max Pedraza
2026-09-23 20:10 ` [PATCH v3 2/7] dt-bindings: display: add a device tree supplied boot logo Max Pedraza
` (5 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
fb_prepare_logo() works out how many rows to keep clear for the logo and
fb_show_logo_line() works out where to draw it, and both of them open code
the same decision: centred if fb_center_logo, top left otherwise. The two
calculations have to agree, because fbcon erases whatever falls outside the
rows that were reserved, and nothing makes them.
Give the position a small structure of its own, with -1 on an axis meaning
centre on that axis, and have both callers ask for it. fb_center_logo
becomes {-1, -1} rather than a second path, and the arithmetic lives in one
function that both use, so they cannot end up disagreeing.
The structure and logo_place_axis() go in linux_logo.h rather than in the
frame buffer code, since where a logo goes is a property of the logo, not
of the one thing that draws it today.
logo_place_axis() also clamps, which the open coded version did not: the
result is now always a position at which the logo lies entirely on screen.
That is not reachable today, since the only positions are the two the
console offers, but it stops being something the next caller has to
remember.
No functional change intended.
Signed-off-by: Max Pedraza <maximpedraza@gmail.com>
---
drivers/video/fbdev/core/fb_logo.c | 58 +++++++++++++++++++-----------
include/linux/linux_logo.h | 29 +++++++++++++++
2 files changed, 67 insertions(+), 20 deletions(-)
diff --git a/drivers/video/fbdev/core/fb_logo.c b/drivers/video/fbdev/core/fb_logo.c
index 0bab8352b6..5ec9f9554f 100644
--- a/drivers/video/fbdev/core/fb_logo.c
+++ b/drivers/video/fbdev/core/fb_logo.c
@@ -8,6 +8,17 @@
bool fb_center_logo __read_mostly;
int fb_logo_count __read_mostly = -1;
+/*
+ * The placement in effect, as asked for on the console command line.
+ */
+static const struct logo_placement *fb_logo_placement(void)
+{
+ static const struct logo_placement centred = { .x = -1, .y = -1 };
+ static const struct logo_placement top_left = { };
+
+ return fb_center_logo ? ¢red : &top_left;
+}
+
static inline unsigned int safe_shift(unsigned int d, int n)
{
return n < 0 ? d >> -n : d << n;
@@ -281,7 +292,11 @@ static int fb_show_logo_line(struct fb_info *info, int rotate,
{
u32 *palette = NULL, *saved_pseudo_palette = NULL;
unsigned char *logo_new = NULL, *logo_rotate = NULL;
+ const struct logo_placement *p;
+ unsigned int xres = info->var.xres;
+ unsigned int yres = info->var.yres;
struct fb_image image;
+ unsigned int block;
/* Return if the frame buffer is not mapped or suspended */
if (logo == NULL || info->state != FBINFO_STATE_RUNNING ||
@@ -322,26 +337,22 @@ static int fb_show_logo_line(struct fb_info *info, int rotate,
fb_set_logo(info, logo, logo_new, fb_logo.depth);
}
- if (fb_center_logo) {
- int xres = info->var.xres;
- int yres = info->var.yres;
+ image.width = logo->width;
+ image.height = logo->height;
- if (rotate == FB_ROTATE_CW || rotate == FB_ROTATE_CCW) {
- xres = info->var.yres;
- yres = info->var.xres;
- }
+ if (rotate == FB_ROTATE_CW || rotate == FB_ROTATE_CCW)
+ swap(xres, yres);
- while (n && (n * (logo->width + 8) - 8 > xres))
- --n;
- image.dx = (xres - (n * (logo->width + 8) - 8)) / 2;
- image.dy = y ?: (yres - logo->height) / 2;
- } else {
- image.dx = 0;
- image.dy = y;
- }
+ while (n && (n * (logo->width + 8) - 8 > xres))
+ --n;
- image.width = logo->width;
- image.height = logo->height;
+ /* The copies are drawn in a row, so they are centred as one block */
+ block = n ? n * (logo->width + 8) - 8 : logo->width;
+
+ p = fb_logo_placement();
+ image.dx = logo_place_axis(p->x, xres, block);
+ /* A stacked logo goes where the caller put it */
+ image.dy = y ? y : logo_place_axis(p->y, yres, image.height);
if (rotate) {
logo_rotate = kmalloc_array(logo->width, logo->height,
@@ -418,6 +429,7 @@ static int fb_show_extra_logos(struct fb_info *info, int y, int rotate)
int fb_prepare_logo(struct fb_info *info, int rotate)
{
int depth = fb_get_color_depth(&info->var, &info->fix);
+ const struct logo_placement *p;
unsigned int yres;
int height;
@@ -480,9 +492,15 @@ int fb_prepare_logo(struct fb_info *info, int rotate)
}
}
- height = fb_logo.logo->height;
- if (fb_center_logo)
- height += (yres - fb_logo.logo->height) / 2;
+ /*
+ * fbcon only leaves the first @height rows of the screen alone, so a
+ * logo placed further down would be drawn and then immediately
+ * cleared. Ask the same placement fb_show_logo_line() will use, so
+ * that the two cannot disagree.
+ */
+ p = fb_logo_placement();
+ height = logo_place_axis(p->y, yres, fb_logo.logo->height) +
+ fb_logo.logo->height;
#ifdef CONFIG_FB_LOGO_EXTRA
height = fb_prepare_extra_logos(info, height, yres);
#endif
diff --git a/include/linux/linux_logo.h b/include/linux/linux_logo.h
index 1e727a2cb4..b3b15d3800 100644
--- a/include/linux/linux_logo.h
+++ b/include/linux/linux_logo.h
@@ -13,6 +13,8 @@
*/
#include <linux/init.h>
+#include <linux/minmax.h>
+#include <linux/types.h>
#define LINUX_LOGO_MONO 1 /* monochrome black/white */
@@ -36,6 +38,33 @@ extern const struct linux_logo logo_linux_clut224;
extern const struct linux_logo logo_spe_clut224;
extern const struct linux_logo *fb_find_logo(int depth);
+
+/*
+ * Where a boot logo goes. A coordinate of -1 centres the logo on that axis.
+ * Whatever draws the logo describes its placement this way and computes it
+ * with logo_place_axis(), so that no two places can end up disagreeing about
+ * where the logo is.
+ */
+struct logo_placement {
+ s32 x, y;
+};
+
+/*
+ * Place a logo along one axis. @pos is the coordinate asked for, or -1 to
+ * centre on that axis. The result is clamped so that the logo always lies
+ * entirely within the screen: the drawing code does not clip, so asking for
+ * more than that would otherwise scribble past the end of the frame buffer.
+ */
+static inline int logo_place_axis(s32 pos, unsigned int span, unsigned int size)
+{
+ int last = (int)span - (int)size;
+
+ if (size > span)
+ return 0;
+
+ return pos == -1 ? last / 2 : clamp(pos, 0, last);
+}
+
#ifdef CONFIG_FB_LOGO_EXTRA
extern void fb_append_extra_logo(const struct linux_logo *logo,
unsigned int n);
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 2/7] dt-bindings: display: add a device tree supplied boot logo
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
2026-09-23 20:10 ` [PATCH v3 1/7] fbdev: describe where the boot logo goes in one place Max Pedraza
@ 2026-09-23 20:10 ` Max Pedraza
2026-09-23 20:10 ` [PATCH v3 3/7] video: logo: allow the boot logo to come from the device tree Max Pedraza
` (4 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
Embedded products frequently need a product specific boot logo. Today that
means pointing CONFIG_LOGO_LINUX_CLUT224_FILE at a different image, which
bakes it into the kernel image: a change of branding, or a second product
sharing the same board support package, requires a separate kernel build,
and requalifying that kernel for what is a cosmetic change.
Add a binding for a "boot-logo-clut224" node, which carries the logo
in the same paletted format the built-in CLUT224 logos already use, plus a
few optional properties describing where on the screen it is drawn.
The node lives under /chosen because a logo is configuration handed over by
firmware rather than a description of the hardware. Open Firmware, which
the device tree descends from, carried a boot logo in the same spirit as
the oem-logo variable under /options, and simple-framebuffer nodes live
under /chosen today for the same reason.
The palette size is derived from the length of the "clut" property rather
than spelled out separately, and "data" holds plain palette indices; the
32 entry offset the frame buffer layer reserves for the console is an
implementation detail and is applied by the kernel, not by the binding.
"logo-position" takes -1 on an axis to mean centre on that axis rather than
carrying a separate boolean for it. A boolean can only centre both axes or
neither, and next to explicit coordinates it would have to override them
silently when a device tree gave both.
A rotation turns the logo, not the screen. "logo-position" and
"logo-offset" are screen pixels whatever "logo-rotation" says, and a
quarter turn only changes how much room the logo takes up. Placing the
logo in a frame that turns with it would make the same pair of
coordinates mean different places on the same panel, decided by a
property that is meant to describe the image.
Every coordinate is bounded. The kernel clamps them anyway, since it cannot
trust a device tree, but a value that cannot possibly be meant is worth
catching in dtbs_check rather than on the panel.
Signed-off-by: Max Pedraza <maximpedraza@gmail.com>
---
.../bindings/display/boot-logo-clut224.yaml | 135 ++++++++++++++++++
MAINTAINERS | 1 +
2 files changed, 136 insertions(+)
create mode 100644 Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
diff --git a/Documentation/devicetree/bindings/display/boot-logo-clut224.yaml b/Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
new file mode 100644
index 0000000000..9dc3471763
--- /dev/null
+++ b/Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
@@ -0,0 +1,135 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/display/boot-logo-clut224.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Boot logo supplied by the device tree
+
+maintainers:
+ - Max Pedraza <maximpedraza@gmail.com>
+
+description: |
+ Embedded systems commonly need a product specific boot logo. Today that
+ means pointing CONFIG_LOGO_LINUX_CLUT224_FILE at a different image, which
+ bakes it into the kernel image: changing the logo means building and
+ deploying a new kernel.
+
+ This node lets the boot logo be described by the device tree instead, so that
+ a single kernel image can serve several products, or several revisions of the
+ same product, that only differ in branding.
+
+ Since a logo is configuration rather than a description of the hardware, the
+ node lives under /chosen, next to the other things firmware hands to the
+ operating system. Open Firmware, which the device tree descends from, carried
+ a boot logo in the same spirit as the oem-logo variable under /options.
+
+ The image is stored in the same paletted format the in-kernel CLUT224 logos
+ use: a palette of at most 224 RGB entries plus one palette index per pixel.
+
+properties:
+ $nodename:
+ const: logo
+
+ compatible:
+ const: boot-logo-clut224
+
+ width:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ description: Logo width in pixels.
+ minimum: 1
+ maximum: 65535
+
+ height:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ description: Logo height in pixels.
+ minimum: 1
+ maximum: 65535
+
+ clut:
+ $ref: /schemas/types.yaml#/definitions/uint8-array
+ description:
+ Colour lookup table, as consecutive red, green and blue bytes per entry.
+ The number of entries is derived from the property length and must not
+ exceed 224.
+ minItems: 3
+ maxItems: 672
+
+ data:
+ $ref: /schemas/types.yaml#/definitions/uint8-array
+ description:
+ One byte per pixel, left to right and top to bottom, each byte being an
+ index into the colour lookup table. The property length must be equal to
+ width multiplied by height.
+
+ logo-position:
+ $ref: /schemas/types.yaml#/definitions/int32-array
+ description:
+ X and Y coordinates, in pixels, of the top left corner of the logo.
+ A value of -1 on an axis centres the logo on that axis instead.
+ Defaults to the top left corner of the screen.
+ items:
+ - description: X coordinate, or -1 to centre horizontally
+ minimum: -1
+ maximum: 65535
+ - description: Y coordinate, or -1 to centre vertically
+ minimum: -1
+ maximum: 65535
+
+ logo-offset:
+ $ref: /schemas/types.yaml#/definitions/int32-array
+ description:
+ X and Y displacement, in screen pixels, applied after the logo has been
+ placed.
+ Mostly useful together with a centred axis, to land the logo somewhere
+ other than the middle of a panel whose usable area is not its centre.
+ items:
+ - description: X displacement
+ minimum: -65535
+ maximum: 65535
+ - description: Y displacement
+ minimum: -65535
+ maximum: 65535
+
+ logo-rotation:
+ $ref: /schemas/types.yaml#/definitions/string
+ description:
+ Rotation applied to the logo before it is drawn. It turns the logo and
+ not the screen, so a quarter turn swaps how much room the logo takes
+ up, but logo-position and logo-offset stay in screen pixels either
+ way.
+ enum: [none, cw, ccw, ud]
+ default: none
+
+required:
+ - compatible
+ - width
+ - height
+ - clut
+ - data
+
+additionalProperties: false
+
+examples:
+ - |
+ // A 4x2 logo using three colours, centred on both axes.
+ / {
+ compatible = "foo";
+ model = "foo";
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ chosen {
+ logo {
+ compatible = "boot-logo-clut224";
+ width = <4>;
+ height = <2>;
+ clut = /bits/ 8 <0xff 0x00 0x00
+ 0x00 0xff 0x00
+ 0x00 0x00 0xff>;
+ data = /bits/ 8 <0x00 0x01 0x01 0x00
+ 0x02 0x00 0x00 0x02>;
+ logo-position = <(-1) (-1)>;
+ };
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index c241444789..804a48e9bc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -10339,6 +10339,7 @@ L: dri-devel@lists.freedesktop.org
S: Maintained
Q: http://patchwork.kernel.org/project/linux-fbdev/list/
T: git git://git.kernel.org/pub/scm/linux/kernel/git/deller/linux-fbdev.git
+F: Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
F: Documentation/fb/
F: drivers/video/
F: include/linux/fb.h
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 3/7] video: logo: allow the boot logo to come from the device tree
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
2026-09-23 20:10 ` [PATCH v3 1/7] fbdev: describe where the boot logo goes in one place Max Pedraza
2026-09-23 20:10 ` [PATCH v3 2/7] dt-bindings: display: add a device tree supplied boot logo Max Pedraza
@ 2026-09-23 20:10 ` Max Pedraza
2026-09-23 20:10 ` [PATCH v3 4/7] fbdev: honour the device tree boot logo placement properties Max Pedraza
` (3 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
Add CONFIG_LOGO_DT_CLUT224, which makes fb_find_logo() look for a node
compatible with "boot-logo-clut224" under /chosen before falling
back to the logos built into the kernel image.
The image is validated before it is used: the palette must have at most
224 entries, the pixel data length must match the geometry, and every
pixel must reference an entry that exists. A malformed node is reported
and ignored rather than drawn, so a bad device tree cannot take the
display down with it.
The image is copied out of the device tree so that the 32 entry offset
the frame buffer layer reserves for the console can be applied to the
pixels. The pixels are allocated with kvmalloc(), since a full screen
image is larger than kmalloc() will comfortably serve.
Unlike the built-in logos the copy is never freed. Those are initdata and
go away in free_initmem(), which runs after async_synchronize_full();
anything this code could hook into runs before that, so releasing the image
here would pull it out from under a display driver whose probe is still in
flight. It is a modest allocation and it lives as long as the device tree
it came from, which also means fb_find_logo() can still hand it out after
the built-in logos are gone.
The node lives under /chosen because a logo is configuration handed over
by firmware rather than a description of the hardware, which is also
where simple-framebuffer nodes live for the same reason.
The lookup is guarded with IS_ENABLED() rather than wrapped in an #ifdef,
so that the code is compile checked whatever the configuration and the
compiler drops it when the option is off. Built for x86_64 with CONFIG_OF=n
the object is left with no unresolved of_* symbol and none of the data.
Signed-off-by: Max Pedraza <maximpedraza@gmail.com>
---
drivers/video/logo/Kconfig | 12 +++
drivers/video/logo/logo.c | 154 ++++++++++++++++++++++++++++++++++++-
include/linux/linux_logo.h | 3 +
3 files changed, 168 insertions(+), 1 deletion(-)
diff --git a/drivers/video/logo/Kconfig b/drivers/video/logo/Kconfig
index cda15b9589..cce89948f1 100644
--- a/drivers/video/logo/Kconfig
+++ b/drivers/video/logo/Kconfig
@@ -76,4 +76,16 @@ config LOGO_LINUX_CLUT224_FILE
magick source_image -compress none -colors 224 destination.ppm
+config LOGO_DT_CLUT224
+ bool "224-color logo supplied by the device tree"
+ depends on OF
+ help
+ Look for a boot logo in the device tree, in a node compatible with
+ "boot-logo-clut224" under /chosen, instead of using one of
+ the logos built into the kernel image. This allows a single kernel
+ image to be used by several products that only differ in branding.
+
+ If no such node is present, or it is disabled, the built-in logo
+ selected above is used, so saying Y here is safe.
+
endif # LOGO
diff --git a/drivers/video/logo/logo.c b/drivers/video/logo/logo.c
index 91535f8848..84afd5b337 100644
--- a/drivers/video/logo/logo.c
+++ b/drivers/video/logo/logo.c
@@ -11,6 +11,9 @@
*/
#include <linux/linux_logo.h>
+#include <linux/of.h>
+#include <linux/sizes.h>
+#include <linux/slab.h>
#include <linux/stddef.h>
#include <linux/module.h>
@@ -22,6 +25,139 @@ static bool nologo;
module_param(nologo, bool, 0);
MODULE_PARM_DESC(nologo, "Disables startup logo");
+/* Boot logo supplied by the device tree */
+
+#define LOGO_DT_MAX_CLUT 224
+/*
+ * The first 32 palette entries are reserved for the console, so the logo
+ * colours start at index 32. That is an implementation detail of the frame
+ * buffer layer rather than a property of the image, so the device tree stores
+ * plain indices and the offset is applied here.
+ */
+#define LOGO_DT_CLUT_OFFSET 32
+/*
+ * Sanity limit on the image size, a device tree is not a good place for more:
+ * a 4K screen is 8.3M pixels, and the copy is kept for the life of the kernel.
+ * One byte per pixel, so this is a byte count as well.
+ */
+#define LOGO_DT_MAX_PIXELS SZ_16M
+
+static struct linux_logo logo_dt_clut224 = {
+ .type = LINUX_LOGO_CLUT224,
+};
+
+static unsigned char *logo_dt_clut;
+static unsigned char *logo_dt_data;
+
+static int logo_dt_parse(struct device_node *np)
+{
+ unsigned int clutsize, npixels, i;
+ unsigned char *clut, *data;
+ u32 width, height;
+ int len, ret;
+
+ ret = of_property_read_u32(np, "width", &width);
+ if (ret)
+ return ret;
+
+ ret = of_property_read_u32(np, "height", &height);
+ if (ret)
+ return ret;
+
+ if (!width || !height || (u64)width * height > LOGO_DT_MAX_PIXELS)
+ return -EINVAL;
+
+ npixels = width * height;
+
+ len = of_property_count_u8_elems(np, "clut");
+ if (len < 3 || len % 3)
+ return -EINVAL;
+
+ clutsize = len / 3;
+ if (clutsize > LOGO_DT_MAX_CLUT)
+ return -EINVAL;
+
+ ret = of_property_count_u8_elems(np, "data");
+ if (ret < 0)
+ return ret;
+ if ((unsigned int)ret != npixels)
+ return -EINVAL;
+
+ clut = kmalloc(len, GFP_KERNEL);
+ if (!clut)
+ return -ENOMEM;
+
+ /* The palette is at most 672 bytes, the pixels can be megabytes */
+ data = kvmalloc(npixels, GFP_KERNEL);
+ if (!data) {
+ ret = -ENOMEM;
+ goto err_free_clut;
+ }
+
+ ret = of_property_read_u8_array(np, "clut", clut, len);
+ if (ret)
+ goto err_free_data;
+
+ ret = of_property_read_u8_array(np, "data", data, npixels);
+ if (ret)
+ goto err_free_data;
+
+ for (i = 0; i < npixels; i++) {
+ if (data[i] >= clutsize) {
+ ret = -ERANGE;
+ goto err_free_data;
+ }
+ data[i] += LOGO_DT_CLUT_OFFSET;
+ }
+
+ logo_dt_clut = clut;
+ logo_dt_data = data;
+
+ logo_dt_clut224.width = width;
+ logo_dt_clut224.height = height;
+ logo_dt_clut224.clutsize = clutsize;
+ logo_dt_clut224.clut = clut;
+ logo_dt_clut224.data = data;
+
+ return 0;
+
+err_free_data:
+ kvfree(data);
+err_free_clut:
+ kfree(clut);
+ return ret;
+}
+
+static const struct linux_logo *logo_dt_find(void)
+{
+ static bool probed;
+ struct device_node *np;
+ int ret;
+
+ if (!IS_ENABLED(CONFIG_LOGO_DT_CLUT224))
+ return NULL;
+
+ if (probed)
+ return logo_dt_data ? &logo_dt_clut224 : NULL;
+
+ probed = true;
+
+ np = of_get_compatible_child(of_chosen, LOGO_DT_COMPATIBLE);
+ if (!np)
+ return NULL;
+
+ if (of_device_is_available(np)) {
+ ret = logo_dt_parse(np);
+ if (ret)
+ pr_warn("logo: ignoring malformed %pOF node (%d)\n",
+ np, ret);
+ }
+
+ of_node_put(np);
+
+ return logo_dt_data ? &logo_dt_clut224 : NULL;
+}
+
/*
* Logos are located in the initdata, and will be freed in kernel_init.
* Use late_init to mark the logos as freed to prevent any further use.
@@ -45,7 +181,23 @@ const struct linux_logo * __ref fb_find_logo(int depth)
{
const struct linux_logo *logo = NULL;
- if (nologo || logos_freed)
+ if (nologo)
+ return NULL;
+
+ /*
+ * A logo supplied by the device tree wins over the built-in ones. It
+ * is an ordinary allocation rather than initdata, so unlike them it
+ * stays valid for the life of the kernel, and is still there for a
+ * display driver whose probe finishes after the built-in logos have
+ * gone.
+ */
+ if (depth >= 8) {
+ logo = logo_dt_find();
+ if (logo)
+ return logo;
+ }
+
+ if (logos_freed)
return NULL;
#ifdef CONFIG_LOGO_LINUX_MONO
diff --git a/include/linux/linux_logo.h b/include/linux/linux_logo.h
index b3b15d3800..1e7e9db6dd 100644
--- a/include/linux/linux_logo.h
+++ b/include/linux/linux_logo.h
@@ -22,6 +22,9 @@
#define LINUX_LOGO_CLUT224 3 /* 224 colors */
#define LINUX_LOGO_GRAY256 4 /* 256 levels grayscale */
+/* Compatible of the /chosen child describing a device tree supplied logo */
+#define LOGO_DT_COMPATIBLE "boot-logo-clut224"
+
struct linux_logo {
int type; /* one of LINUX_LOGO_* */
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 4/7] fbdev: honour the device tree boot logo placement properties
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
` (2 preceding siblings ...)
2026-09-23 20:10 ` [PATCH v3 3/7] video: logo: allow the boot logo to come from the device tree Max Pedraza
@ 2026-09-23 20:10 ` Max Pedraza
2026-09-23 20:10 ` [PATCH v3 5/7] dt-bindings: display: allow the boot logo in a reserved memory region Max Pedraza
` (2 subsequent siblings)
6 siblings, 0 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
A logo supplied by the device tree describes a specific product, so where
it lands on the panel matters in a way it does not for the built-in Tux.
Fill in the placement added earlier from the optional "logo-position",
"logo-offset" and "logo-rotation" properties of the
"boot-logo-clut224" node, so that it takes the place of what the
console command line would otherwise have asked for.
"logo-position" gives the top left corner, with -1 on an axis meaning
centre on that axis, which is what fb_center_logo already meant.
"logo-offset" is added afterwards, and is the reason the offset joins the
structure here. Centring and then displacing is what panels with a
partially visible area need, where the usable region is not the centre of
the mode, and it is the only way to express it when the image comes from a
reserved memory region and the device tree therefore does not know the logo
size. Both come straight from the device tree, so the sum is done in 64
bits and clamped, and a silly pair of values lands the logo against an edge
rather than off the screen.
The properties are read by logo_dt_placement(), in logo.c next to the
parser of the image that comes from the same node, so that everything that
knows the binding lives in one place. The frame buffer code only asks for
the result.
The rotation values match FB_ROTATE_*, but they do not mean what a
rotated console means. fbcon turns the screen, so it looks at the panel
sideways, places the logo in the console's own frame and maps the result
back. A rotation asked for by the device tree turns the logo alone, on a
screen that stays where it is, so the placement stays in screen pixels
and only the room the logo takes up changes. fb_rotate_logo() is split
into the part that turns the image and the part that moves the
placement, and a device tree rotation uses only the former.
A logo placed by the device tree is drawn once rather than once per CPU:
the device tree names one position, and repeating the image from there
would either overlap the copies or walk them off the reserved region.
For the same reason the extra logos appended by fb_append_extra_logo() are
not drawn when the device tree supplied the logo. They stack up from
wherever the previous logo ended, which is an arbitrary point once the
device tree has placed the first one, so the two do not compose. Only
PowerPC Cell appends any, and a message is printed if there was anything to
drop. The space fb_prepare_extra_logos() reserves for them is left alone,
so that combination keeps a gap it no longer fills.
The node is read once, from fb_prepare_logo(), since fb_show_logo() is only
ever reached after that has run. The read is guarded with IS_ENABLED()
rather than an #ifdef so that the code is compile checked whatever the
configuration, and optimised away when the option is off.
A node whose image is rejected still has its placement honoured, so a
malformed node yields the built-in logo drawn where the device tree asked
for it.
Signed-off-by: Max Pedraza <maximpedraza@gmail.com>
---
drivers/video/fbdev/core/fb_logo.c | 189 ++++++++++++++++++++++++-----
drivers/video/logo/logo.c | 78 ++++++++++++
include/linux/linux_logo.h | 45 +++++--
3 files changed, 274 insertions(+), 38 deletions(-)
diff --git a/drivers/video/fbdev/core/fb_logo.c b/drivers/video/fbdev/core/fb_logo.c
index 5ec9f9554f..ff7d0b409e 100644
--- a/drivers/video/fbdev/core/fb_logo.c
+++ b/drivers/video/fbdev/core/fb_logo.c
@@ -9,13 +9,72 @@ bool fb_center_logo __read_mostly;
int fb_logo_count __read_mostly = -1;
/*
- * The placement in effect, as asked for on the console command line.
+ * Placement of a logo supplied by the device tree. Both the image and its
+ * placement are parsed by drivers/video/logo/logo.c, here we only keep a copy
+ * of where it goes.
+ */
+static struct {
+ struct logo_placement pos;
+ bool valid;
+ int rotation; /* LOGO_ROTATE_* (== FB_ROTATE_*), or -1 */
+} fb_logo_dt;
+
+/*
+ * Read the placement out of the device tree. Called once, from
+ * fb_prepare_logo(): fb_show_logo() is only ever reached after that has run,
+ * so everything below can just look at fb_logo_dt.
+ */
+static void fb_logo_dt_read(void)
+{
+ static bool read_done;
+
+ /*
+ * logo_dt_placement() checks the option too, but it lives in another
+ * translation unit, so without the check here the compiler would have
+ * to keep fb_logo_dt around for a call that can only fail.
+ */
+ if (!IS_ENABLED(CONFIG_LOGO_DT_CLUT224) || read_done)
+ return;
+
+ fb_logo_dt.valid = !logo_dt_placement(&fb_logo_dt.pos,
+ &fb_logo_dt.rotation);
+ /* Last, so that nothing can observe a half filled in placement */
+ read_done = true;
+}
+
+/*
+ * A rotation asked for by the device tree turns the logo, not the screen, so
+ * it is placed in screen pixels. A rotated console turns the screen, and the
+ * logo is placed in the console's own frame. Telling the two apart is what
+ * this is for.
+ */
+static bool fb_logo_dt_rotated(void)
+{
+ return IS_ENABLED(CONFIG_LOGO_DT_CLUT224) && fb_logo_dt.valid &&
+ fb_logo_dt.rotation >= 0;
+}
+
+static int fb_logo_dt_rotation(int rotate)
+{
+ if (fb_logo_dt_rotated())
+ return fb_logo_dt.rotation;
+
+ return rotate;
+}
+
+/*
+ * The placement in effect: what the device tree asked for if it supplied a
+ * logo, otherwise what the console command line asked for. fb_prepare_logo()
+ * has already called fb_logo_dt_read() by the time anything gets here.
*/
static const struct logo_placement *fb_logo_placement(void)
{
static const struct logo_placement centred = { .x = -1, .y = -1 };
static const struct logo_placement top_left = { };
+ if (IS_ENABLED(CONFIG_LOGO_DT_CLUT224) && fb_logo_dt.valid)
+ return &fb_logo_dt.pos;
+
return fb_center_logo ? ¢red : &top_left;
}
@@ -218,35 +277,45 @@ static void fb_rotate_logo_ccw(const u8 *in, u8 *out, u32 width, u32 height)
out[height * (w - j) + i] = *in++;
}
-static void fb_rotate_logo(struct fb_info *info, u8 *dst,
- struct fb_image *image, int rotate)
+/* Turn the image itself, leaving @image->dx and @image->dy alone */
+static void fb_rotate_logo_image(u8 *dst, struct fb_image *image, int rotate)
{
- u32 tmp;
-
if (rotate == FB_ROTATE_UD) {
fb_rotate_logo_ud(image->data, dst, image->width,
image->height);
- image->dx = info->var.xres - image->width - image->dx;
- image->dy = info->var.yres - image->height - image->dy;
} else if (rotate == FB_ROTATE_CW) {
fb_rotate_logo_cw(image->data, dst, image->width,
image->height);
swap(image->width, image->height);
- tmp = image->dy;
- image->dy = image->dx;
- image->dx = info->var.xres - image->width - tmp;
} else if (rotate == FB_ROTATE_CCW) {
fb_rotate_logo_ccw(image->data, dst, image->width,
image->height);
swap(image->width, image->height);
- tmp = image->dx;
- image->dx = image->dy;
- image->dy = info->var.yres - image->height - tmp;
}
image->data = dst;
}
+/* As above, and map the placement from the console's frame to the screen */
+static void fb_rotate_logo(struct fb_info *info, u8 *dst,
+ struct fb_image *image, int rotate)
+{
+ u32 tmp = rotate == FB_ROTATE_CW ? image->dy : image->dx;
+
+ fb_rotate_logo_image(dst, image, rotate);
+
+ if (rotate == FB_ROTATE_UD) {
+ image->dx = info->var.xres - image->width - image->dx;
+ image->dy = info->var.yres - image->height - image->dy;
+ } else if (rotate == FB_ROTATE_CW) {
+ image->dy = image->dx;
+ image->dx = info->var.xres - image->width - tmp;
+ } else if (rotate == FB_ROTATE_CCW) {
+ image->dx = image->dy;
+ image->dy = info->var.yres - image->height - tmp;
+ }
+}
+
static void fb_do_show_logo(struct fb_info *info, struct fb_image *image,
int rotate, unsigned int num)
{
@@ -295,6 +364,8 @@ static int fb_show_logo_line(struct fb_info *info, int rotate,
const struct logo_placement *p;
unsigned int xres = info->var.xres;
unsigned int yres = info->var.yres;
+ bool dt_rotated = fb_logo_dt_rotated();
+ unsigned int fw, fh;
struct fb_image image;
unsigned int block;
@@ -340,28 +411,60 @@ static int fb_show_logo_line(struct fb_info *info, int rotate,
image.width = logo->width;
image.height = logo->height;
- if (rotate == FB_ROTATE_CW || rotate == FB_ROTATE_CCW)
- swap(xres, yres);
+ /* How much of the screen the logo covers once it has been turned */
+ fw = image.width;
+ fh = image.height;
+ if (rotate == FB_ROTATE_CW || rotate == FB_ROTATE_CCW) {
+ swap(fw, fh);
+ /* A turned console is looked at sideways, a turned logo is not */
+ if (!dt_rotated)
+ swap(xres, yres);
+ }
- while (n && (n * (logo->width + 8) - 8 > xres))
- --n;
+ if (IS_ENABLED(CONFIG_LOGO_DT_CLUT224) && fb_logo_dt.valid) {
+ /*
+ * The device tree asks for the logo at one place, so draw it
+ * once. Repeating it per CPU would either overlap the copies
+ * or walk them out of the region reserved for the logo.
+ */
+ n = 1;
+ } else {
+ while (n && (n * (logo->width + 8) - 8 > xres))
+ --n;
+ }
/* The copies are drawn in a row, so they are centred as one block */
block = n ? n * (logo->width + 8) - 8 : logo->width;
p = fb_logo_placement();
- image.dx = logo_place_axis(p->x, xres, block);
- /* A stacked logo goes where the caller put it */
- image.dy = y ? y : logo_place_axis(p->y, yres, image.height);
+ if (dt_rotated) {
+ /*
+ * The device tree names a place on the screen, so the logo is
+ * placed by the room it takes up there and drawn as it lies.
+ */
+ image.dx = logo_place_axis(p->x, p->offset_x, xres, fw);
+ image.dy = logo_place_axis(p->y, p->offset_y, yres, fh);
+ } else {
+ image.dx = logo_place_axis(p->x, p->offset_x, xres, block);
+ /* A stacked logo goes where the caller put it */
+ image.dy = y ? y : logo_place_axis(p->y, p->offset_y, yres,
+ image.height);
+ }
if (rotate) {
logo_rotate = kmalloc_array(logo->width, logo->height,
GFP_KERNEL);
- if (logo_rotate)
- fb_rotate_logo(info, logo_rotate, &image, rotate);
+ if (logo_rotate) {
+ if (dt_rotated)
+ fb_rotate_logo_image(logo_rotate, &image,
+ rotate);
+ else
+ fb_rotate_logo(info, logo_rotate, &image,
+ rotate);
+ }
}
- fb_do_show_logo(info, &image, rotate, n);
+ fb_do_show_logo(info, &image, dt_rotated ? FB_ROTATE_UR : rotate, n);
kfree(palette);
if (saved_pseudo_palette != NULL)
@@ -414,10 +517,23 @@ static int fb_prepare_extra_logos(struct fb_info *info, unsigned int height,
return height;
}
+/*
+ * A logo supplied by the device tree is placed where the device tree asks,
+ * and the extra logos stack up from wherever the previous one ended. Those
+ * two do not compose: the stack would start at an arbitrary point and walk
+ * off the screen. The device tree wins, and says so if anything is dropped.
+ */
static int fb_show_extra_logos(struct fb_info *info, int y, int rotate)
{
unsigned int i;
+ if (IS_ENABLED(CONFIG_LOGO_DT_CLUT224) && fb_logo_dt.valid) {
+ if (fb_logo_ex_num)
+ pr_info("fb: device tree logo in use, %u extra logo(s) not drawn\n",
+ fb_logo_ex_num);
+ return y;
+ }
+
for (i = 0; i < fb_logo_ex_num; i++)
y = fb_show_logo_line(info, rotate,
fb_logo_ex[i].logo, y, fb_logo_ex[i].n);
@@ -430,11 +546,14 @@ int fb_prepare_logo(struct fb_info *info, int rotate)
{
int depth = fb_get_color_depth(&info->var, &info->fix);
const struct logo_placement *p;
- unsigned int yres;
+ unsigned int yres, fh;
int height;
memset(&fb_logo, 0, sizeof(struct logo_data));
+ fb_logo_dt_read();
+ rotate = fb_logo_dt_rotation(rotate);
+
if (info->flags & FBINFO_MISC_TILEBLITTING ||
info->fbops->owner || !fb_logo_count)
return 0;
@@ -458,12 +577,23 @@ int fb_prepare_logo(struct fb_info *info, int rotate)
if (!fb_logo.logo)
return 0;
- if (rotate == FB_ROTATE_UR || rotate == FB_ROTATE_UD)
+ /*
+ * A turned console is looked at sideways, so the logo is measured
+ * against the console's own frame. A logo turned by the device tree
+ * leaves the screen alone, and only covers a different part of it.
+ */
+ fh = fb_logo.logo->height;
+ if (fb_logo_dt_rotated()) {
yres = info->var.yres;
- else
+ if (rotate == FB_ROTATE_CW || rotate == FB_ROTATE_CCW)
+ fh = fb_logo.logo->width;
+ } else if (rotate == FB_ROTATE_UR || rotate == FB_ROTATE_UD) {
+ yres = info->var.yres;
+ } else {
yres = info->var.xres;
+ }
- if (fb_logo.logo->height > yres) {
+ if (fh > yres) {
fb_logo.logo = NULL;
return 0;
}
@@ -499,8 +629,7 @@ int fb_prepare_logo(struct fb_info *info, int rotate)
* that the two cannot disagree.
*/
p = fb_logo_placement();
- height = logo_place_axis(p->y, yres, fb_logo.logo->height) +
- fb_logo.logo->height;
+ height = logo_place_axis(p->y, p->offset_y, yres, fh) + fh;
#ifdef CONFIG_FB_LOGO_EXTRA
height = fb_prepare_extra_logos(info, height, yres);
#endif
@@ -516,6 +645,8 @@ int fb_show_logo(struct fb_info *info, int rotate)
if (!fb_logo_count)
return 0;
+ rotate = fb_logo_dt_rotation(rotate);
+
count = fb_logo_count < 0 ? num_online_cpus() : fb_logo_count;
y = fb_show_logo_line(info, rotate, fb_logo.logo, 0, count);
#ifdef CONFIG_FB_LOGO_EXTRA
diff --git a/drivers/video/logo/logo.c b/drivers/video/logo/logo.c
index 84afd5b337..b600fc3aab 100644
--- a/drivers/video/logo/logo.c
+++ b/drivers/video/logo/logo.c
@@ -158,6 +158,84 @@ static const struct linux_logo *logo_dt_find(void)
return logo_dt_data ? &logo_dt_clut224 : NULL;
}
+static int logo_dt_parse_rotation(const char *rotation)
+{
+ if (!strcmp(rotation, "none"))
+ return LOGO_ROTATE_NONE;
+ if (!strcmp(rotation, "cw"))
+ return LOGO_ROTATE_CW;
+ if (!strcmp(rotation, "ud"))
+ return LOGO_ROTATE_UD;
+ if (!strcmp(rotation, "ccw"))
+ return LOGO_ROTATE_CCW;
+
+ return -EINVAL;
+}
+
+/**
+ * logo_dt_placement - where a logo supplied by the device tree asks to go
+ * @pos: filled in with the requested position and offset
+ * @rotation: filled in with one of LOGO_ROTATE_*, or -1 when the node does
+ * not ask for any
+ *
+ * Only the placement is read here, the image itself is fb_find_logo()'s
+ * business. It lives next to the image parser rather than in any one user so
+ * that everything that draws the logo reads the same properties the same way.
+ *
+ * Return: 0 if the device tree supplies a logo node, -ENODEV otherwise, in
+ * which case @pos and @rotation are left untouched.
+ */
+int logo_dt_placement(struct logo_placement *pos, int *rotation)
+{
+ struct device_node *np;
+ const char *str;
+ u32 val[2];
+ int rot;
+
+ if (!IS_ENABLED(CONFIG_LOGO_DT_CLUT224))
+ return -ENODEV;
+
+ /*
+ * Same node the image itself comes from, looked up the same way: a
+ * child of /chosen, not just anything compatible anywhere in the tree.
+ */
+ np = of_get_compatible_child(of_chosen, LOGO_DT_COMPATIBLE);
+ if (!np)
+ return -ENODEV;
+
+ if (!of_device_is_available(np)) {
+ of_node_put(np);
+ return -ENODEV;
+ }
+
+ *pos = (struct logo_placement){ };
+ *rotation = -1;
+
+ if (!of_property_read_u32_array(np, "logo-position", val, 2)) {
+ pos->x = (s32)val[0];
+ pos->y = (s32)val[1];
+ }
+
+ if (!of_property_read_u32_array(np, "logo-offset", val, 2)) {
+ pos->offset_x = (s32)val[0];
+ pos->offset_y = (s32)val[1];
+ }
+
+ if (!of_property_read_string(np, "logo-rotation", &str)) {
+ rot = logo_dt_parse_rotation(str);
+ if (rot < 0)
+ pr_warn("logo: %pOF: unknown logo-rotation \"%s\"\n",
+ np, str);
+ else
+ *rotation = rot;
+ }
+
+ of_node_put(np);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(logo_dt_placement);
+
/*
* Logos are located in the initdata, and will be freed in kernel_init.
* Use late_init to mark the logos as freed to prevent any further use.
diff --git a/include/linux/linux_logo.h b/include/linux/linux_logo.h
index 1e7e9db6dd..a8b7bde8d4 100644
--- a/include/linux/linux_logo.h
+++ b/include/linux/linux_logo.h
@@ -12,6 +12,7 @@
* Copyright (C) 2003 Geert Uytterhoeven <geert@linux-m68k.org>
*/
+#include <linux/errno.h>
#include <linux/init.h>
#include <linux/minmax.h>
#include <linux/types.h>
@@ -43,29 +44,55 @@ extern const struct linux_logo logo_spe_clut224;
extern const struct linux_logo *fb_find_logo(int depth);
/*
- * Where a boot logo goes. A coordinate of -1 centres the logo on that axis.
- * Whatever draws the logo describes its placement this way and computes it
- * with logo_place_axis(), so that no two places can end up disagreeing about
- * where the logo is.
+ * Where a boot logo goes. A coordinate of -1 centres the logo on that axis,
+ * and the offset is added afterwards. Whatever draws the logo describes its
+ * placement this way and computes it with logo_place_axis(), so that no two
+ * places can end up disagreeing about where the logo is.
*/
struct logo_placement {
s32 x, y;
+ s32 offset_x, offset_y;
};
+/* Rotation a device tree supplied logo can ask for, same values as FB_ROTATE_* */
+#define LOGO_ROTATE_NONE 0
+#define LOGO_ROTATE_CW 1
+#define LOGO_ROTATE_UD 2
+#define LOGO_ROTATE_CCW 3
+
+#ifdef CONFIG_LOGO
+int logo_dt_placement(struct logo_placement *pos, int *rotation);
+#else
+static inline int logo_dt_placement(struct logo_placement *pos, int *rotation)
+{
+ return -ENODEV;
+}
+#endif
+
/*
* Place a logo along one axis. @pos is the coordinate asked for, or -1 to
- * centre on that axis. The result is clamped so that the logo always lies
- * entirely within the screen: the drawing code does not clip, so asking for
- * more than that would otherwise scribble past the end of the frame buffer.
+ * centre on that axis, and @off is applied afterwards. The result is clamped
+ * so that the logo always lies entirely within the screen: the drawing code
+ * does not clip, so a device tree asking for more than that would otherwise
+ * scribble past the end of the frame buffer.
*/
-static inline int logo_place_axis(s32 pos, unsigned int span, unsigned int size)
+static inline int logo_place_axis(s32 pos, s32 off, unsigned int span,
+ unsigned int size)
{
int last = (int)span - (int)size;
+ s64 coord;
if (size > span)
return 0;
- return pos == -1 ? last / 2 : clamp(pos, 0, last);
+ coord = pos == -1 ? last / 2 : pos;
+
+ /*
+ * Both come straight from the device tree, so the sum is done wide
+ * enough that a silly pair of values lands the logo against an edge
+ * instead of wrapping round to the other one.
+ */
+ return clamp_t(s64, coord + off, 0, last);
}
#ifdef CONFIG_FB_LOGO_EXTRA
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 5/7] dt-bindings: display: allow the boot logo in a reserved memory region
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
` (3 preceding siblings ...)
2026-09-23 20:10 ` [PATCH v3 4/7] fbdev: honour the device tree boot logo placement properties Max Pedraza
@ 2026-09-23 20:10 ` Max Pedraza
2026-09-23 20:10 ` [PATCH v3 6/7] video: logo: allow the boot logo to come from " Max Pedraza
2026-09-23 20:10 ` [PATCH v3 7/7] video: logo: add ppmtodtlogo host tool Max Pedraza
6 siblings, 0 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
Carrying the image in the device tree ties it to the device tree, but the
image and where it goes on screen are independent axes of variation. One
board sold to several customers wants several device trees that differ in
the logo; one customer with several products built on that board wants the
same logo placed differently on each panel. The second case would otherwise
mean duplicating the same image into every device tree.
Let the node point at a reserved memory region filled in by the bootloader
instead, so one image can be shared by device trees that differ only in
placement. The region starts with a small header carrying a magic number
and the geometry, so the kernel can tell a logo from an empty or stale
region and bounds check everything against the reservation.
A phandle to a declared region is used rather than a bare address: the
reservation is what makes the memory safe to read and what gives the kernel
a size to validate against.
The two ways of supplying the image are mutually exclusive, which is what
the oneOf introduced here says. Until now the image properties were simply
required, since they were the only way to supply one.
Signed-off-by: Max Pedraza <maximpedraza@gmail.com>
---
.../bindings/display/boot-logo-clut224.yaml | 40 +++++++++++++++----
1 file changed, 33 insertions(+), 7 deletions(-)
diff --git a/Documentation/devicetree/bindings/display/boot-logo-clut224.yaml b/Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
index 9dc3471763..a71fc09a7c 100644
--- a/Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
+++ b/Documentation/devicetree/bindings/display/boot-logo-clut224.yaml
@@ -62,12 +62,31 @@ properties:
index into the colour lookup table. The property length must be equal to
width multiplied by height.
+ memory-region:
+ maxItems: 1
+ description: |
+ Reserved memory region holding the logo, as an alternative to carrying
+ it in the width, height, clut and data properties. The bootloader is
+ expected to have placed the image there before starting the kernel.
+
+ This lets one image be shared by several device trees that differ only
+ in where the logo goes, which is what a family of products built on the
+ same board but with different panels needs.
+
+ The region starts with a header of four little endian 32 bit words:
+ the magic number 0x4f474f4c ("LOGO"), the width, the height and the
+ number of palette entries. The palette follows, as consecutive red,
+ green and blue bytes per entry, and then one byte per pixel, each an
+ index into that palette.
+
logo-position:
$ref: /schemas/types.yaml#/definitions/int32-array
description:
- X and Y coordinates, in pixels, of the top left corner of the logo.
- A value of -1 on an axis centres the logo on that axis instead.
- Defaults to the top left corner of the screen.
+ X and Y coordinates, in screen pixels, of the top left corner of the
+ logo once it has been rotated. A value of -1 on an axis centres the
+ logo on that axis instead, which is the only way to say it when the
+ logo size is not known to the device tree, as is the case for the
+ memory-region form. Defaults to the top left corner of the screen.
items:
- description: X coordinate, or -1 to centre horizontally
minimum: -1
@@ -103,10 +122,17 @@ properties:
required:
- compatible
- - width
- - height
- - clut
- - data
+
+# The image either lives in the device tree or in a reserved memory region,
+# never both.
+oneOf:
+ - required:
+ - width
+ - height
+ - clut
+ - data
+ - required:
+ - memory-region
additionalProperties: false
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 6/7] video: logo: allow the boot logo to come from a reserved memory region
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
` (4 preceding siblings ...)
2026-09-23 20:10 ` [PATCH v3 5/7] dt-bindings: display: allow the boot logo in a reserved memory region Max Pedraza
@ 2026-09-23 20:10 ` Max Pedraza
2026-09-23 20:10 ` [PATCH v3 7/7] video: logo: add ppmtodtlogo host tool Max Pedraza
6 siblings, 0 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
Take the image from the region a "memory-region" phandle points at, when
the node has one, instead of from the width, height, clut and data
properties.
Only a declared region is accepted, never a bare physical address. That
matters for more than tidiness: the region is reserved before the allocator
starts, so the logo code can never be pointed at memory the kernel is using
for something else, and a size is known, so every field can be bounds
checked. The region is mapped with memremap(), which copes with a no-map
reservation and fails cleanly instead of handing back a bogus pointer the
way phys_to_virt() on an arbitrary address would.
The image is validated and copied out rather than used in place: the magic
number has to match, the geometry has to be sane, the palette and pixels
have to fit inside the reserved region, and every pixel has to reference an
existing palette entry. Anything else is reported and ignored, falling back
to the built-in logo. Copying also means nothing that happens to the region
afterwards can affect what has already been validated, and it keeps the
pixel data in the same shape both paths produce.
Signed-off-by: Max Pedraza <maximpedraza@gmail.com>
---
drivers/video/logo/logo.c | 176 ++++++++++++++++++++++++++++++--------
1 file changed, 138 insertions(+), 38 deletions(-)
diff --git a/drivers/video/logo/logo.c b/drivers/video/logo/logo.c
index b600fc3aab..6f0adcd34e 100644
--- a/drivers/video/logo/logo.c
+++ b/drivers/video/logo/logo.c
@@ -10,8 +10,10 @@
* Copyright (C) 2003 Geert Uytterhoeven <geert@linux-m68k.org>
*/
+#include <linux/io.h>
#include <linux/linux_logo.h>
#include <linux/of.h>
+#include <linux/of_reserved_mem.h>
#include <linux/sizes.h>
#include <linux/slab.h>
#include <linux/stddef.h>
@@ -38,9 +40,19 @@ MODULE_PARM_DESC(nologo, "Disables startup logo");
/*
* Sanity limit on the image size, a device tree is not a good place for more:
* a 4K screen is 8.3M pixels, and the copy is kept for the life of the kernel.
- * One byte per pixel, so this is a byte count as well.
+ * One byte per pixel, so this is a byte count as well. Keep in step with
+ * MAX_PIXELS in ppmtodtlogo.
*/
#define LOGO_DT_MAX_PIXELS SZ_16M
+/* "LOGO", little endian, at the start of a handed over memory region */
+#define LOGO_DT_MAGIC 0x4f474f4c
+
+struct logo_dt_header {
+ __le32 magic;
+ __le32 width;
+ __le32 height;
+ __le32 clutsize;
+};
static struct linux_logo logo_dt_clut224 = {
.type = LINUX_LOGO_CLUT224,
@@ -49,59 +61,42 @@ static struct linux_logo logo_dt_clut224 = {
static unsigned char *logo_dt_clut;
static unsigned char *logo_dt_data;
-static int logo_dt_parse(struct device_node *np)
+/* Reject geometries that cannot describe a sane image before using them */
+static int logo_dt_check_geometry(u32 width, u32 height, u32 clutsize)
{
- unsigned int clutsize, npixels, i;
- unsigned char *clut, *data;
- u32 width, height;
- int len, ret;
-
- ret = of_property_read_u32(np, "width", &width);
- if (ret)
- return ret;
-
- ret = of_property_read_u32(np, "height", &height);
- if (ret)
- return ret;
-
if (!width || !height || (u64)width * height > LOGO_DT_MAX_PIXELS)
return -EINVAL;
- npixels = width * height;
-
- len = of_property_count_u8_elems(np, "clut");
- if (len < 3 || len % 3)
+ if (!clutsize || clutsize > LOGO_DT_MAX_CLUT)
return -EINVAL;
- clutsize = len / 3;
- if (clutsize > LOGO_DT_MAX_CLUT)
- return -EINVAL;
+ return 0;
+}
- ret = of_property_count_u8_elems(np, "data");
- if (ret < 0)
- return ret;
- if ((unsigned int)ret != npixels)
- return -EINVAL;
+/*
+ * Take a private copy of an image that has already been range checked, so
+ * that nothing else can change it under us, and shift the pixels into the
+ * palette slots the frame buffer layer leaves to the logo.
+ */
+static int logo_dt_store(u32 width, u32 height, u32 clutsize,
+ const u8 *clut_src, const u8 *data_src)
+{
+ unsigned int npixels = width * height;
+ unsigned char *clut, *data;
+ unsigned int i;
+ int ret;
- clut = kmalloc(len, GFP_KERNEL);
+ /* The palette is at most 672 bytes, the pixels can be megabytes */
+ clut = kmemdup(clut_src, clutsize * 3, GFP_KERNEL);
if (!clut)
return -ENOMEM;
- /* The palette is at most 672 bytes, the pixels can be megabytes */
- data = kvmalloc(npixels, GFP_KERNEL);
+ data = kvmemdup(data_src, npixels, GFP_KERNEL);
if (!data) {
ret = -ENOMEM;
goto err_free_clut;
}
- ret = of_property_read_u8_array(np, "clut", clut, len);
- if (ret)
- goto err_free_data;
-
- ret = of_property_read_u8_array(np, "data", data, npixels);
- if (ret)
- goto err_free_data;
-
for (i = 0; i < npixels; i++) {
if (data[i] >= clutsize) {
ret = -ERANGE;
@@ -128,6 +123,111 @@ static int logo_dt_parse(struct device_node *np)
return ret;
}
+static int logo_dt_parse_properties(struct device_node *np)
+{
+ const u8 *clut, *data;
+ u32 width, height;
+ int len, ret;
+
+ ret = of_property_read_u32(np, "width", &width);
+ if (ret)
+ return ret;
+
+ ret = of_property_read_u32(np, "height", &height);
+ if (ret)
+ return ret;
+
+ len = of_property_count_u8_elems(np, "clut");
+ if (len < 3 || len % 3)
+ return -EINVAL;
+
+ ret = logo_dt_check_geometry(width, height, len / 3);
+ if (ret)
+ return ret;
+
+ if (of_property_count_u8_elems(np, "data") != width * height)
+ return -EINVAL;
+
+ clut = of_get_property(np, "clut", NULL);
+ data = of_get_property(np, "data", NULL);
+ if (!clut || !data)
+ return -EINVAL;
+
+ return logo_dt_store(width, height, len / 3, clut, data);
+}
+
+/*
+ * Image handed over by the bootloader in a reserved memory region. Only a
+ * region the device tree declared is accepted, never a bare address, so the
+ * kernel can never be pointed at memory it is using for something else, and
+ * so that a size is known and every access can be bounds checked.
+ */
+static int logo_dt_parse_memory_region(struct device_node *np)
+{
+ u32 width, height, clutsize;
+ const struct logo_dt_header *hdr;
+ struct device_node *mem_np;
+ struct reserved_mem *rmem;
+ size_t clutlen, datalen;
+ const u8 *payload;
+ void *mem;
+ int ret;
+
+ mem_np = of_parse_phandle(np, "memory-region", 0);
+ if (!mem_np)
+ return -ENOENT;
+
+ rmem = of_reserved_mem_lookup(mem_np);
+ of_node_put(mem_np);
+ if (!rmem)
+ return -EINVAL;
+
+ if (rmem->size < sizeof(*hdr))
+ return -EINVAL;
+
+ mem = memremap(rmem->base, rmem->size, MEMREMAP_WB);
+ if (!mem)
+ return -ENOMEM;
+
+ hdr = mem;
+ if (le32_to_cpu(hdr->magic) != LOGO_DT_MAGIC) {
+ ret = -EINVAL;
+ goto out_unmap;
+ }
+
+ width = le32_to_cpu(hdr->width);
+ height = le32_to_cpu(hdr->height);
+ clutsize = le32_to_cpu(hdr->clutsize);
+
+ ret = logo_dt_check_geometry(width, height, clutsize);
+ if (ret)
+ goto out_unmap;
+
+ clutlen = (size_t)clutsize * 3;
+ datalen = (size_t)width * height;
+
+ /* Everything the header promises has to fit inside the region */
+ if (sizeof(*hdr) + clutlen + datalen > rmem->size) {
+ ret = -EINVAL;
+ goto out_unmap;
+ }
+
+ payload = (const u8 *)(hdr + 1);
+ ret = logo_dt_store(width, height, clutsize, payload, payload + clutlen);
+
+out_unmap:
+ memunmap(mem);
+ return ret;
+}
+
+static int logo_dt_parse(struct device_node *np)
+{
+ if (of_property_present(np, "memory-region"))
+ return logo_dt_parse_memory_region(np);
+
+ return logo_dt_parse_properties(np);
+}
+
static const struct linux_logo *logo_dt_find(void)
{
static bool probed;
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 7/7] video: logo: add ppmtodtlogo host tool
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
` (5 preceding siblings ...)
2026-09-23 20:10 ` [PATCH v3 6/7] video: logo: allow the boot logo to come from " Max Pedraza
@ 2026-09-23 20:10 ` Max Pedraza
6 siblings, 0 replies; 8+ messages in thread
From: Max Pedraza @ 2026-09-23 20:10 UTC (permalink / raw)
To: Helge Deller, Thomas Zimmermann, Simona Vetter, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Maxime Ripard
Cc: linux-fbdev, devicetree, dri-devel, linux-kernel, Max Pedraza
The byte arrays a "boot-logo-clut224" node carries are not meant to
be written by hand. Add a host tool that converts a PPM image into the
node, along the lines of the existing pnmtologo: plain C, no dependencies,
and no quantization of its own -- the image must already use at most 224
distinct colours, and the tool points at ImageMagick when it does not.
Three output flavours:
ppmtodtlogo logo.ppm a complete overlay
ppmtodtlogo -t dtsi logo.ppm a bare node, for inclusion
ppmtodtlogo -t bin -o logo.bin ... the blob for a reserved memory
region, header included
The devicetree outputs also carry the optional placement properties,
commented out unless requested on the command line, so the generated file
documents what can be tuned without regenerating the image.
The tool is built when CONFIG_LOGO_DT_CLUT224 is enabled but is not used
by the kernel build itself.
Signed-off-by: Max Pedraza <maximpedraza@gmail.com>
---
drivers/video/logo/Makefile | 6 +-
drivers/video/logo/ppmtodtlogo.c | 416 +++++++++++++++++++++++++++++++
2 files changed, 421 insertions(+), 1 deletion(-)
create mode 100644 drivers/video/logo/ppmtodtlogo.c
diff --git a/drivers/video/logo/Makefile b/drivers/video/logo/Makefile
index 937b37d3b6..6dc205d24e 100644
--- a/drivers/video/logo/Makefile
+++ b/drivers/video/logo/Makefile
@@ -10,7 +10,11 @@ obj-$(CONFIG_SPU_BASE) += logo_spe_clut224.o
# How to generate logo's
-hostprogs := pnmtologo
+hostprogs := pnmtologo ppmtodtlogo
+
+# Not used by the build itself: converts a user's image into the devicetree
+# node or memory blob the "boot-logo-clut224" binding consumes.
+always-$(CONFIG_LOGO_DT_CLUT224) += ppmtodtlogo
# Create commands like "pnmtologo -t mono -n logo_linux_mono -o ..."
quiet_cmd_logo = LOGO $@
diff --git a/drivers/video/logo/ppmtodtlogo.c b/drivers/video/logo/ppmtodtlogo.c
new file mode 100644
index 0000000000..7fded9ce9b
--- /dev/null
+++ b/drivers/video/logo/ppmtodtlogo.c
@@ -0,0 +1,416 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Convert a PPM image into a devicetree boot logo node, or into the binary
+ * blob the "boot-logo-clut224" binding reads from a reserved memory
+ * region.
+ *
+ * Like pnmtologo, this tool does not quantize: the image must already use
+ * at most 224 distinct colours. Reduce it first if needed, for instance:
+ *
+ * magick logo.png -colors 224 logo.ppm
+ *
+ * The node is emitted under /chosen, where the binding expects it: a logo is
+ * configuration handed over by firmware rather than a description of the
+ * hardware.
+ *
+ * The devicetree output stores plain palette indices; the 32 entry offset
+ * the frame buffer layer reserves for the console is applied by the kernel.
+ */
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define MAX_CLUT_COLORS 224
+/* Keep in step with LOGO_DT_MAX_PIXELS in drivers/video/logo/logo.c */
+#define MAX_PIXELS (16U * 1024 * 1024)
+#define BLOB_MAGIC 0x4f474f4cU /* "LOGO", little endian */
+#define BYTES_PER_LINE 12
+
+static const char *programname;
+static const char *filename;
+static const char *outputname;
+static FILE *out;
+
+enum output_type {
+ OUTPUT_DTS, /* complete overlay */
+ OUTPUT_DTSI, /* bare node, for inclusion */
+ OUTPUT_BIN, /* blob for a reserved memory region */
+};
+
+static enum output_type output_type = OUTPUT_DTS;
+
+/* Placement options, emitted into the node */
+static int opt_centered;
+static char *opt_position;
+static char *opt_offset;
+static const char *opt_rotation;
+
+struct color {
+ unsigned char red;
+ unsigned char green;
+ unsigned char blue;
+};
+
+static unsigned int logo_width;
+static unsigned int logo_height;
+static unsigned char *logo_data;
+static struct color logo_clut[MAX_CLUT_COLORS];
+static unsigned int logo_clutsize;
+
+static void die(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ exit(1);
+}
+
+static void usage(void)
+{
+ die("Usage: %s [options] <filename>\n"
+ "\n"
+ "Convert a PPM image into a \"boot-logo-clut224\" node.\n"
+ "The image must use at most %d distinct colours.\n"
+ "\n"
+ " -o <output> write to file instead of stdout\n"
+ " -t <type> dts (default), dtsi or bin\n"
+ " -c centre the logo (logo-position = <(-1) (-1)>)\n"
+ " -p <x>,<y> logo-position, -1 on an axis centres it\n"
+ " -f <dx>,<dy> logo-offset\n"
+ " -r <rotation> logo-rotation: cw, ccw, ud or none\n"
+ " -h this help\n",
+ programname, MAX_CLUT_COLORS);
+}
+
+static unsigned int get_number(FILE *fp)
+{
+ int c;
+ unsigned int val;
+
+ /* Skip leading whitespace */
+ do {
+ c = fgetc(fp);
+ if (c == EOF)
+ die("%s: end of file\n", filename);
+ if (c == '#') {
+ /* Ignore comments 'till end of line */
+ do {
+ c = fgetc(fp);
+ if (c == EOF)
+ die("%s: end of file\n", filename);
+ } while (c != '\n');
+ }
+ } while (isspace(c));
+
+ if (!isdigit(c))
+ die("%s: expected a number\n", filename);
+
+ /* Parse decimal number */
+ val = 0;
+ while (isdigit(c)) {
+ val = 10 * val + c - '0';
+ c = fgetc(fp);
+ if (c == EOF)
+ break;
+ }
+ return val;
+}
+
+static unsigned char get_byte(FILE *fp)
+{
+ int c = fgetc(fp);
+
+ if (c == EOF)
+ die("%s: end of file\n", filename);
+ return c;
+}
+
+static unsigned int find_clut_entry(struct color color)
+{
+ unsigned int i;
+
+ for (i = 0; i < logo_clutsize; i++)
+ if (logo_clut[i].red == color.red &&
+ logo_clut[i].green == color.green &&
+ logo_clut[i].blue == color.blue)
+ return i;
+
+ if (logo_clutsize == MAX_CLUT_COLORS)
+ die("%s: more than %d colors, reduce the image first, e.g.\n"
+ " magick %s -colors %d out.ppm\n",
+ filename, MAX_CLUT_COLORS, filename, MAX_CLUT_COLORS);
+
+ logo_clut[logo_clutsize] = color;
+ return logo_clutsize++;
+}
+
+static void read_image(void)
+{
+ unsigned int i, npixels, maxval;
+ int magic, raw;
+ FILE *fp;
+
+ fp = fopen(filename, "rb");
+ if (!fp)
+ die("Cannot open file %s: %s\n", filename, strerror(errno));
+
+ if (fgetc(fp) != 'P')
+ die("%s is not a PPM file\n", filename);
+
+ magic = fgetc(fp);
+ switch (magic) {
+ case '3':
+ raw = 0;
+ break;
+ case '6':
+ raw = 1;
+ break;
+ default:
+ die("%s is not a PPM file (only P3 and P6 are supported)\n",
+ filename);
+ }
+
+ logo_width = get_number(fp);
+ logo_height = get_number(fp);
+ maxval = get_number(fp);
+ if (maxval != 255)
+ die("%s: maximum color value must be 255\n", filename);
+
+ if (!logo_width || !logo_height)
+ die("%s: zero sized image\n", filename);
+ if ((unsigned long long)logo_width * logo_height > MAX_PIXELS)
+ die("%s: image too large\n", filename);
+
+ npixels = logo_width * logo_height;
+ logo_data = malloc(npixels);
+ if (!logo_data)
+ die("%s\n", strerror(errno));
+
+ for (i = 0; i < npixels; i++) {
+ struct color color;
+
+ if (raw) {
+ color.red = get_byte(fp);
+ color.green = get_byte(fp);
+ color.blue = get_byte(fp);
+ } else {
+ color.red = get_number(fp);
+ color.green = get_number(fp);
+ color.blue = get_number(fp);
+ }
+ logo_data[i] = find_clut_entry(color);
+ }
+
+ fclose(fp);
+}
+
+static void write_bytes(const unsigned char *data, unsigned int len,
+ const char *indent)
+{
+ unsigned int i;
+
+ for (i = 0; i < len; i++) {
+ if (i % BYTES_PER_LINE == 0)
+ fprintf(out, "%s%s", i ? "\n" : "", indent);
+ else
+ fputc(' ', out);
+ fprintf(out, "0x%02x", data[i]);
+ }
+}
+
+static void write_placement(const char *indent)
+{
+ const char *pos = "0 0";
+
+ fprintf(out, "%s/* logo-position gives the top left corner; -1 on an axis\n",
+ indent);
+ fprintf(out, "%s * centres it there. logo-offset is added afterwards. */\n",
+ indent);
+
+ /* -c is a shorthand for centring on both axes; -p wins over it */
+ if (opt_position)
+ pos = opt_position;
+ else if (opt_centered)
+ pos = "(-1) (-1)";
+
+ fprintf(out, "%s%slogo-position = <%s>;\n", indent,
+ opt_position || opt_centered ? "" : "// ", pos);
+ fprintf(out, "%s%slogo-offset = <%s>;\n", indent,
+ opt_offset ? "" : "// ", opt_offset ? opt_offset : "0 0");
+ fprintf(out, "%s%slogo-rotation = \"%s\";\t/* cw, ccw, ud, none */\n",
+ indent, opt_rotation ? "" : "// ",
+ opt_rotation ? opt_rotation : "ccw");
+}
+
+static void write_node(const char *indent)
+{
+ char subindent[16];
+
+ snprintf(subindent, sizeof(subindent), "%s\t\t", indent);
+
+ fprintf(out, "%scompatible = \"boot-logo-clut224\";\n", indent);
+ fprintf(out, "\n");
+ write_placement(indent);
+ fprintf(out, "\n");
+ fprintf(out, "%swidth = <%u>;\n", indent, logo_width);
+ fprintf(out, "%sheight = <%u>;\n", indent, logo_height);
+ fprintf(out, "\n");
+ fprintf(out, "%sclut = /bits/ 8 <", indent);
+ write_bytes((const unsigned char *)logo_clut, logo_clutsize * 3,
+ subindent);
+ fprintf(out, ">;\n");
+ fprintf(out, "\n");
+ fprintf(out, "%sdata = /bits/ 8 <", indent);
+ write_bytes(logo_data, logo_width * logo_height, subindent);
+ fprintf(out, ">;\n");
+}
+
+static void write_header_comment(void)
+{
+ fprintf(out, "/*\n");
+ fprintf(out, " * Boot logo generated by ppmtodtlogo from %s\n",
+ filename);
+ fprintf(out, " * %ux%u pixels, %u colours.\n", logo_width, logo_height,
+ logo_clutsize);
+ fprintf(out, " */\n");
+}
+
+static void write_dts(void)
+{
+ fprintf(out, "/dts-v1/;\n/plugin/;\n\n");
+ write_header_comment();
+ fprintf(out, "\n");
+ fprintf(out, "/ {\n");
+ fprintf(out, "\tfragment@101 {\n");
+ fprintf(out, "\t\ttarget-path = \"/chosen\";\n");
+ fprintf(out, "\n");
+ fprintf(out, "\t\t__overlay__ {\n");
+ fprintf(out, "\t\t\tlogo {\n");
+ write_node("\t\t\t\t");
+ fprintf(out, "\t\t\t};\n");
+ fprintf(out, "\t\t};\n");
+ fprintf(out, "\t};\n");
+ fprintf(out, "};\n");
+}
+
+static void write_dtsi(void)
+{
+ write_header_comment();
+ fprintf(out, "\n");
+ fprintf(out, "chosen {\n");
+ fprintf(out, "\tlogo {\n");
+ write_node("\t\t");
+ fprintf(out, "\t};\n");
+ fprintf(out, "};\n");
+}
+
+static void put_le32(unsigned int val)
+{
+ fputc(val & 0xff, out);
+ fputc((val >> 8) & 0xff, out);
+ fputc((val >> 16) & 0xff, out);
+ fputc((val >> 24) & 0xff, out);
+}
+
+static void write_bin(void)
+{
+ put_le32(BLOB_MAGIC);
+ put_le32(logo_width);
+ put_le32(logo_height);
+ put_le32(logo_clutsize);
+ fwrite(logo_clut, 3, logo_clutsize, out);
+ fwrite(logo_data, 1, logo_width * logo_height, out);
+}
+
+int main(int argc, char *argv[])
+{
+ int opt;
+ char *p;
+
+ programname = argv[0];
+
+ while ((opt = getopt(argc, argv, "o:t:cp:f:r:h")) != -1) {
+ switch (opt) {
+ case 'o':
+ outputname = optarg;
+ break;
+ case 't':
+ if (!strcmp(optarg, "dts"))
+ output_type = OUTPUT_DTS;
+ else if (!strcmp(optarg, "dtsi"))
+ output_type = OUTPUT_DTSI;
+ else if (!strcmp(optarg, "bin"))
+ output_type = OUTPUT_BIN;
+ else
+ usage();
+ break;
+ case 'c':
+ opt_centered = 1;
+ break;
+ case 'p':
+ opt_position = optarg;
+ break;
+ case 'f':
+ opt_offset = optarg;
+ break;
+ case 'r':
+ if (strcmp(optarg, "cw") && strcmp(optarg, "ccw") &&
+ strcmp(optarg, "ud") && strcmp(optarg, "none"))
+ usage();
+ opt_rotation = optarg;
+ break;
+ default:
+ usage();
+ }
+ }
+ if (optind != argc - 1)
+ usage();
+ filename = argv[optind];
+
+ /* "10,20" and "10 20" are both accepted for -p and -f */
+ for (p = opt_position; p && *p; p++)
+ if (*p == ',')
+ *p = ' ';
+ for (p = opt_offset; p && *p; p++)
+ if (*p == ',')
+ *p = ' ';
+
+ read_image();
+
+ if (outputname) {
+ out = fopen(outputname,
+ output_type == OUTPUT_BIN ? "wb" : "w");
+ if (!out)
+ die("Cannot create file %s: %s\n", outputname,
+ strerror(errno));
+ } else {
+ out = stdout;
+ }
+
+ switch (output_type) {
+ case OUTPUT_DTS:
+ write_dts();
+ break;
+ case OUTPUT_DTSI:
+ write_dtsi();
+ break;
+ case OUTPUT_BIN:
+ write_bin();
+ break;
+ }
+
+ if (outputname)
+ fclose(out);
+
+ fprintf(stderr, "%s: %ux%u pixels, %u colours\n", filename, logo_width,
+ logo_height, logo_clutsize);
+
+ return 0;
+}
--
2.39.5
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-09-23 20:11 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-23 20:10 [PATCH v3 0/7] Boot logo supplied by the device tree Max Pedraza
2026-09-23 20:10 ` [PATCH v3 1/7] fbdev: describe where the boot logo goes in one place Max Pedraza
2026-09-23 20:10 ` [PATCH v3 2/7] dt-bindings: display: add a device tree supplied boot logo Max Pedraza
2026-09-23 20:10 ` [PATCH v3 3/7] video: logo: allow the boot logo to come from the device tree Max Pedraza
2026-09-23 20:10 ` [PATCH v3 4/7] fbdev: honour the device tree boot logo placement properties Max Pedraza
2026-09-23 20:10 ` [PATCH v3 5/7] dt-bindings: display: allow the boot logo in a reserved memory region Max Pedraza
2026-09-23 20:10 ` [PATCH v3 6/7] video: logo: allow the boot logo to come from " Max Pedraza
2026-09-23 20:10 ` [PATCH v3 7/7] video: logo: add ppmtodtlogo host tool Max Pedraza
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®