/* $NetBSD: vbe.c,v 1.10 2019/12/15 03:38:17 christos Exp $ */

/*-
* Copyright (c) 2009 Jared D. McNeill <[email protected]>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
*    notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
*    notice, this list of conditions and the following disclaimer in the
*    documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
* ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

/*
* VESA BIOS Extensions routines
*/

#include <lib/libsa/stand.h>
#include <lib/libkern/libkern.h>
#include <machine/bootinfo.h>
#include "libi386.h"
#include "vbe.h"

extern const uint8_t rasops_cmap[];
static uint8_t *vbe_edid = NULL;
static int vbe_edid_valid = 0;

static struct _vbestate {
       int             available;
       int             modenum;
} vbestate;

/*
* https://pdos.csail.mit.edu/6.828/2018/readings/hardware/vbe3.pdf
* p32
*/
#define VBE_MODEATTR_MODE_HARDWARE_SUPPORTED            0x0001u
#define VBE_MODEATTR_RESERVED_1                         0x0002u
#define VBE_MODEATTR_TTY_OUTPUT_FUNCTIONS_SUPPORTED     0x0004u
#define VBE_MODEATTR_COLOR_MODE                         0x0008u
#define VBE_MODEATTR_GRAPHICS_MODE                      0x0010u
#define VBE_MODEATTR_VGA_COMPATIBLE_MODE                0x0020u
#define VBE_MODEATTR_VGA_COMPATIBLE_WINDOWD_MEMORY_MODE 0x0040u
#define VBE_MODEATTR_LINEAR_FRAME_BUFFER_MODE           0x0080u
#define VBE_MODEATTR_DOUBLE_SCAN_MODE                   0x0100u
#define VBE_MODEATTR_INTERLACED_MODE                    0x0200u
#define VBE_MODEATTR_HARDWARE_TRIPPLE_BUFFERING_SUPPORT 0x0400u
#define VBE_MODEATTR_HARDWARE_STEREOSCOPIC_SUPPORT      0x0800u
#define VBE_MODEATTR_DUAL_DISPLAY_START_ADDRESS_SUPPORT 0x1000u
#define VBE_MODEATTR_RESERVED_2                         0x2000u
#define VBE_MODEATTR_RESERVED_3                         0x4000u
#define VBE_MODEATTR_RESERVED_4                         0x8000u

/*
* p36
*/
#define VBE_MEMMODEL_TEXT               0x00u
#define VBE_MEMMODEL_CGA                0x01u
#define VBE_MEMMODEL_HERCULES           0x02u
#define VBE_MEMMODEL_PLANAR             0x03u
#define VBE_MEMMODEL_PACKED_PIXEL       0x04u
#define VBE_MEMMODEL_NON_CHAIN_4_256    0x05u
#define VBE_MEMMODEL_DIRECT_COLOR       0x06u
#define VBE_MEMMODEL_YUV                0x07u
/* VESA Reserved                        0x08u-0x0fu */
/* OEM Reserved                         0x10u-0xffU */


static int
vbe_mode_is_supported(struct modeinfoblock *mi)
{
       if ((mi->ModeAttributes & VBE_MODEATTR_MODE_HARDWARE_SUPPORTED) == 0)
               return 0;       /* mode not supported by hardware */
       if ((mi->ModeAttributes & VBE_MODEATTR_COLOR_MODE) == 0)
               return 0;       /* only color modes are supported */
       if ((mi->ModeAttributes & VBE_MODEATTR_GRAPHICS_MODE) == 0)
               return 0;       /* text mode */
       if ((mi->ModeAttributes & VBE_MODEATTR_LINEAR_FRAME_BUFFER_MODE) == 0)
               return 0;       /* linear fb not available */
       if (mi->NumberOfPlanes != 1)
               return 0;       /* planar mode not supported */
       if (mi->MemoryModel != VBE_MEMMODEL_PACKED_PIXEL /* Packed pixel */ &&
           mi->MemoryModel != VBE_MEMMODEL_DIRECT_COLOR /* Direct Color */)
               return 0;       /* unsupported pixel format */
       return 1;
}

static bool
vbe_check(void)
{
       if (!vbestate.available) {
               printf("VBE not available\n");
               return false;
       }
       return true;
}

void
vbe_init(void)
{
       struct vbeinfoblock vbe;

       memset(&vbe, 0, sizeof(vbe));
       memcpy(vbe.VbeSignature, "VBE2", 4);
       if (biosvbe_info(&vbe) != 0x004f)
               return;
       if (memcmp(vbe.VbeSignature, "VESA", 4) != 0)
               return;

       vbestate.available = 1;
       vbestate.modenum = 0;
}

