root/drivers/video/fbdev/efifb.c
// SPDX-License-Identifier: GPL-2.0
/*
 * Framebuffer driver for EFI/UEFI based system
 *
 * (c) 2006 Edgar Hucek <gimli@dark-green.com>
 * Original efi driver written by Gerd Knorr <kraxel@goldbach.in-berlin.de>
 *
 */

#include <linux/aperture.h>
#include <linux/kernel.h>
#include <linux/efi.h>
#include <linux/efi-bgrt.h>
#include <linux/errno.h>
#include <linux/fb.h>
#include <linux/platform_device.h>
#include <linux/printk.h>
#include <linux/sysfb.h>
#include <video/vga.h>
#include <asm/efi.h>
#include <drm/drm_utils.h> /* For drm_get_panel_orientation_quirk */
#include <drm/drm_connector.h>  /* For DRM_MODE_PANEL_ORIENTATION_* */

struct bmp_file_header {
        u16 id;
        u32 file_size;
        u32 reserved;
        u32 bitmap_offset;
} __packed;

struct bmp_dib_header {
        u32 dib_header_size;
        s32 width;
        s32 height;
        u16 planes;
        u16 bpp;
        u32 compression;
        u32 bitmap_size;
        u32 horz_resolution;
        u32 vert_resolution;
        u32 colors_used;
        u32 colors_important;
} __packed;

static bool use_bgrt = true;
static bool request_mem_succeeded = false;
static u64 mem_flags = EFI_MEMORY_WC | EFI_MEMORY_UC;

struct efifb_par {
        u32 pseudo_palette[16];
        resource_size_t base;
        resource_size_t size;
};

static struct fb_var_screeninfo efifb_defined = {
        .activate               = FB_ACTIVATE_NOW,
        .height                 = -1,
        .width                  = -1,
        .right_margin           = 32,
        .upper_margin           = 16,
        .lower_margin           = 4,
        .vsync_len              = 4,
        .vmode                  = FB_VMODE_NONINTERLACED,
};

static struct fb_fix_screeninfo efifb_fix = {
        .id                     = "EFI VGA",
        .type                   = FB_TYPE_PACKED_PIXELS,
        .accel                  = FB_ACCEL_NONE,
        .visual                 = FB_VISUAL_TRUECOLOR,
};

static int efifb_setcolreg(unsigned regno, unsigned red, unsigned green,
                           unsigned blue, unsigned transp,
                           struct fb_info *info)
{
        /*
         *  Set a single color register. The values supplied are
         *  already rounded down to the hardware's capabilities
         *  (according to the entries in the `var' structure). Return
         *  != 0 for invalid regno.
         */

        if (regno >= info->cmap.len)
                return 1;

        if (regno < 16) {
                red   >>= 16 - info->var.red.length;
                green >>= 16 - info->var.green.length;
                blue  >>= 16 - info->var.blue.length;
                ((u32 *)(info->pseudo_palette))[regno] =
                        (red   << info->var.red.offset)   |
                        (green << info->var.green.offset) |
                        (blue  << info->var.blue.offset);
        }
        return 0;
}

/*
 * If fbcon deffered console takeover is configured, the intent is for the
 * framebuffer to show the boot graphics (e.g. vendor logo) until there is some
 * (error) message to display. But the boot graphics may have been destroyed by
 * e.g. option ROM output, detect this and restore the boot graphics.
 */
#if defined CONFIG_FRAMEBUFFER_CONSOLE_DEFERRED_TAKEOVER && \
    defined CONFIG_ACPI_BGRT
static void efifb_copy_bmp(u8 *src, u32 *dst, int width, const struct screen_info *si)
{
        u8 r, g, b;

        while (width--) {
                b = *src++;
                g = *src++;
                r = *src++;
                *dst++ = (r << si->red_pos)   |
                         (g << si->green_pos) |
                         (b << si->blue_pos);
        }
}

#ifdef CONFIG_X86
/*
 * On x86 some firmwares use a low non native resolution for the display when
 * they have shown some text messages. While keeping the bgrt filled with info
 * for the native resolution. If the bgrt image intended for the native
 * resolution still fits, it will be displayed very close to the right edge of
 * the display looking quite bad. This function checks for this.
 */
