Contributors: 8
Author Tokens Token Proportion Commits Commit Proportion
Arvind Sankar 1398 54.78% 20 50.00%
Ard Biesheuvel 780 30.56% 9 22.50%
Thomas Zimmermann 227 8.89% 3 7.50%
Matt Fleming 106 4.15% 4 10.00%
Hans de Goede 16 0.63% 1 2.50%
Linn Crosetto 16 0.63% 1 2.50%
Roy Franz 7 0.27% 1 2.50%
Matthew Garrett 2 0.08% 1 2.50%
Total 2552 40


// SPDX-License-Identifier: GPL-2.0
/* -----------------------------------------------------------------------
 *
 *   Copyright 2011 Intel Corporation; author Matt Fleming
 *
 * ----------------------------------------------------------------------- */

#include <linux/bitops.h>
#include <linux/ctype.h>
#include <linux/efi.h>
#include <linux/screen_info.h>
#include <linux/string.h>
#include <asm/efi.h>
#include <asm/setup.h>
#include <video/edid.h>

#include "efistub.h"

enum efi_cmdline_option {
	EFI_CMDLINE_NONE,
	EFI_CMDLINE_MODE_NUM,
	EFI_CMDLINE_RES,
	EFI_CMDLINE_AUTO,
	EFI_CMDLINE_LIST
};

static struct {
	enum efi_cmdline_option option;
	union {
		u32 mode;
		struct {
			u32 width, height;
			int format;
			u8 depth;
		} res;
	};
} cmdline = { .option = EFI_CMDLINE_NONE };

static bool parse_modenum(char *option, char **next)
{
	u32 m;

	if (!strstarts(option, "mode="))
		return false;
	option += strlen("mode=");
	m = simple_strtoull(option, &option, 0);
	if (*option && *option++ != ',')
		return false;
	cmdline.option = EFI_CMDLINE_MODE_NUM;
	cmdline.mode   = m;

	*next = option;
	return true;
}

static bool parse_res(char *option, char **next)
{
	u32 w, h, d = 0;
	int pf = -1;

	if (!isdigit(*option))
		return false;
	w = simple_strtoull(option, &option, 10);
	if (*option++ != 'x' || !isdigit(*option))
		return false;
	h = simple_strtoull(option, &option, 10);
	if (*option == '-') {
		option++;
		if (strstarts(option, "rgb")) {
			option += strlen("rgb");
			pf = PIXEL_RGB_RESERVED_8BIT_PER_COLOR;
		} else if (strstarts(option, "bgr")) {
			option += strlen("bgr");
			pf = PIXEL_BGR_RESERVED_8BIT_PER_COLOR;
		} else if (isdigit(*option))
			d = simple_strtoull(option, &option, 10);
		else
			return false;
	}
	if (*option && *option++ != ',')
		return false;
	cmdline.option     = EFI_CMDLINE_RES;
	cmdline.res.width  = w;
	cmdline.res.height = h;
	cmdline.res.format = pf;
	cmdline.res.depth  = d;

	*next = option;
	return true;
}

static bool parse_auto(char *option, char **next)
{
	if (!strstarts(option, "auto"))
		return false;
	option += strlen("auto");
	if (*option && *option++ != ',')
		return false;
	cmdline.option = EFI_CMDLINE_AUTO;

	*next = option;
	return true;
}

static bool parse_list(char *option, char **next)
{
	if (!strstarts(option, "list"))
		return false;
	option += strlen("list");
	if (*option && *option++ != ',')
		return false;
	cmdline.option = EFI_CMDLINE_LIST;

	*next = option;
	return true;
}

void efi_parse_option_graphics(char *option)
{
	while (*option) {
		if (parse_modenum(option, &option))
			continue;
		if (parse_res(option, &option))
			continue;
		if (parse_auto(option, &option))
			continue;
		if (parse_list(option, &option))
			continue;

		while (*option && *option++ != ',')
			;
	}
}

static u32 choose_mode_modenum(efi_graphics_output_protocol_t *gop)
{
	efi_graphics_output_mode_info_t *info __free(efi_pool) = NULL;
	efi_graphics_output_protocol_mode_t *mode;
	unsigned long info_size;
	u32 max_mode, cur_mode;
	efi_status_t status;
	int pf;

	mode = efi_table_attr(gop, mode);

	cur_mode = efi_table_attr(mode, mode);
	if (cmdline.mode == cur_mode)
		return cur_mode;

	max_mode = efi_table_attr(mode, max_mode);
	if (cmdline.mode >= max_mode) {
		efi_err("Requested mode is invalid\n");
		return cur_mode;
	}

	status = efi_call_proto(gop, query_mode, cmdline.mode, &info_size, &info);
	if (status != EFI_SUCCESS) {
		efi_err("Couldn't get mode information\n");
		return cur_mode;
	}

	pf = info->pixel_format;
	if (pf == PIXEL_BLT_ONLY || pf >= PIXEL_FORMAT_MAX) {
		efi_err("Invalid PixelFormat\n");
		return cur_mode;
	}

	return cmdline.mode;
}

