root/src/add-ons/kernel/drivers/graphics/vesa/vesa.cpp
/*
 * Copyright 2005-2009, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


#include "vesa_private.h"
#include "vesa.h"

#include <boot_item.h>
#include <driver_settings.h>
#include <frame_buffer_console.h>
#include <util/kernel_cpp.h>
#include <vm/vm.h>

#include "driver.h"
#include "utility.h"
#include "vesa_info.h"


#define LINEAR_ADDRESS(segment, offset) \
                (((addr_t)(segment) << 4) + (addr_t)(offset))
#define SEGMENTED_TO_LINEAR(segmented) \
                LINEAR_ADDRESS((addr_t)(segmented) >> 16, (addr_t)(segmented) & 0xffff)


bios_module_info* sBIOSModule;


/*!     Loads the BIOS module and sets up a state for it. The BIOS module is only
        loaded when we need it, as it is quite a large module.
*/
status_t
vbe_call_prepare(bios_state** state)
{
        status_t status;

        status = get_module(B_BIOS_MODULE_NAME, (module_info**)&sBIOSModule);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": failed to get BIOS module: %s\n",
                        strerror(status));
                return status;
        }

        status = sBIOSModule->prepare(state);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": failed to prepare BIOS state: %s\n",
                        strerror(status));
                put_module(B_BIOS_MODULE_NAME);
        }

        return status;
}


void
vbe_call_finish(bios_state* state)
{
        sBIOSModule->finish(state);
        put_module(B_BIOS_MODULE_NAME);
}


static status_t
find_graphics_card(addr_t frameBuffer, addr_t& base, size_t& size, uint16_t& vendorId,
        uint16_t& productId)
{
        // TODO: when we port this over to the new driver API, this mechanism can be
        // used to find the right device_node
        pci_module_info* pci;
        if (get_module(B_PCI_MODULE_NAME, (module_info**)&pci) != B_OK)
                return B_ERROR;

        pci_info info;
        for (int32 index = 0; pci->get_nth_pci_info(index, &info) == B_OK; index++) {
                if (info.class_base != PCI_display)
                        continue;

                // check PCI BARs
                for (uint32 i = 0; i < 6; i++) {
                        if (info.u.h0.base_registers[i] <= frameBuffer
                                && info.u.h0.base_registers[i] + info.u.h0.base_register_sizes[i]
                                        > frameBuffer) {
                                // found it!
                                base = info.u.h0.base_registers[i];
                                size = info.u.h0.base_register_sizes[i];
                                vendorId = info.vendor_id;
                                productId = info.device_id;

                                put_module(B_PCI_MODULE_NAME);
                                return B_OK;
                        }
                }
        }

        put_module(B_PCI_MODULE_NAME);
        return B_ENTRY_NOT_FOUND;
}


uint32
get_color_space_for_depth(uint32 depth)
{
        switch (depth) {
                case 1:
                        return B_GRAY1;
                case 4:
                        return B_GRAY8;
                                // the app_server is smart enough to translate this to VGA mode
                case 8:
                        return B_CMAP8;
                case 15:
                        return B_RGB15;
                case 16:
                        return B_RGB16;
                case 24:
                        return B_RGB24;
                case 32:
                        return B_RGB32;
        }

        return 0;
}


status_t
vbe_get_mode_info(bios_state* state, uint16 mode, struct vbe_mode_info* modeInfo)
{
        void* vbeModeInfo = sBIOSModule->allocate_mem(state,
                sizeof(struct vbe_mode_info));
        if (vbeModeInfo == NULL)
                return B_NO_MEMORY;
        memset(vbeModeInfo, 0, sizeof(vbe_mode_info));

        uint32 physicalAddress = sBIOSModule->physical_address(state, vbeModeInfo);
        bios_regs regs = {};
        regs.eax = 0x4f01;
        regs.ecx = mode;
        regs.es  = physicalAddress >> 4;
        regs.edi = physicalAddress - (regs.es << 4);

        status_t status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vbe_get_mode_info(%u): BIOS failed: %s\n", mode,
                        strerror(status));
                return status;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vbe_get_mode_info(%u): BIOS returned "
                        "0x%04" B_PRIx32 "\n", mode, regs.eax & 0xffff);
                return B_ENTRY_NOT_FOUND;
        }

        memcpy(modeInfo, vbeModeInfo, sizeof(struct vbe_mode_info));
        return B_OK;
}