static bool efifb_bgrt_sanity_check(const struct screen_info *si, u32 bmp_width)
{
        /*
         * All x86 firmwares horizontally center the image (the yoffset
         * calculations differ between boards, but xoffset is predictable).
         */
        u32 expected_xoffset = (si->lfb_width - bmp_width) / 2;

        return bgrt_tab.image_offset_x == expected_xoffset;
}
#else
static bool efifb_bgrt_sanity_check(const struct screen_info *si, u32 bmp_width)
{
        return true;
}
#endif

static void efifb_show_boot_graphics(struct fb_info *info, const struct screen_info *si)
{
        u32 bmp_width, bmp_height, bmp_pitch, dst_x, y, src_y;
        struct bmp_file_header *file_header;
        struct bmp_dib_header *dib_header;
        void *bgrt_image = NULL;
        u8 *dst = info->screen_base;

        if (!use_bgrt)
                return;

        if (!bgrt_tab.image_address) {
                pr_info("efifb: No BGRT, not showing boot graphics\n");
                return;
        }

        if (bgrt_tab.status & 0x06) {
                pr_info("efifb: BGRT rotation bits set, not showing boot graphics\n");
                return;
        }

        /* Avoid flashing the logo if we're going to print std probe messages */
        if (console_loglevel > CONSOLE_LOGLEVEL_QUIET)
                return;

        /* bgrt_tab.status is unreliable, so we don't check it */

        if (si->lfb_depth != 32) {
                pr_info("efifb: not 32 bits, not showing boot graphics\n");
                return;
        }

        bgrt_image = memremap(bgrt_tab.image_address, bgrt_image_size,
                              MEMREMAP_WB);
        if (!bgrt_image) {
                pr_warn("efifb: Ignoring BGRT: failed to map image memory\n");
                return;
        }

        if (bgrt_image_size < (sizeof(*file_header) + sizeof(*dib_header)))
                goto error;

        file_header = bgrt_image;
        if (file_header->id != 0x4d42 || file_header->reserved != 0)
                goto error;

        dib_header = bgrt_image + sizeof(*file_header);
        if (dib_header->dib_header_size != 40 || dib_header->width < 0 ||
            dib_header->planes != 1 || dib_header->bpp != 24 ||
            dib_header->compression != 0)
                goto error;

        bmp_width = dib_header->width;
        bmp_height = abs(dib_header->height);
        bmp_pitch = round_up(3 * bmp_width, 4);

        if ((file_header->bitmap_offset + bmp_pitch * bmp_height) >
                                bgrt_image_size)
                goto error;

        if ((bgrt_tab.image_offset_x + bmp_width) > si->lfb_width ||
            (bgrt_tab.image_offset_y + bmp_height) > si->lfb_height)
                goto error;

        if (!efifb_bgrt_sanity_check(si, bmp_width))
                goto error;

        pr_info("efifb: showing boot graphics\n");

        for (y = 0; y < si->lfb_height; y++, dst += si->lfb_linelength) {
                /* Only background? */
                if (y < bgrt_tab.image_offset_y ||
                    y >= (bgrt_tab.image_offset_y + bmp_height)) {
                        memset(dst, 0, 4 * si->lfb_width);
                        continue;
                }

                src_y = y - bgrt_tab.image_offset_y;
                /* Positive header height means upside down row order */
                if (dib_header->height > 0)
                        src_y = (bmp_height - 1) - src_y;

                memset(dst, 0, bgrt_tab.image_offset_x * 4);
                dst_x = bgrt_tab.image_offset_x;
                efifb_copy_bmp(bgrt_image + file_header->bitmap_offset +
                                            src_y * bmp_pitch,
                               (u32 *)dst + dst_x, bmp_width, si);
                dst_x += bmp_width;
                memset((u32 *)dst + dst_x, 0, (si->lfb_width - dst_x) * 4);
        }

        memunmap(bgrt_image);
        return;

error:
        memunmap(bgrt_image);
        pr_warn("efifb: Ignoring BGRT: unexpected or invalid BMP data\n");
}
#else
static inline void efifb_show_boot_graphics(struct fb_info *info, const struct screen_info *si)
{ }
#endif

/*
 * fb_ops.fb_destroy is called by the last put_fb_info() call at the end
 * of unregister_framebuffer() or fb_release(). Do any cleanup here.
 */