static u32 choose_mode(efi_graphics_output_protocol_t *gop,
		       bool (*match)(const efi_graphics_output_mode_info_t *, u32, void *),
		       void *ctx)
{
	efi_graphics_output_protocol_mode_t *mode = efi_table_attr(gop, mode);
	u32 max_mode = efi_table_attr(mode, max_mode);

	for (u32 m = 0; m < max_mode; m++) {
		efi_graphics_output_mode_info_t *info __free(efi_pool) = NULL;
		unsigned long info_size;
		efi_status_t status;

		status = efi_call_proto(gop, query_mode, m, &info_size, &info);
		if (status != EFI_SUCCESS)
			continue;

		if (match(info, m, ctx))
			return m;
	}
	return (unsigned long)ctx;
}

static u8 pixel_bpp(int pixel_format, efi_pixel_bitmask_t pixel_info)
{
	if (pixel_format == PIXEL_BIT_MASK) {
		u32 mask = pixel_info.red_mask | pixel_info.green_mask |
			   pixel_info.blue_mask | pixel_info.reserved_mask;
		if (!mask)
			return 0;
		return __fls(mask) - __ffs(mask) + 1;
	} else
		return 32;
}

static bool match_res(const efi_graphics_output_mode_info_t *info, u32 mode, void *ctx)
{
	efi_pixel_bitmask_t pi = info->pixel_information;
	int pf = info->pixel_format;

	if (pf == PIXEL_BLT_ONLY || pf >= PIXEL_FORMAT_MAX)
		return false;

	return cmdline.res.width == info->horizontal_resolution &&
	       cmdline.res.height == info->vertical_resolution &&
	       (cmdline.res.format < 0 || cmdline.res.format == pf) &&
	       (!cmdline.res.depth || cmdline.res.depth == pixel_bpp(pf, pi));
}

static u32 choose_mode_res(efi_graphics_output_protocol_t *gop)
{
	efi_graphics_output_protocol_mode_t *mode = efi_table_attr(gop, mode);
	unsigned long cur_mode = efi_table_attr(mode, mode);

	if (match_res(efi_table_attr(mode, info), cur_mode, NULL))
		return cur_mode;

	return choose_mode(gop, match_res, (void *)cur_mode);
}

struct match {
	u32	mode;
	u32	area;
	u8	depth;
};

static bool match_auto(const efi_graphics_output_mode_info_t *info, u32 mode, void *ctx)
{
	u32 area = info->horizontal_resolution * info->vertical_resolution;
	efi_pixel_bitmask_t pi = info->pixel_information;
	int pf = info->pixel_format;
	u8 depth = pixel_bpp(pf, pi);
	struct match *m = ctx;

	if (pf == PIXEL_BLT_ONLY || pf >= PIXEL_FORMAT_MAX)
		return false;

	if (area > m->area || (area == m->area && depth > m->depth))
		*m = (struct match){ mode, area, depth };

	return false;
}

static u32 choose_mode_auto(efi_graphics_output_protocol_t *gop)
{
	struct match match = {};

	choose_mode(gop, match_auto, &match);

	return match.mode;
}

static bool match_list(const efi_graphics_output_mode_info_t *info, u32 mode, void *ctx)
{
	efi_pixel_bitmask_t pi = info->pixel_information;
	u32 cur_mode = (unsigned long)ctx;
	int pf = info->pixel_format;
	const char *dstr;
	u8 depth = 0;
	bool valid;

	valid = !(pf == PIXEL_BLT_ONLY || pf >= PIXEL_FORMAT_MAX);

	switch (pf) {
	case PIXEL_RGB_RESERVED_8BIT_PER_COLOR:
		dstr = "rgb";
		break;
	case PIXEL_BGR_RESERVED_8BIT_PER_COLOR:
		dstr = "bgr";
		break;
	case PIXEL_BIT_MASK:
		dstr = "";
		depth = pixel_bpp(pf, pi);
		break;
	case PIXEL_BLT_ONLY:
		dstr = "blt";
		break;
	default:
		dstr = "xxx";
		break;
	}

	efi_printk("Mode %3u %c%c: Resolution %ux%u-%s%.0hhu\n",
		    mode,
		    (mode == cur_mode) ? '*' : ' ',
		    !valid ? '-' : ' ',
		    info->horizontal_resolution,
		    info->vertical_resolution,
		    dstr, depth);

	return false;
}