status_t
vbe_set_mode(bios_state* state, uint16 mode)
{
        bios_regs regs = {};
        regs.eax = 0x4f02;
        regs.ebx = (mode & SET_MODE_MASK) | SET_MODE_LINEAR_BUFFER;

        status_t status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vbe_set_mode(%u): BIOS failed: %s\n", mode,
                        strerror(status));
                return status;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vbe_set_mode(%u): BIOS returned 0x%04" B_PRIx32
                        "\n", mode, regs.eax & 0xffff);
                return B_ERROR;
        }

        return B_OK;
}


static uint32
vbe_to_system_dpms(uint8 vbeMode)
{
        uint32 mode = 0;
        if ((vbeMode & (DPMS_OFF | DPMS_REDUCED_ON)) != 0)
                mode |= B_DPMS_OFF;
        if ((vbeMode & DPMS_STANDBY) != 0)
                mode |= B_DPMS_STAND_BY;
        if ((vbeMode & DPMS_SUSPEND) != 0)
                mode |= B_DPMS_SUSPEND;

        return mode;
}


static status_t
vbe_get_dpms_capabilities(bios_state* state, uint32& vbeMode, uint32& mode)
{
        // we always return a valid mode
        vbeMode = 0;
        mode = B_DPMS_ON;

        bios_regs regs = {};
        regs.eax = 0x4f10;
        regs.ebx = 0;
        regs.esi = 0;
        regs.edi = 0;

        status_t status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vbe_get_dpms_capabilities(): BIOS failed: %s\n",
                        strerror(status));
                return status;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vbe_get_dpms_capabilities(): BIOS returned "
                        "0x%04" B_PRIx32 "\n", regs.eax & 0xffff);
                return B_ERROR;
        }

        vbeMode = regs.ebx >> 8;
        mode = vbe_to_system_dpms(vbeMode);
        return status;
}


status_t
vbe_set_bits_per_gun(bios_state* state, vesa_info& info, uint8 bits)
{
        info.bits_per_gun = 6;

        bios_regs regs = {};
        regs.eax = 0x4f08;
        regs.ebx = (bits << 8) | 1;

        status_t status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vbe_set_bits_per_gun(): BIOS failed: %s\n",
                        strerror(status));
                return status;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vbe_set_bits_per_gun(): BIOS returned "
                        "0x%04" B_PRIx32 "\n", regs.eax & 0xffff);
                return B_ERROR;
        }

        info.bits_per_gun = regs.ebx >> 8;
        return B_OK;
}


// We used to read the existing mode set by the bootsplash to avoid setting the same mode
// when entering app_server, but this has been disabled. So now there is always a modeset
// done when app_server starts.
#if 0
static status_t
vbe_get_vesa_info(bios_state* state, vesa_info& info)
{
        vbe_info_block* infoHeader = (vbe_info_block*)sBIOSModule->allocate_mem(state, 256);
        phys_addr_t physicalAddress = sBIOSModule->physical_address(state, infoHeader);

        bios_regs regs = {};
        regs.eax = 0x4f00;
        regs.es  = physicalAddress >> 4;
        regs.edi = physicalAddress - (regs.es << 4);

        status_t status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vbe_get_vesa_info(): BIOS failed: %s\n",
                        strerror(status));
                return status;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vbe_get_vesa_info(): BIOS returned "
                        "0x%04" B_PRIx32 "\n", regs.eax & 0xffff);
                return B_ERROR;
        }

        info.shared_info->vram_size = infoHeader->total_memory * 65536;
        strlcpy(info.shared_info->name, (char*)sBIOSModule->virtual_address(state,
                        SEGMENTED_TO_LINEAR(infoHeader->oem_string)), 32);

        return status;
}
#endif