static void efifb_destroy(struct fb_info *info)
{
        struct efifb_par *par = info->par;

        if (info->screen_base) {
                if (mem_flags & (EFI_MEMORY_UC | EFI_MEMORY_WC))
                        iounmap(info->screen_base);
                else
                        memunmap(info->screen_base);
        }

        if (request_mem_succeeded)
                release_mem_region(par->base, par->size);
        fb_dealloc_cmap(&info->cmap);

        framebuffer_release(info);
}

static const struct fb_ops efifb_ops = {
        .owner          = THIS_MODULE,
        FB_DEFAULT_IOMEM_OPS,
        .fb_destroy     = efifb_destroy,
        .fb_setcolreg   = efifb_setcolreg,
};

static void efifb_setup(struct screen_info *si, char *options)
{
        char *this_opt;

        if (options && *options) {
                while ((this_opt = strsep(&options, ",")) != NULL) {
                        if (!*this_opt) continue;

                        efifb_setup_from_dmi(si, this_opt);

                        if (!strncmp(this_opt, "base:", 5))
                                si->lfb_base = simple_strtoul(this_opt+5, NULL, 0);
                        else if (!strncmp(this_opt, "stride:", 7))
                                si->lfb_linelength = simple_strtoul(this_opt+7, NULL, 0) * 4;
                        else if (!strncmp(this_opt, "height:", 7))
                                si->lfb_height = simple_strtoul(this_opt+7, NULL, 0);
                        else if (!strncmp(this_opt, "width:", 6))
                                si->lfb_width = simple_strtoul(this_opt+6, NULL, 0);
                        else if (!strcmp(this_opt, "nowc"))
                                mem_flags &= ~EFI_MEMORY_WC;
                        else if (!strcmp(this_opt, "nobgrt"))
                                use_bgrt = false;
                }
        }
}

static inline bool fb_base_is_valid(struct screen_info *si)
{
        if (si->lfb_base)
                return true;

        if (!(si->capabilities & VIDEO_CAPABILITY_64BIT_BASE))
                return false;

        if (si->ext_lfb_base)
                return true;

        return false;
}