static u32 choose_mode_list(efi_graphics_output_protocol_t *gop)
{
	efi_graphics_output_protocol_mode_t *mode = efi_table_attr(gop, mode);
	unsigned long cur_mode = efi_table_attr(mode, mode);
	u32 max_mode = efi_table_attr(mode, max_mode);
	efi_input_key_t key;
	efi_status_t status;

	efi_printk("Available graphics modes are 0-%u\n", max_mode-1);
	efi_puts("  * = current mode\n"
		 "  - = unusable mode\n");

	choose_mode(gop, match_list, (void *)cur_mode);

	efi_puts("\nPress any key to continue (or wait 10 seconds)\n");
	status = efi_wait_for_key(10 * EFI_USEC_PER_SEC, &key);
	if (status != EFI_SUCCESS && status != EFI_TIMEOUT) {
		efi_err("Unable to read key, continuing in 10 seconds\n");
		efi_bs_call(stall, 10 * EFI_USEC_PER_SEC);
	}

	return cur_mode;
}

static void set_mode(efi_graphics_output_protocol_t *gop)
{
	efi_graphics_output_protocol_mode_t *mode;
	u32 cur_mode, new_mode;

	switch (cmdline.option) {
	case EFI_CMDLINE_MODE_NUM:
		new_mode = choose_mode_modenum(gop);
		break;
	case EFI_CMDLINE_RES:
		new_mode = choose_mode_res(gop);
		break;
	case EFI_CMDLINE_AUTO:
		new_mode = choose_mode_auto(gop);
		break;
	case EFI_CMDLINE_LIST:
		new_mode = choose_mode_list(gop);
		break;
	default:
		return;
	}

	mode = efi_table_attr(gop, mode);
	cur_mode = efi_table_attr(mode, mode);

	if (new_mode == cur_mode)
		return;

	if (efi_call_proto(gop, set_mode, new_mode) != EFI_SUCCESS)
		efi_err("Failed to set requested mode\n");
}

static void find_bits(u32 mask, u8 *pos, u8 *size)
{
	if (!mask) {
		*pos = *size = 0;
		return;
	}

	/* UEFI spec guarantees that the set bits are contiguous */
	*pos  = __ffs(mask);
	*size = __fls(mask) - *pos + 1;
}

static void setup_screen_info(struct screen_info *si, const efi_graphics_output_protocol_t *gop)
{
	const efi_graphics_output_protocol_mode_t *mode = efi_table_attr(gop, mode);
	const efi_graphics_output_mode_info_t *info = efi_table_attr(mode, info);

	si->orig_video_isVGA = VIDEO_TYPE_EFI;

	si->lfb_width  = info->horizontal_resolution;
	si->lfb_height = info->vertical_resolution;

	efi_set_u64_split(efi_table_attr(mode, frame_buffer_base),
			  &si->lfb_base, &si->ext_lfb_base);
	if (si->ext_lfb_base)
		si->capabilities |= VIDEO_CAPABILITY_64BIT_BASE;
	si->pages = 1;

	if (info->pixel_format == PIXEL_BIT_MASK) {
		find_bits(info->pixel_information.red_mask, &si->red_pos, &si->red_size);
		find_bits(info->pixel_information.green_mask, &si->green_pos, &si->green_size);
		find_bits(info->pixel_information.blue_mask, &si->blue_pos, &si->blue_size);
		find_bits(info->pixel_information.reserved_mask, &si->rsvd_pos, &si->rsvd_size);
		si->lfb_depth = si->red_size + si->green_size + si->blue_size + si->rsvd_size;
		si->lfb_linelength = (info->pixels_per_scan_line * si->lfb_depth) / 8;
	} else {
		if (info->pixel_format == PIXEL_RGB_RESERVED_8BIT_PER_COLOR) {
			si->red_pos   = 0;
			si->blue_pos  = 16;
		} else /* PIXEL_BGR_RESERVED_8BIT_PER_COLOR */ {
			si->blue_pos  = 0;
			si->red_pos   = 16;
		}

		si->green_pos = 8;
		si->rsvd_pos  = 24;
		si->red_size = 8;
		si->green_size = 8;
		si->blue_size = 8;
		si->rsvd_size = 8;
		si->lfb_depth = 32;
		si->lfb_linelength = info->pixels_per_scan_line * 4;
	}

	si->lfb_size = si->lfb_linelength * si->lfb_height;
	si->capabilities |= VIDEO_CAPABILITY_SKIP_QUIRKS;
}

