vesa: live BIOS patching for Intel video devices

The VESA standard does not define any way for software to set a custom
video mode, which means normally we would be constrained to whichever
modes the video card manufacturer decided to provide. However, since we
run the BIOS in an emulated environment, it is possible (and even quite
easy) to patch it and inject any video mode we want, provided we know
the format to use and where to put the info in.

This approach was used in the NewOS VESA driver, as well as in
915resolution (a tool that predates the availability of native drivers
for Linux for Intel videocards). Later on it was also used in Chameleon
and Clover, bootloaders that are used for hackintoshes (running MacOS on
unsupported hardware).

This commit implements full support for Intel cards only, AMD and NVidia
will be added later (but there is preliminary code to detect them)

Change-Id: I2c528ba18b3863f486da694860a10761efcbfb3f
Reviewed-on: https://review.haiku-os.org/c/haiku/+/4624
Tested-by: Commit checker robot <no-reply+buildbot@haiku-os.org>
Reviewed-by: waddlesplash <waddlesplash@gmail.com>
This commit is contained in:
Adrien Destugues 2021-10-20 12:41:19 +02:00 committed by waddlesplash
parent 6804f6c764
commit 1005a27603
7 changed files with 287 additions and 5 deletions

View File

@ -23,8 +23,15 @@ struct vesa_mode {
uint8 bits_per_pixel;
};
enum bios_type_enum {
kUnknownBiosType = 0,
kIntelBiosType,
kNVidiaBiosType,
kAtomBiosType1,
kAtomBiosType2
};
struct vesa_shared_info {
int32 type;
area_id mode_list_area; // area containing display mode list
uint32 mode_count;
display_mode current_mode;
@ -40,6 +47,10 @@ struct vesa_shared_info {
edid1_info edid_info;
bool has_edid;
bios_type_enum bios_type;
uint16 mode_table_offset;
// Atombios only: offset to the table of video modes in the bios, used for patching in
// extra video modes.
uint32 dpms_capabilities;
};
@ -53,6 +64,7 @@ enum {
VESA_GET_DPMS_MODE,
VESA_SET_DPMS_MODE,
VESA_SET_INDEXED_COLORS,
VESA_SET_CUSTOM_DISPLAY_MODE,
VGA_PLANAR_BLIT,
};

View File

@ -19,7 +19,7 @@ typedef struct accelerant_info {
area_id mode_list_area;
// cloned list of standard display modes
display_mode *mode_list;
uint16 current_mode;
int16 current_mode; // index in the mode_list, or -1 if using a custom mode
vesa_mode *vesa_modes;
} accelerant_info;

View File

@ -13,6 +13,7 @@
#include "accelerant_protos.h"
#include "accelerant.h"
#include "utility.h"
#include "vesa_info.h"
//#define TRACE_MODE
@ -53,6 +54,11 @@ is_mode_supported(display_mode* mode)
{
vesa_mode* modes = gInfo->vesa_modes;
if (gInfo->shared_info->bios_type == kIntelBiosType) {
// We know how to patch the BIOS, so we can set any mode we want
return true;
}
for (uint32 i = gInfo->shared_info->vesa_mode_count; i-- > 0;) {
// search mode in VESA mode list
// TODO: list is ordered, we could use binary search
@ -136,7 +142,8 @@ vesa_propose_display_mode(display_mode* target, const display_mode* low,
{
TRACE(("vesa_propose_display_mode()\n"));
// just search for the specified mode in the list
// Search for the specified mode in the list. If it's in there, we don't need a custom mode and
// we just normalize it to the info provided by the VESA BIOS.
for (uint32 i = 0; i < gInfo->shared_info->mode_count; i++) {
display_mode* current = &gInfo->mode_list[i];
@ -149,6 +156,17 @@ vesa_propose_display_mode(display_mode* target, const display_mode* low,
*target = *current;
return B_OK;
}
if (gInfo->shared_info->bios_type == kIntelBiosType) {
// The driver says it knows the BIOS type, and therefore how to patch it to apply custom
// modes. However, it only knows how to do so for 32bit modes.
// TODO: for nVidia there is actually an hardcoded list of possible modes (because no one
// figured out how to generate the required values for an arbitrary mode), so we should
// check against that.
if (target->space == B_RGB32)
return B_OK;
}
return B_BAD_VALUE;
}
@ -163,7 +181,7 @@ vesa_set_display_mode(display_mode* _mode)
return B_BAD_VALUE;
vesa_mode* modes = gInfo->vesa_modes;
for (uint32 i = gInfo->shared_info->vesa_mode_count; i-- > 0;) {
for (int32 i = gInfo->shared_info->vesa_mode_count; i-- > 0;) {
// search mode in VESA mode list
// TODO: list is ordered, we could use binary search
if (modes[i].width == mode.virtual_width
@ -179,7 +197,14 @@ vesa_set_display_mode(display_mode* _mode)
}
}
return B_UNSUPPORTED;
// If the mode is not found in the list of standard mode, live patch the BIOS to get it anyway
status_t result = ioctl(gInfo->device, VESA_SET_CUSTOM_DISPLAY_MODE,
&mode, sizeof(display_mode));
if (result == B_OK) {
gInfo->current_mode = -1;
}
return result;
}

View File

@ -1,7 +1,10 @@
SubDir HAIKU_TOP src add-ons kernel drivers graphics vesa ;
UsePrivateHeaders [ FDirName graphics common ] ;
UsePrivateHeaders [ FDirName graphics radeon_hd atombios ] ;
UsePrivateHeaders [ FDirName graphics vesa ] ;
UseHeaders [ FDirName $(HAIKU_TOP) headers compatibility gnu ]
[ FDirName $(HAIKU_TOP) headers posix ] : true ;
UsePrivateKernelHeaders ;
KernelAddon vesa :

View File

@ -137,6 +137,18 @@ device_ioctl(void* cookie, uint32 msg, void* buffer, size_t bufferLength)
return vesa_set_display_mode(*info, mode);
}
case VESA_SET_CUSTOM_DISPLAY_MODE:
{
if (bufferLength != sizeof(display_mode))
return B_BAD_VALUE;
display_mode mode;
if (user_memcpy(&mode, buffer, sizeof(display_mode)) != B_OK)
return B_BAD_ADDRESS;
return vesa_set_custom_display_mode(*info, mode);
}
case VESA_GET_DPMS_MODE:
{
if (bufferLength != sizeof(uint32))

View File

@ -7,6 +7,7 @@
#include "vesa_private.h"
#include "vesa.h"
#define _GNU_SOURCE
#include <string.h>
#include <drivers/bios.h>
@ -20,6 +21,8 @@
#include "utility.h"
#include "vesa_info.h"
#include "atombios.h"
static bios_module_info* sBIOSModule;
@ -276,6 +279,65 @@ vbe_set_bits_per_gun(vesa_info& info, uint8 bits)
}
/*! Identify the BIOS type if it's one of the common ones, and locate where the BIOS store its
* allowed video modes table. We can then patch this table to add extra video modes that the
* manufacturer didn't allow.
*/
static void
vbe_identify_bios(vesa_shared_info* sharedInfo)
{
// TODO vbe_call_prepare/vbe_call_finish is a costly operation (loading and unloading the
// BIOS module all the time). We should try to do it less often.
bios_state* state;
status_t status = vbe_call_prepare(&state);
if (status != B_OK)
return;
// Get a pointer to the BIOS
const uintptr_t kBiosBase = 0xc0000;
uint8_t* bios = (uint8_t*)sBIOSModule->virtual_address(state, kBiosBase);
const size_t kAtomBiosHeaderOffset = 0x48;
const char kAtomSignature[] = {'A', 'T', 'O', 'M'};
ATOM_ROM_HEADER* atomRomHeader = (ATOM_ROM_HEADER*)(bios + kAtomBiosHeaderOffset);
sharedInfo->bios_type = kUnknownBiosType;
if (*(uint16*)(bios + 0x44) == 0x8086) {
dprintf(DEVICE_NAME ": detected Intel BIOS\n");
// TODO check if we can find the mode table
sharedInfo->bios_type = kIntelBiosType;
} else if (memcmp(atomRomHeader->uaFirmWareSignature, kAtomSignature, 4) == 0) {
dprintf(DEVICE_NAME ": detected ATOM BIOS\n");
ATOM_MASTER_DATA_TABLE* masterDataTable = (ATOM_MASTER_DATA_TABLE*)(bios
+ atomRomHeader->usMasterDataTableOffset);
dprintf(DEVICE_NAME ": list of data tables: %p", &masterDataTable->ListOfDataTables);
ATOM_ANALOG_TV_INFO* standardVesaTable = (ATOM_ANALOG_TV_INFO*)(bios
+ masterDataTable->ListOfDataTables.StandardVESA_Timing);
dprintf(DEVICE_NAME ": std_vesa: %p", standardVesaTable);
if (standardVesaTable->aModeTimings == NULL) {
dprintf(DEVICE_NAME ": unable to locate the mode table\n");
} else {
sharedInfo->bios_type = kAtomBiosType1;
// TODO detect kAtomBiosType2
// TODO set sharedInfo->mode_table_offset
}
} else if (memmem(bios, 512, "NVID", 4) != NULL) {
dprintf(DEVICE_NAME ": detected nVidia BIOS\n");
// TODO check if we can find the mode table
sharedInfo->bios_type = kNVidiaBiosType;
} else {
dprintf(DEVICE_NAME ": unknown BIOS type, custom video modes will not be available\n");
}
vbe_call_finish(state);
}
/*! Remaps the frame buffer if necessary; if we've already mapped the complete
frame buffer, there is no need to map it again.
*/
@ -402,6 +464,8 @@ vesa_init(vesa_info& info)
memcpy(&sharedInfo.edid_info, edidInfo, sizeof(edid1_info));
}
vbe_identify_bios(&sharedInfo);
vbe_get_dpms_capabilities(info.vbe_dpms_capabilities,
sharedInfo.dpms_capabilities);
if (bufferInfo->depth <= 8)
@ -471,6 +535,171 @@ out:
}
status_t
vbe_patch_intel_bios(bios_state* state, display_mode& mode)
{
edid1_detailed_timing_raw timing;
timing.pixel_clock = mode.timing.pixel_clock / 10;
timing.h_active = mode.timing.h_display & 0xFF;
timing.h_active_high = (mode.timing.h_display >> 8) & 0xF;
uint16 h_blank = mode.timing.h_total - mode.timing.h_display;
timing.h_blank = h_blank & 0xff;
timing.h_blank_high = (h_blank >> 8) & 0xF;
timing.v_active = mode.timing.v_display & 0xFF;
timing.v_active_high = (mode.timing.v_display >> 8) & 0xF;
uint16 v_blank = mode.timing.v_total - mode.timing.v_display;
timing.v_blank = v_blank & 0xff;
timing.v_blank_high = (v_blank >> 8) & 0xF;
uint16 h_sync_off = mode.timing.h_sync_start - mode.timing.h_display;
timing.h_sync_off = h_sync_off & 0xFF;
timing.h_sync_off_high = (h_sync_off >> 8) & 0x3;
uint16 h_sync_width = mode.timing.h_sync_end - mode.timing.h_sync_start;
timing.h_sync_width = h_sync_width & 0xFF;
timing.h_sync_width_high = (h_sync_width >> 8) & 0x3;
uint16 v_sync_off = mode.timing.v_sync_start - mode.timing.v_display;
timing.v_sync_off = v_sync_off & 0xF;
timing.v_sync_off_high = (v_sync_off >> 4) & 0x3;
uint16 v_sync_width = mode.timing.v_sync_end - mode.timing.v_sync_start;
timing.v_sync_width = v_sync_width & 0xF;
timing.v_sync_width_high = (v_sync_width >> 4) & 0x3;
timing.h_size = 0;
timing.v_size = 0;
timing.h_size_high = 0;
timing.v_size_high = 0;
timing.h_border = 0;
timing.v_border = 0;
timing.interlaced = 0;
timing.stereo = 0;
timing.sync = 3;
timing.misc = 0;
timing.stereo_il = 0;
static const uint8 knownMode[] = { 0x64, 0x19, 0x00, 0x40, 0x41, 0x00, 0x26, 0x30, 0x18,
0x88, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18};
// This is the EDID description for a standard 1024x768 timing. We will find and replace
// all occurences of it in the BIOS with our custom mode.
// Get a pointer to the BIOS
const uintptr_t kBiosBase = 0xc0000;
const size_t kBiosSize = 0x10000;
uint8_t* bios = (uint8_t*)sBIOSModule->virtual_address(state, kBiosBase);
uint8_t* biosEnd = bios + kBiosSize;
int replacementCount = 0;
while (bios < biosEnd) {
bios = (uint8_t*)memmem(bios, biosEnd - bios, knownMode, sizeof(knownMode));
if (bios == NULL)
break;
memcpy(bios, &timing, sizeof(timing));
bios++; // Skip one byte so the next memmem finds another occurence
replacementCount++;
}
dprintf(DEVICE_NAME ": patched custom mode in %d locations\n", replacementCount);
// Did we manage to find a mode descriptor to replace?
if (replacementCount == 0)
return B_NOT_SUPPORTED;
return B_OK;
}
status_t
vesa_set_custom_display_mode(vesa_info& info, display_mode& mode)
{
if (info.shared_info->bios_type == kUnknownBiosType)
return B_NOT_SUPPORTED;
// Prepare BIOS environment
bios_state* state;
status_t status = vbe_call_prepare(&state);
if (status != B_OK)
return status;
int32 modeIndex; // Index of the mode that will be patched
// Patch bios to inject custom video mode
switch (info.shared_info->bios_type) {
case kIntelBiosType:
status = vbe_patch_intel_bios(state, mode);
// The patch replaces the 1024x768 modes with our custom resolution. We can then use
// mode 0x118 which is the standard VBE2 mode for 1024x768 at 32 bits per pixel, and
// know it will use our new timings.
// TODO use modes 105 (256 colors), 116 (15 bit) and 117 (16 bit) depending on the
// requested colorspace. They should work too.
// TODO ideally locate the 1024x768 modes from the mode list in info.modes[], instead
// of hardcoding its number, in case the BIOS does not use VBE2 standard numbering.
modeIndex = 0x118;
break;
#if 0
case kNVidiaBiosType:
status = vbe_patch_nvidia_bios(state, mode);
break;
case kAtomBiosType1:
status = vbe_patch_atom1_bios(state, mode);
break;
case kAtomBiosType2:
status = vbe_patch_atom2_bios(state, mode);
break;
#endif
default:
status = B_NOT_SUPPORTED;
break;
}
if (status != B_OK)
goto out;
// Get mode information
struct vbe_mode_info modeInfo;
status = vbe_get_mode_info(state, modeIndex, &modeInfo);
if (status != B_OK) {
dprintf(DEVICE_NAME ": vesa_set_custom_display_mode(): cannot get mode info\n");
goto out;
}
dprintf(DEVICE_NAME ": custom mode resolution %dx%d\n", modeInfo.width, modeInfo.height);
// Set mode
status = vbe_set_mode(state, modeIndex);
if (status != B_OK) {
dprintf(DEVICE_NAME ": vesa_set_custom_display_mode(): cannot set mode\n");
goto out;
}
if (info.modes[modeIndex].bits_per_pixel <= 8)
vbe_set_bits_per_gun(state, info, 8);
// Map new frame buffer if necessary
status = remap_frame_buffer(info, modeInfo.physical_base, modeInfo.width,
modeInfo.height, modeInfo.bits_per_pixel, modeInfo.bytes_per_row,
false);
if (status == B_OK) {
// Update shared frame buffer information
info.shared_info->current_mode.virtual_width = modeInfo.width;
info.shared_info->current_mode.virtual_height = modeInfo.height;
info.shared_info->current_mode.space = get_color_space_for_depth(
modeInfo.bits_per_pixel);
}
out:
vbe_call_finish(state);
return status;
}
status_t
vesa_get_dpms_mode(vesa_info& info, uint32& mode)
{

View File

@ -39,6 +39,7 @@ struct vesa_info {
extern status_t vesa_init(vesa_info& info);
extern void vesa_uninit(vesa_info& info);
extern status_t vesa_set_display_mode(vesa_info& info, uint32 mode);
extern status_t vesa_set_custom_display_mode(vesa_info& info, display_mode& mode);
extern status_t vesa_get_dpms_mode(vesa_info& info, uint32& mode);
extern status_t vesa_set_dpms_mode(vesa_info& info, uint32 mode);
extern status_t vesa_set_indexed_colors(vesa_info& info, uint8 first,