int
vbe_available(void)
{
       return vbestate.available;
}

int
vbe_set_palette(const uint8_t *cmap, int slot)
{
       struct paletteentry pe;
       int ret;

       if (!vbe_check())
               return 1;

       pe.Blue = cmap[2] >> 2;
       pe.Green = cmap[1] >> 2;
       pe.Red = cmap[0] >> 2;
       pe.Alignment = 0;

       ret = biosvbe_palette_data(0x0600, slot, &pe);

       return ret == 0x004f ? 0 : 1;
}

int
vbe_set_mode(int modenum)
{
       struct modeinfoblock mi;
       struct btinfo_framebuffer fb;
       int ret, i;

       if (!vbe_check())
               return 1;

       ret = biosvbe_get_mode_info(modenum, &mi);
       if (ret != 0x004f) {
               printf("mode 0x%x invalid\n", modenum);
               return 1;
       }

       if (!vbe_mode_is_supported(&mi)) {
               printf("mode 0x%x not supported\n", modenum);
               return 1;
       }

       ret = biosvbe_set_mode(modenum);
       if (ret != 0x004f) {
               printf("mode 0x%x could not be set\n", modenum);
               return 1;
       }

       /* Setup palette for packed pixel mode */
       if (mi.MemoryModel == 0x04)
               for (i = 0; i < 256; i++)
                       vbe_set_palette(&rasops_cmap[i * 3], i);

       memset(&fb, 0, sizeof(fb));
       fb.physaddr = (uint64_t)mi.PhysBasePtr & 0xffffffff;
       fb.width = mi.XResolution;
       fb.height = mi.YResolution;
       fb.stride = mi.BytesPerScanLine;
       fb.depth = mi.BitsPerPixel;
       fb.flags = 0;
       fb.rnum = mi.RedMaskSize;
       fb.rpos = mi.RedFieldPosition;
       fb.gnum = mi.GreenMaskSize;
       fb.gpos = mi.GreenFieldPosition;
       fb.bnum = mi.BlueMaskSize;
       fb.bpos = mi.BlueFieldPosition;
       fb.vbemode = modenum;

       framebuffer_configure(&fb);

       return 0;
}

int
vbe_commit(void)
{
       int ret = 1;

       if (vbestate.modenum > 0) {
               ret = vbe_set_mode(vbestate.modenum);
               if (ret) {
                       printf("WARNING: failed to set VBE mode 0x%x\n",
                           vbestate.modenum);
                       wait_sec(5);
               }
       }
       return ret;
}

static void *
vbe_farptr(uint32_t farptr)
{
       return VBEPHYPTR((((farptr & 0xffff0000) >> 12) + (farptr & 0xffff)));
}

static int
vbe_parse_mode_str(char *str, int *x, int *y, int *depth)
{
       char *p;

       p = str;
       *x = strtoul(p, NULL, 0);
       if (*x == 0)
               return 0;
       p = strchr(p, 'x');
       if (!p)
               return 0;
       ++p;
       *y = strtoul(p, NULL, 0);
       if (*y == 0)
               return 0;
       p = strchr(p, 'x');
       if (!p)
               *depth = 8;
       else {
               ++p;
               *depth = strtoul(p, NULL, 0);
               if (*depth == 0)
                       return 0;
       }

       return 1;
}

static int
vbe_find_mode_xyd(int x, int y, int depth)
{
       struct vbeinfoblock vbe;
       struct modeinfoblock mi;
       uint32_t farptr;
       uint16_t mode;
       int safety = 0;

       memset(&vbe, 0, sizeof(vbe));
       memcpy(vbe.VbeSignature, "VBE2", 4);
       if (biosvbe_info(&vbe) != 0x004f)
               return 0;
       if (memcmp(vbe.VbeSignature, "VESA", 4) != 0)
               return 0;
       farptr = vbe.VideoModePtr;
       if (farptr == 0)
               return 0;

       while ((mode = *(uint16_t *)vbe_farptr(farptr)) != 0xffff) {
               safety++;
               farptr += 2;
               if (safety == 100)
                       return 0;
               if (biosvbe_get_mode_info(mode, &mi) != 0x004f)
                       continue;
               /* we only care about linear modes here */
               if (vbe_mode_is_supported(&mi) == 0)
                       continue;
               safety = 0;
               if (mi.XResolution == x &&
                   mi.YResolution == y &&
                   mi.BitsPerPixel == depth)
                       return mode;
       }

       return 0;
}

static int
vbe_find_mode(char *str)
{
       int x, y, depth;

       if (!vbe_parse_mode_str(str, &x, &y, &depth))
               return 0;

       return vbe_find_mode_xyd(x, y, depth);
}