/*!     Remaps the frame buffer if necessary; if we've already mapped the complete
        frame buffer, there is no need to map it again.
*/
status_t
remap_frame_buffer(vesa_info& info, addr_t physicalBase, uint32 width,
        uint32 height, int8 depth, uint32 bytesPerRow, bool initializing)
{
        vesa_shared_info& sharedInfo = *info.shared_info;
        addr_t frameBuffer = info.frame_buffer;

        if (!info.complete_frame_buffer_mapped) {
                addr_t base = physicalBase;
                size_t size = bytesPerRow * height;
                bool remap = !initializing;

                if (info.physical_frame_buffer_size != 0) {
                        // we can map the complete frame buffer
                        base = info.physical_frame_buffer;
                        size = info.physical_frame_buffer_size;
                        remap = true;
                }

                if (remap) {
                        area_id area = map_physical_memory("vesa frame buffer", base,
                                size, B_ANY_KERNEL_ADDRESS, B_KERNEL_READ_AREA | B_KERNEL_WRITE_AREA,
                                (void**)&frameBuffer);
                        if (area < 0)
                                return area;

                        frame_buffer_update(frameBuffer, width, height, depth,
                                bytesPerRow);

                        vm_change_clones_to_null_areas(info.frame_buffer_area);
                        delete_area(info.frame_buffer_area);

                        info.frame_buffer = frameBuffer;
                        info.frame_buffer_area = area;

                        // Turn on write combining for the area
                        vm_set_area_memory_type(area, base, B_WRITE_COMBINING_MEMORY);

                        if (info.physical_frame_buffer_size != 0)
                                info.complete_frame_buffer_mapped = true;
                }
        }

        if (info.complete_frame_buffer_mapped)
                frameBuffer += physicalBase - info.physical_frame_buffer;

        // Update shared frame buffer information
        sharedInfo.bytes_per_row = bytesPerRow;

        return B_OK;
}


//      #pragma mark -