static void setup_edid_info(struct edid_info *edid, u32 gop_size_of_edid, u8 *gop_edid)
{
	if (!gop_edid || gop_size_of_edid < 128)
		memset(edid->dummy, 0, sizeof(edid->dummy));
	else
		memcpy(edid->dummy, gop_edid, min(gop_size_of_edid, sizeof(edid->dummy)));
}

static efi_handle_t find_handle_with_primary_gop(unsigned long num, const efi_handle_t handles[],
						 efi_graphics_output_protocol_t **found_gop)
{
	efi_graphics_output_protocol_t *first_gop;
	efi_handle_t h, first_gop_handle;

	first_gop_handle = NULL;
	first_gop = NULL;

	for_each_efi_handle(h, handles, num) {
		efi_status_t status;

		efi_graphics_output_protocol_t *gop;
		efi_graphics_output_protocol_mode_t *mode;
		efi_graphics_output_mode_info_t *info;
		void *dummy = NULL;

		status = efi_bs_call(handle_protocol, h,
				     &EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID,
				     (void **)&gop);
		if (status != EFI_SUCCESS)
			continue;

		mode = efi_table_attr(gop, mode);
		info = efi_table_attr(mode, info);
		if (info->pixel_format == PIXEL_BLT_ONLY ||
		    info->pixel_format >= PIXEL_FORMAT_MAX)
			continue;

		/*
		 * Systems that use the UEFI Console Splitter may
		 * provide multiple GOP devices, not all of which are
		 * backed by real hardware. The workaround is to search
		 * for a GOP implementing the ConOut protocol, and if
		 * one isn't found, to just fall back to the first GOP.
		 *
		 * Once we've found a GOP supporting ConOut,
		 * don't bother looking any further.
		 */
		status = efi_bs_call(handle_protocol, h,
				     &EFI_CONSOLE_OUT_DEVICE_GUID, &dummy);
		if (status == EFI_SUCCESS) {
			if (found_gop)
				*found_gop = gop;
			return h;
		} else if (!first_gop_handle) {
			first_gop_handle = h;
			first_gop = gop;
		}
	}

	if (found_gop)
		*found_gop = first_gop;
	return first_gop_handle;
}

efi_status_t efi_setup_graphics(struct screen_info *si, struct edid_info *edid)
{
	efi_handle_t *handles __free(efi_pool) = NULL;
	efi_handle_t handle;
	efi_graphics_output_protocol_t *gop;
	efi_status_t status;
	unsigned long num;

	status = efi_bs_call(locate_handle_buffer, EFI_LOCATE_BY_PROTOCOL,
			      &EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID, NULL, &num,
			      &handles);
	if (status != EFI_SUCCESS)
		return status;

	handle = find_handle_with_primary_gop(num, handles, &gop);
	if (!handle)
		return EFI_NOT_FOUND;

	/* Change mode if requested */
	set_mode(gop);

	/* EFI framebuffer */
	if (si)
		setup_screen_info(si, gop);

	/* Display EDID for primary GOP */
	if (edid) {
		efi_edid_discovered_protocol_t *discovered_edid;
		efi_edid_active_protocol_t *active_edid;
		u32 gop_size_of_edid = 0;
		u8 *gop_edid = NULL;

		status = efi_bs_call(handle_protocol, handle, &EFI_EDID_ACTIVE_PROTOCOL_GUID,
				     (void **)&active_edid);
		if (status == EFI_SUCCESS) {
			gop_size_of_edid = efi_table_attr(active_edid, size_of_edid);
			gop_edid = efi_table_attr(active_edid, edid);
		} else {
			status = efi_bs_call(handle_protocol, handle,
					     &EFI_EDID_DISCOVERED_PROTOCOL_GUID,
					     (void **)&discovered_edid);
			if (status == EFI_SUCCESS) {
				gop_size_of_edid = efi_table_attr(discovered_edid, size_of_edid);
				gop_edid = efi_table_attr(discovered_edid, edid);
			}
		}

		setup_edid_info(edid, gop_size_of_edid, gop_edid);
	}

	return EFI_SUCCESS;
}