static void
vbe_dump_mode(int modenum, struct modeinfoblock *mi)
{
       printf("0x%x=%dx%dx%d", modenum,
           mi->XResolution, mi->YResolution, mi->BitsPerPixel);
}

static int
vbe_get_edid(int *pwidth, int *pheight)
{
       const uint8_t magic[] = EDID_MAGIC;
       int ddc_caps, ret;

       ddc_caps = biosvbe_ddc_caps();
       if (ddc_caps == 0) {
               return 1;
       }

       if (vbe_edid == NULL) {
               vbe_edid = alloc(128);
       }
       if (vbe_edid_valid == 0) {
               ret = biosvbe_ddc_read_edid(0, vbe_edid);
               if (ret != 0x004f)
                       return 1;
               if (memcmp(vbe_edid, magic, sizeof(magic)) != 0)
                       return 1;
               vbe_edid_valid = 1;
       }

       *pwidth = vbe_edid[EDID_DESC_BLOCK + 2] |
           (((int)vbe_edid[EDID_DESC_BLOCK + 4] & 0xf0) << 4);
       *pheight = vbe_edid[EDID_DESC_BLOCK + 5] |
           (((int)vbe_edid[EDID_DESC_BLOCK + 7] & 0xf0) << 4);

       return 0;
}

void
vbe_modelist(void)
{
       struct vbeinfoblock vbe;
       struct modeinfoblock mi;
       uint32_t farptr;
       uint16_t mode;
       int nmodes = 0, safety = 0;
       int ddc_caps, edid_width, edid_height;

       if (!vbe_check())
               return;

       ddc_caps = biosvbe_ddc_caps();
       if (ddc_caps & 3) {
               printf("DDC");
               if (ddc_caps & 1)
                       printf(" [DDC1]");
               if (ddc_caps & 2)
                       printf(" [DDC2]");

               if (vbe_get_edid(&edid_width, &edid_height) != 0)
                       printf(": no EDID information\n");
               else
                       printf(": EDID %dx%d\n", edid_width, edid_height);
       }

       printf("Modes: ");
       memset(&vbe, 0, sizeof(vbe));
       memcpy(vbe.VbeSignature, "VBE2", 4);
       if (biosvbe_info(&vbe) != 0x004f)
               goto done;
       if (memcmp(vbe.VbeSignature, "VESA", 4) != 0)
               goto done;
       farptr = vbe.VideoModePtr;
       if (farptr == 0)
               goto done;

       while ((mode = *(uint16_t *)vbe_farptr(farptr)) != 0xffff) {
               safety++;
               farptr += 2;
               if (safety == 100) {
                       printf("[?] ");
                       break;
               }
               if (biosvbe_get_mode_info(mode, &mi) != 0x004f)
                       continue;
               /* we only care about linear modes here */
               if (vbe_mode_is_supported(&mi) == 0)
                       continue;
               safety = 0;
               if (nmodes % 4 == 0)
                       printf("\n");
               else
                       printf("  ");
               vbe_dump_mode(mode, &mi);
               nmodes++;
       }

done:
       if (nmodes == 0)
               printf("none found");
       printf("\n");
}

void
command_vesa(char *cmd)
{
       char arg[20];
       int modenum, edid_width, edid_height;

       if (!vbe_check())
               return;

       strlcpy(arg, cmd, sizeof(arg));

       if (strcmp(arg, "list") == 0) {
               vbe_modelist();
               return;
       }

       if (strcmp(arg, "disabled") == 0 || strcmp(arg, "off") == 0) {
               vbestate.modenum = 0;
               return;
       }

       if (strcmp(arg, "enabled") == 0 || strcmp(arg, "on") == 0) {
               if (vbe_get_edid(&edid_width, &edid_height) != 0) {
                       modenum = VBE_DEFAULT_MODE;
               } else {
                       modenum = vbe_find_mode_xyd(edid_width, edid_height, 8);
                       if (modenum == 0)
                               modenum = VBE_DEFAULT_MODE;
               }
       } else if (strncmp(arg, "0x", 2) == 0) {
               modenum = strtoul(arg, NULL, 0);
       } else if (strchr(arg, 'x') != NULL) {
               modenum = vbe_find_mode(arg);
               if (modenum == 0) {
                       printf("mode %s not supported by firmware\n", arg);
                       return;
               }
       } else {
               modenum = 0;
       }

       if (modenum >= 0x100) {
               vbestate.modenum = modenum;
               return;
       }

       printf("invalid flag, must be 'on', 'off', 'list', "
           "a display mode, or a VBE mode number\n");
}