status_t
vesa_init(vesa_info& info)
{
        frame_buffer_boot_info* bufferInfo
                = (frame_buffer_boot_info*)get_boot_item(FRAME_BUFFER_BOOT_INFO, NULL);
        if (bufferInfo == NULL)
                return B_ERROR;

        info.vbe_capabilities = bufferInfo->vesa_capabilities;
        info.complete_frame_buffer_mapped = false;

        // Find out which PCI device we belong to, so that we know its frame buffer
        // size
        uint16_t vendorId = 0;
        uint16_t productId = 0;
        find_graphics_card(bufferInfo->physical_frame_buffer, info.physical_frame_buffer,
                info.physical_frame_buffer_size, vendorId, productId);

        size_t modesSize = 0;
        vesa_mode* modes = (vesa_mode*)get_boot_item(VESA_MODES_BOOT_INFO,
                &modesSize);
        info.modes = modes;

        size_t sharedSize = (sizeof(vesa_shared_info) + 7) & ~7;

        info.shared_area = create_area("vesa shared info",
                (void**)&info.shared_info, B_ANY_KERNEL_ADDRESS,
                ROUND_TO_PAGE_SIZE(sharedSize + modesSize), B_FULL_LOCK,
                B_KERNEL_READ_AREA | B_KERNEL_WRITE_AREA | B_CLONEABLE_AREA);
        if (info.shared_area < 0)
                return info.shared_area;

        vesa_shared_info& sharedInfo = *info.shared_info;

        memset(&sharedInfo, 0, sizeof(vesa_shared_info));

        if (modes != NULL) {
                sharedInfo.vesa_mode_offset = sharedSize;
                sharedInfo.vesa_mode_count = modesSize / sizeof(vesa_mode);

                memcpy((uint8*)&sharedInfo + sharedSize, modes, modesSize);
        }

        info.frame_buffer_area = bufferInfo->area;

        remap_frame_buffer(info, bufferInfo->physical_frame_buffer,
                bufferInfo->width, bufferInfo->height, bufferInfo->depth,
                bufferInfo->bytes_per_row, true);
                // Does not matter if this fails - the frame buffer was already mapped
                // before.

        sharedInfo.current_mode.virtual_width = bufferInfo->width;
        sharedInfo.current_mode.virtual_height = bufferInfo->height;
        sharedInfo.current_mode.space = get_color_space_for_depth(
                bufferInfo->depth);

        edid1_info* edidInfo = (edid1_info*)get_boot_item(VESA_EDID_BOOT_INFO,
                NULL);
        if (edidInfo != NULL) {
                sharedInfo.has_edid = true;
                memcpy(&sharedInfo.edid_info, edidInfo, sizeof(edid1_info));
        }

        bios_state* state;
        status_t status = vbe_call_prepare(&state);
        if (status != B_OK)
                return status;

        // Determine if BIOS patching can be used to inject extra video modes not available in the
        // VESA BIOS. Enable this by default only on cards where it was confirmed to work.
        bool patchingAllowed = false;

        static const struct {
                uint16_t vendor;
                uint16_t device;
        } kSupportedDevices[] = {
                { 0x8086, 0x0046 },
                { 0x8086, 0x0be1 },
        };

        for (size_t i = 0; i < B_COUNT_OF(kSupportedDevices); i++) {
                if (vendorId == kSupportedDevices[i].vendor && productId == kSupportedDevices[i].device) {
                        patchingAllowed = true;
                        break;
                }
        }

        // Allow the user to enable or disable the setting manually if they want to.
        void* settings = load_driver_settings("vesa");
        if (settings != NULL) {
                patchingAllowed = get_driver_boolean_parameter(settings, "bios_patching",
                        patchingAllowed, true);
                unload_driver_settings(settings);
        }

        // Finally, if the setting is enabled, see if we can identify the type of BIOS we are dealing
        // with, and can locate the videomode tables we need to patch.
        if (patchingAllowed)
                vesa_identify_bios(state, &sharedInfo);
        else
                sharedInfo.bios_type = kUnknownBiosType;

        vbe_get_dpms_capabilities(state, info.vbe_dpms_capabilities,
                sharedInfo.dpms_capabilities);
        if (bufferInfo->depth <= 8)
                vbe_set_bits_per_gun(state, info, 8);

        vbe_call_finish(state);

        dprintf(DEVICE_NAME ": vesa_init() completed successfully!\n");
        return B_OK;
}


void
vesa_uninit(vesa_info& info)
{
        dprintf(DEVICE_NAME": vesa_uninit()\n");

        vm_change_clones_to_null_areas(info.frame_buffer_area);
        delete_area(info.frame_buffer_area);
        delete_area(info.shared_area);
}


status_t
vesa_set_display_mode(vesa_info& info, uint32 mode)
{
        if (mode >= info.shared_info->vesa_mode_count)
                return B_ENTRY_NOT_FOUND;

        // Prepare BIOS environment
        bios_state* state;
        status_t status = vbe_call_prepare(&state);
        if (status != B_OK)
                return status;

        // Get mode information
        struct vbe_mode_info modeInfo;
        status = vbe_get_mode_info(state, info.modes[mode].mode, &modeInfo);
        if (status != B_OK) {
                dprintf(DEVICE_NAME": vesa_set_display_mode(): cannot get mode info\n");
                goto out;
        }

        // Set mode
        status = vbe_set_mode(state, info.modes[mode].mode);
        if (status != B_OK) {
                dprintf(DEVICE_NAME": vesa_set_display_mode(): cannot set mode\n");
                goto out;
        }

        if (info.modes[mode].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)
{
        mode = B_DPMS_ON;
                // we always return a valid mode

        // Prepare BIOS environment
        bios_state* state;
        status_t status = vbe_call_prepare(&state);
        if (status != B_OK)
                return status;

        bios_regs regs = {};
        regs.eax = 0x4f10;
        regs.ebx = 2;
        regs.esi = 0;
        regs.edi = 0;

        status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vesa_get_dpms_mode(): BIOS failed: %s\n",
                        strerror(status));
                goto out;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vesa_get_dpms_mode(): BIOS returned "
                        "0x%" B_PRIx32 "\n", regs.eax & 0xffff);
                status = B_ERROR;
                goto out;
        }

        mode = vbe_to_system_dpms(regs.ebx >> 8);