#define efifb_attr_decl(name, fmt)                                      \
static ssize_t name##_show(struct device *dev,                          \
                           struct device_attribute *attr,               \
                           char *buf)                                   \
{                                                                       \
        struct screen_info *si = dev_get_drvdata(dev);                  \
        if (!si)                                                        \
                return -ENODEV;                                         \
        return sprintf(buf, fmt "\n", (si->lfb_##name));                \
}                                                                       \
static DEVICE_ATTR_RO(name)

efifb_attr_decl(base, "0x%x");
efifb_attr_decl(linelength, "%u");
efifb_attr_decl(height, "%u");
efifb_attr_decl(width, "%u");
efifb_attr_decl(depth, "%u");

static struct attribute *efifb_attrs[] = {
        &dev_attr_base.attr,
        &dev_attr_linelength.attr,
        &dev_attr_width.attr,
        &dev_attr_height.attr,
        &dev_attr_depth.attr,
        NULL
};
ATTRIBUTE_GROUPS(efifb);

static int efifb_probe(struct platform_device *dev)
{
        struct sysfb_display_info *dpy;
        struct screen_info *si;
        struct fb_info *info;
        struct efifb_par *par;
        int err, orientation;
        unsigned int size_vmode;
        unsigned int size_remap;
        unsigned int size_total;
        char *option = NULL;
        efi_memory_desc_t md;

        /*
         * If we fail probing the device, the kernel might try a different
         * driver. We get a copy of the attached screen_info, so that we can
         * modify its values without affecting later drivers.
         */
        dpy = dev_get_platdata(&dev->dev);
        if (!dpy)
                return -ENODEV;

        si = devm_kmemdup(&dev->dev, &dpy->screen, sizeof(*si), GFP_KERNEL);
        if (!si)
                return -ENOMEM;

        dev_set_drvdata(&dev->dev, si);

        if (si->orig_video_isVGA != VIDEO_TYPE_EFI)
                return -ENODEV;

        if (fb_get_options("efifb", &option))
                return -ENODEV;
        efifb_setup(si, option);

        /* We don't get linelength from UGA Draw Protocol, only from
         * EFI Graphics Protocol.  So if it's not in DMI, and it's not
         * passed in from the user, we really can't use the framebuffer.
         */
        if (!si->lfb_linelength)
                return -ENODEV;

        if (!si->lfb_depth)
                si->lfb_depth = 32;
        if (!si->pages)
                si->pages = 1;
        if (!fb_base_is_valid(si)) {
                printk(KERN_DEBUG "efifb: invalid framebuffer address\n");
                return -ENODEV;
        }
        printk(KERN_INFO "efifb: probing for efifb\n");

        /* just assume they're all unset if any are */
        if (!si->blue_size) {
                si->blue_size = 8;
                si->blue_pos = 0;
                si->green_size = 8;
                si->green_pos = 8;
                si->red_size = 8;
                si->red_pos = 16;
                si->rsvd_size = 8;
                si->rsvd_pos = 24;
        }

        efifb_fix.smem_start = __screen_info_lfb_base(si);

        efifb_defined.bits_per_pixel = si->lfb_depth;
        efifb_defined.xres = si->lfb_width;
        efifb_defined.yres = si->lfb_height;
        efifb_fix.line_length = si->lfb_linelength;

        /*   size_vmode -- that is the amount of memory needed for the
         *                 used video mode, i.e. the minimum amount of
         *                 memory we need. */
        size_vmode = efifb_defined.yres * efifb_fix.line_length;

        /*   size_total -- all video memory we have. Used for
         *                 entries, ressource allocation and bounds
         *                 checking. */
        size_total = si->lfb_size;
        if (size_total < size_vmode)
                size_total = size_vmode;

        /*   size_remap -- the amount of video memory we are going to
         *                 use for efifb.  With modern cards it is no
         *                 option to simply use size_total as that
         *                 wastes plenty of kernel address space. */
        size_remap  = size_vmode * 2;
        if (size_remap > size_total)
                size_remap = size_total;
        if (size_remap % PAGE_SIZE)
                size_remap += PAGE_SIZE - (size_remap % PAGE_SIZE);
        efifb_fix.smem_len = size_remap;

        if (request_mem_region(efifb_fix.smem_start, size_remap, "efifb")) {
                request_mem_succeeded = true;
        } else {
                /* We cannot make this fatal. Sometimes this comes from magic
                   spaces our resource handlers simply don't know about */
                pr_warn("efifb: cannot reserve video memory at 0x%lx\n",
                        efifb_fix.smem_start);
        }

        info = framebuffer_alloc(sizeof(*par), &dev->dev);
        if (!info) {
                err = -ENOMEM;
                goto err_release_mem;
        }
        par = info->par;
        info->pseudo_palette = par->pseudo_palette;

        par->base = efifb_fix.smem_start;
        par->size = size_remap;

        if (efi_enabled(EFI_MEMMAP) &&
            !efi_mem_desc_lookup(efifb_fix.smem_start, &md)) {
                if ((efifb_fix.smem_start + efifb_fix.smem_len) >
                    (md.phys_addr + (md.num_pages << EFI_PAGE_SHIFT))) {
                        pr_err("efifb: video memory @ 0x%lx spans multiple EFI memory regions\n",
                               efifb_fix.smem_start);
                        err = -EIO;
                        goto err_release_fb;
                }
                /*
                 * If the UEFI memory map covers the efifb region, we may only
                 * remap it using the attributes the memory map prescribes.
                 */
                md.attribute &= EFI_MEMORY_UC | EFI_MEMORY_WC |
                                EFI_MEMORY_WT | EFI_MEMORY_WB;
                if (md.attribute) {
                        mem_flags |= EFI_MEMORY_WT | EFI_MEMORY_WB;
                        mem_flags &= md.attribute;
                }
        }
        if (mem_flags & EFI_MEMORY_WC)
                info->screen_base = ioremap_wc(efifb_fix.smem_start,
                                               efifb_fix.smem_len);
        else if (mem_flags & EFI_MEMORY_UC)
                info->screen_base = ioremap(efifb_fix.smem_start,
                                            efifb_fix.smem_len);
        else if (mem_flags & EFI_MEMORY_WT)
                info->screen_base = memremap(efifb_fix.smem_start,
                                             efifb_fix.smem_len, MEMREMAP_WT);
        else if (mem_flags & EFI_MEMORY_WB)
                info->screen_base = memremap(efifb_fix.smem_start,
                                             efifb_fix.smem_len, MEMREMAP_WB);
        if (!info->screen_base) {
                pr_err("efifb: abort, cannot remap video memory 0x%x @ 0x%lx\n",
                        efifb_fix.smem_len, efifb_fix.smem_start);
                err = -EIO;
                goto err_release_fb;
        }

        efifb_show_boot_graphics(info, si);

        pr_info("efifb: framebuffer at 0x%lx, using %dk, total %dk\n",
               efifb_fix.smem_start, size_remap/1024, size_total/1024);
        pr_info("efifb: mode is %dx%dx%d, linelength=%d, pages=%d\n",
               efifb_defined.xres, efifb_defined.yres,
               efifb_defined.bits_per_pixel, efifb_fix.line_length,
               si->pages);

        efifb_defined.xres_virtual = efifb_defined.xres;
        efifb_defined.yres_virtual = efifb_fix.smem_len /
                                        efifb_fix.line_length;
        pr_info("efifb: scrolling: redraw\n");
        efifb_defined.yres_virtual = efifb_defined.yres;

        /* some dummy values for timing to make fbset happy */
        efifb_defined.pixclock     = 10000000 / efifb_defined.xres *
                                        1000 / efifb_defined.yres;
        efifb_defined.left_margin  = (efifb_defined.xres / 8) & 0xf8;
        efifb_defined.hsync_len    = (efifb_defined.xres / 8) & 0xf8;

        efifb_defined.red.offset    = si->red_pos;
        efifb_defined.red.length    = si->red_size;
        efifb_defined.green.offset  = si->green_pos;
        efifb_defined.green.length  = si->green_size;
        efifb_defined.blue.offset   = si->blue_pos;
        efifb_defined.blue.length   = si->blue_size;
        efifb_defined.transp.offset = si->rsvd_pos;
        efifb_defined.transp.length = si->rsvd_size;

        pr_info("efifb: %s: "
               "size=%d:%d:%d:%d, shift=%d:%d:%d:%d\n",
               "Truecolor",
               si->rsvd_size,
               si->red_size,
               si->green_size,
               si->blue_size,
               si->rsvd_pos,
               si->red_pos,
               si->green_pos,
               si->blue_pos);

        efifb_fix.ypanstep  = 0;
        efifb_fix.ywrapstep = 0;

        info->fbops = &efifb_ops;
        info->var = efifb_defined;
        info->fix = efifb_fix;

        orientation = drm_get_panel_orientation_quirk(efifb_defined.xres,
                                                      efifb_defined.yres);
        switch (orientation) {
        default:
                info->fbcon_rotate_hint = FB_ROTATE_UR;
                break;
        case DRM_MODE_PANEL_ORIENTATION_BOTTOM_UP:
                info->fbcon_rotate_hint = FB_ROTATE_UD;
                break;
        case DRM_MODE_PANEL_ORIENTATION_LEFT_UP:
                info->fbcon_rotate_hint = FB_ROTATE_CCW;
                break;
        case DRM_MODE_PANEL_ORIENTATION_RIGHT_UP:
                info->fbcon_rotate_hint = FB_ROTATE_CW;
                break;
        }

        err = fb_alloc_cmap(&info->cmap, 256, 0);
        if (err < 0) {
                pr_err("efifb: cannot allocate colormap\n");
                goto err_unmap;
        }

        err = devm_aperture_acquire_for_platform_device(dev, par->base, par->size);
        if (err) {
                pr_err("efifb: cannot acquire aperture\n");
                goto err_fb_dealloc_cmap;
        }
        err = devm_register_framebuffer(&dev->dev, info);
        if (err < 0) {
                pr_err("efifb: cannot register framebuffer\n");
                goto err_fb_dealloc_cmap;
        }
        fb_info(info, "%s frame buffer device\n", info->fix.id);
        return 0;

err_fb_dealloc_cmap:
        fb_dealloc_cmap(&info->cmap);
err_unmap:
        if (mem_flags & (EFI_MEMORY_UC | EFI_MEMORY_WC))
                iounmap(info->screen_base);
        else
                memunmap(info->screen_base);
err_release_fb:
        framebuffer_release(info);
err_release_mem:
        if (request_mem_succeeded)
                release_mem_region(efifb_fix.smem_start, size_total);
        return err;
}

static struct platform_driver efifb_driver = {
        .driver = {
                .name = "efi-framebuffer",
                .dev_groups = efifb_groups,
        },
        .probe = efifb_probe,
};

builtin_platform_driver(efifb_driver);