out:
        vbe_call_finish(state);
        return status;
}


status_t
vesa_set_dpms_mode(vesa_info& info, uint32 mode)
{
        // Only let supported modes through
        mode &= info.shared_info->dpms_capabilities;

        uint8 vbeMode = 0;
        if ((mode & B_DPMS_OFF) != 0)
                vbeMode |= DPMS_OFF | DPMS_REDUCED_ON;
        if ((mode & B_DPMS_STAND_BY) != 0)
                vbeMode |= DPMS_STANDBY;
        if ((mode & B_DPMS_SUSPEND) != 0)
                vbeMode |= DPMS_SUSPEND;

        vbeMode &= info.vbe_dpms_capabilities;

        // Prepare BIOS environment
        bios_state* state;
        status_t status = vbe_call_prepare(&state);
        if (status != B_OK)
                return status;

        bios_regs regs = {};
        regs.eax = 0x4f10;
        regs.ebx = (vbeMode << 8) | 1;
        regs.esi = 0;
        regs.edi = 0;

        status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vesa_set_dpms_mode(): BIOS failed: %s\n",
                        strerror(status));
                goto out;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vesa_set_dpms_mode(): BIOS returned "
                        "0x%04" B_PRIx32 "\n", regs.eax & 0xffff);
                status = B_ERROR;
                goto out;
        }

out:
        vbe_call_finish(state);
        return status;
}


status_t
vesa_set_indexed_colors(vesa_info& info, uint8 first, uint8* colors,
        uint16 count)
{
        bios_regs regs = {};
        uint32 shift, physicalAddress;

        if (first + count > 256)
                count = 256 - first;

        // Prepare BIOS environment
        bios_state* state;
        status_t status = vbe_call_prepare(&state);
        if (status != B_OK)
                return status;

        uint8* palette = (uint8*)sBIOSModule->allocate_mem(state, 256 * 4);
        if (palette == NULL) {
                status = B_NO_MEMORY;
                goto out;
        }

        shift = 8 - info.bits_per_gun;

        // convert colors to VESA palette
        for (int32 i = first; i < count; i++) {
                uint8 color[3];
                if (user_memcpy(color, &colors[i * 3], 3) < B_OK) {
                        status = B_BAD_ADDRESS;
                        goto out;
                }

                // order is BGR-
                palette[i * 4 + 0] = color[2] >> shift;
                palette[i * 4 + 1] = color[1] >> shift;
                palette[i * 4 + 2] = color[0] >> shift;
                palette[i * 4 + 3] = 0;
        }

        // set palette
        physicalAddress = sBIOSModule->physical_address(state, palette);
        regs.eax = 0x4f09;
        regs.ebx = 0;
        regs.ecx = count;
        regs.edx = first;
        regs.es  = physicalAddress >> 4;
        regs.edi = physicalAddress - (regs.es << 4);

        status = sBIOSModule->interrupt(state, 0x10, &regs);
        if (status != B_OK) {
                dprintf(DEVICE_NAME ": vesa_set_indexed_colors(): BIOS failed: %s\n",
                        strerror(status));
                goto out;
        }

        if ((regs.eax & 0xffff) != 0x4f) {
                dprintf(DEVICE_NAME ": vesa_set_indexed_colors(): BIOS returned "
                        "0x%04" B_PRIx32 "\n", regs.eax & 0xffff);
                status = B_ERROR;
        }

out:
        vbe_call_finish(state);
        return status;
}