/*
 * (C) 1994 Christoph Niemann (original code)
 *          Christoph.Niemann@linux.org
 * All modifications in this file to the original code are
 * (C) Copyright 1992, ..., 2007 the "DOSEMU-Development-Team".
 *
 * for details see file COPYING.DOSEMU in the DOSEMU distribution
 */

/*
 * s3.c
 *
 * This file contains support for VGA-cards with an S3 chip.
 *
 * The programming information was taken from the file vgadoc3.zip
 * by Finn Thoegersen and from the german computer magazine c't,
 * nov. 1992, p 242.
 *
 * Additional information came from the SuperProbe package of
 * XFree86 version 2.1.1 and from the S3-driver in XFree86.
 */

#define S3_C


#include <stdio.h>
#include <unistd.h>

#include "config.h"
#include "emu.h"
#include "port.h"
#include "video.h"
#include "vc.h"
#include "vga.h"
#include "s3.h"
#include "priv.h"

static int s3_chip = 0;
static int s3_chiprev = 0;
static int s3_memsize = 0;
static int s3_series = 0;
static int s3_Ramdac = 0;
static int s3_8514_base = BASE_8514_1;
#if 0
/* code calling this array is #if 0'ed out too. Don't know why -- Bart */
static unsigned short s3BtLowBits[] = { 0x3C8, 0x3C9, 0x3C6, 0x3C7 };
#endif

#define IS_TRIO (s3_series==S3_928 && (s3_chip&0xC0F0)==0x00E0)

static void out_crt(const int index, const int value)
{
	port_out(index, CRT_I);
	port_out(value, CRT_D);
}

static int in_crt(const int index)
{
	port_out(index, CRT_I);
	return (port_in(CRT_D) & 0xff);
}

static void out_seq(const int index,int value)
{
	value=(value<<8)+index;
	port_out_w(value, 0x3c4);
}
static int in_seq(const int index)
{
	port_out(index, 0x3c4);
	return (port_in(0x3c5) & 0xff);
}

/*
 * These functions are from XFree-86:
 * /mit/server/ddx/x386/common_hw/ATTDac.c
 */
static void setdaccomm(unsigned char value)
{
        port_in(0x3c6);
        port_in(0x3c6);
        port_in(0x3c6);
        port_in(0x3c6);
        port_out(value, 0x3c6);
        port_in(0x3c8);
}

static unsigned char getdaccomm(void)
{
        unsigned char ret;
        port_in(0x3c6);
        port_in(0x3c6);
        port_in(0x3c6);
        port_in(0x3c6);
        ret = port_in(0x3c6);
        port_in(0x3c8);
        return ret;
}

static unsigned char dactocomm(void)
{
        port_in(0x3c6);
        port_in(0x3c6);
        port_in(0x3c6);
        return port_in(0x3c6);
}

#define S + 256

static short s3_crt_regs[0x40] = {
/*	 0   1   2   3   4   5   6   7   8   9   a   b   c   d   e   f */
/* 3 */	 4,  5,  6,  7,  8, -1, -1, -1,  0,  1,  9, 10, 11, -1, -1, -1,
/* 4 */	14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
/* 5 */	30 S,31 S, -1,39 S,32 S,33 S, -1, -1,34 S,35 S,36 S, -1, -1,37 S,38 S, -1,
/* 6 */	40 S,41 S,42 S,43 S,44 S,45 S, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };

#undef S

/*
 * Lock S3's registers.
 * There are more locks, but this should suffice.
 *
 * More complete Extended VGA Register Lock Documentation, as of Ferraro:
 *
 * Register     Bit     Controls Access To:             Function
 * CR33         1       CR7 bits 1 and 6                1=disable write protect
 *                                                        setting of CR11 bit 7
 * CR33         4       Ramdac Register                 1=disable writes
 * CR33         6       Palette/Overscan Registers      1=lock
 * CR34         5       Memory Configuration bit 5      1=lock
 * CR34         7       Misc Register bit 3-2 (Clock)   1=lock
 * CR35         4       Vertical Timing Registers       1=lock
 * CR35         5       Horizontal Timing Registers     1=lock
 *
 * XXXX mostly, need to lock the enhanced command regs on the 805 (and
 * probably below) to avoid display corruption.
 */
static void s3_lock(void)
{
    out_crt(0x39, 0x00);		/* Lock system control regs. */
    out_crt(0x38, 0x00);		/* Lock special regs. */
}

static void s3_lock_enh(void)
{
    if (s3_chip > S3_911)
	out_crt(0x40, in_crt(0x40) & ~0x01);    /* Lock enhanced command regs. */
    s3_lock();
}

/*
 * Unlock S3's registers.
 * There are more locks, but this should suffice.
 */
static void s3_unlock(void)
{
    out_crt(0x38, 0x48);		/* Unlock special regs. */
    out_crt(0x39, 0xa5);		/* Unlock system control regs. */
}

static void s3_unlock_enh(void)
{
    s3_unlock();
    if (s3_chip > S3_911)
	out_crt(0x40, in_crt(0x40) | 0x01);     /* Unlock enhanced command regs. */
}


static void s3_save_ext_regs(u_char xregs[], u_short xregs16[])
{
	int i;
	v_printf("s3_save_ext_regs(): start.\n");

	emu_video_retrace_off();
	/*
	 * Save status of the extensions enable/disable regs
	 */
	xregs[0] = in_crt(0x38);
	xregs[1] = in_crt(0x39);
	/*
	 * Enable extensions
	 */
	s3_unlock_enh();

	xregs[13] = port_in(0x3cc);

	for (i = 0; i < 5; i++)
		xregs[i + 4] = in_crt(0x30 + i);

	xregs[9] = in_crt(0x3a);
	xregs[10] = in_crt(0x3b);
	xregs[11] = in_crt(0x3c);

	for (i = 0; i < 0x10; i++)
		xregs[i + 14] = in_crt(0x40 + i);

	if (s3_chip > S3_911)
	{
		/*
		 * I have an S3-911, so this part is untested.
		 * This part has now been tested with an S3-928 PCI
		 */
		v_printf("s3_save_ext_regs(): saving additional regs\n");

		xregs[30] = in_crt(0x50);
		xregs[31] = in_crt(0x51);

		xregs[32] = in_crt(0x54);
		xregs[33] = in_crt(0x55);

		xregs[34] = in_crt(0x58);
		xregs[35] = in_crt(0x59);
		xregs[36] = in_crt(0x5a);

		xregs[37] = in_crt(0x5d);
		xregs[38] = in_crt(0x5e);

		xregs[39] = in_crt(0x53);

		for (i = 0; i < 6; i++)
			xregs[40 + i] = in_crt(0x60 + i);

		/* s3 trio extended registers */
		/* get S3 ext. SR registers */
                if (IS_TRIO) {
		    xregs[50] = in_seq(0x08);
		    out_seq(0x08, 0x06);	/* unlock extended seq regs */
		    xregs[51] = in_seq(0x09);
		    xregs[52] = in_seq(0x0A);
		    xregs[53] = in_seq(0x0D);
		    xregs[54] = in_seq(0x10);
		    xregs[55] = in_seq(0x11);
		    xregs[56] = in_seq(0x12);
		    xregs[57] = in_seq(0x13);
		    xregs[58] = in_seq(0x15);
		    xregs[59] = in_seq(0x18);
		    out_seq(0x08, xregs[50]); /* restore lock */
		}
	}
	if (s3_Ramdac == S3_NORMAL_DAC)
	{
		v_printf("S3: Saving Ramdac data\n");
		xregs[49] = getdaccomm();
		setdaccomm((xregs[49] | 0x10));
		(void)dactocomm();
		port_out(0x08, 0x3c7);
		xregs[48] = port_in(0x3c8);
		setdaccomm(xregs[49]);
	}

	port_in(0x3da);			/* reset flip-flop */
	port_out(0x36, 0x3c0);
	xregs[12] = port_in(0x3c1);
	port_out(xregs[12], 0x3c0);

	/*
	 * restore the status of the extensions
	 */
	s3_lock_enh();
	out_crt(0x38, xregs[0]);
	out_crt(0x39, xregs[1]);

	xregs[2] = port_in(0x102);
	port_out(1, 0x102);
	if (can_do_root_stuff) {
		priv_iopl(3);
		xregs16[0] = port_in_w(0x8000 | s3_8514_base);	/* CUR_Y */
		xregs16[1] = port_in_w(0x8400 | s3_8514_base);	/* CUR_X */
		xregs16[2] = port_in_w(0x8800 | s3_8514_base);
		xregs16[3] = port_in_w(0x8c00 | s3_8514_base);
		priv_iopl(0);
	} else {
		xregs16[0] = std_port_inw(0x8000 | s3_8514_base);	/* CUR_Y */
		xregs16[1] = std_port_inw(0x8400 | s3_8514_base);	/* CUR_X */
		xregs16[2] = std_port_inw(0x8800 | s3_8514_base);
		xregs16[3] = std_port_inw(0x8c00 | s3_8514_base);
	}
	port_out(xregs[2], 0x102);
	emu_video_retrace_on();

}

static void s3_restore_ext_regs(u_char xregs[], u_short xregs16[])
{
	int i;
        v_printf("s3_restore_ext_regs(): start.\n");

	emu_video_retrace_off();
        /*
         * Enable extensions
         */
 	s3_unlock_enh();

	port_out(xregs[13], 0x3c2);

	for (i = 0; i < 5; i++)
		out_crt(0x30 + i, xregs[i + 4]);

	out_crt(0x3a, xregs[9]);
	out_crt(0x3b, xregs[10]);
	out_crt(0x3c, xregs[11]);

	for (i = 0; i < 0x10; i++)
		out_crt(0x40 + i, xregs[i + 14]);

	if (s3_chip > S3_911)
	{
		v_printf("s3_restore_ext_regs(): restoring additional regs\n");
		out_crt(0x50, xregs[30]);
		out_crt(0x51, xregs[31]);

		out_crt(0x54, xregs[32]);
		out_crt(0x55, xregs[33]);

		out_crt(0x58, xregs[34]);
		out_crt(0x59, xregs[35]);
		out_crt(0x5a, xregs[36]);

		out_crt(0x5d, xregs[37]);
		out_crt(0x5e, xregs[38]);
		out_crt(0x53, xregs[39]);

		for (i = 0; i < 6; i++)
			out_crt(0x60 + i, xregs[40 + i]);

		/* s3 trio extended registers */
                /* set S3 ext. SR registers */
                if (IS_TRIO) {
		     out_seq(0x08, 0x06);    /* unlock extended seq regs */
		     out_seq(0x09, xregs[51]);
		     out_seq(0x0A, xregs[52]);
		     out_seq(0x0D, xregs[53]);
		     out_seq(0x10, xregs[54]);
		     out_seq(0x11, xregs[55]);
		     out_seq(0x12, xregs[56]);
		     out_seq(0x13, xregs[57]);
		     out_seq(0x15, xregs[58]);
		     out_seq(0x18, xregs[59]);
		     out_seq(0x08, xregs[50]); /* restore lock */
		}
	}

	if (s3_Ramdac == S3_NORMAL_DAC)
	{
		int c;
		v_printf("S3: Restoring Ramdac data\n");
		c=getdaccomm();
		setdaccomm( c | 0x10 );
		(void)dactocomm();
		port_out(0x08, 0x3c7);
		port_out(xregs[48], 0x3c8);
		setdaccomm(c);
		setdaccomm(xregs[49]);
	}

	port_in(0x3da);
	port_out(0x36, 0x3c0);
	port_out(xregs[12], 0x3c0);

	s3_lock_enh();
	out_crt(0x38, xregs[0]);
	out_crt(0x39, xregs[1]);

	port_out(1, 0x102);
	if (can_do_root_stuff) {
		priv_iopl(3);
		port_out_w(xregs16[0], 0x8000 | s3_8514_base);
		port_out_w(xregs16[1], 0x8400 | s3_8514_base);
		port_out_w(xregs16[2], 0x8800 | s3_8514_base);
		port_out_w(xregs16[3], 0x8c00 | s3_8514_base);
		priv_iopl(0);
	} else {
		std_port_outw(0x8000 | s3_8514_base, xregs16[0]);
		std_port_outw(0x8400 | s3_8514_base, xregs16[1]);
		std_port_outw(0x8800 | s3_8514_base, xregs16[2]);
		std_port_outw(0x8c00 | s3_8514_base, xregs16[3]);
	}
	port_out(xregs[2], 0x102);
	emu_video_retrace_on();
}

static void s3_set_bank(u_char bank)
{
	/*
	 * For S3 chips the memory bank is always the same for
	 * reading and writing
	 */
	int mode = in_crt(0x38);
	int old_reg;

	v_printf("s3_set_bank(%d)\n", bank);

	out_crt(0x38, 0x48);
	old_reg = in_crt(0x35);
	out_crt(0x35, (bank & 0x0f) + (old_reg & 0xf0));
	if ((bank & 0xf0) && (s3_chip > S3_911))
	{
		/*
		 * This is untested (I don't have > 1 MB VRAM)
		 */
		int mode2 = in_crt(0x39);
		out_crt(0x39, 0xa5);
		old_reg = in_crt(0x51);
		out_crt(0x51, (bank & 0xf0) >> 2);
		out_crt(0x39, mode2);
	}
	out_crt(0x38, mode);
}

static u_char s3_ext_video_port_in(ioport_t port)
{
	int x_reg;

	switch(port)
	{
	case CRT_DC:
	case CRT_DM:
		if (dosemu_regs.regs[CRTI] < 0x70 && dosemu_regs.regs[CRTI] >= 0x30 &&
				(x_reg = s3_crt_regs[dosemu_regs.regs[CRTI] - 0x30]) != -1)
		{
			/*
			 * some registers are available only on `advanced'
			 * s3 cards.
			 */
			if ((x_reg & 0xff) && (s3_chip <= S3_911))
				break;
			x_reg &= 0xff;
			v_printf("S3: Read on CRT_D 0x%02x got 0x%02x\n",
					dosemu_regs.regs[CRTI],
					dosemu_regs.xregs[x_reg]);
			return dosemu_regs.xregs[x_reg];
		}
		break;
	}
	v_printf("S3: Bad Read on port 0x%04x\n", port);
	return 0;
}

static void s3_ext_video_port_out(ioport_t port, u_char value)
{
	int x_reg;

	switch(port & 0x02ff)
	{
	case CRT_DC:
	case CRT_DM:
		if (dosemu_regs.regs[CRTI] < 0x70 && dosemu_regs.regs[CRTI] >= 0x30 &&
				(x_reg = s3_crt_regs[dosemu_regs.regs[CRTI] - 0x30]) != -1)
		{
			/*
			 * some registers are available only on `advanced'
			 * s3 cards.
			 */
			if ((x_reg & 0xff) && (s3_chip <= S3_911))
				break;
			x_reg &= 0xff;
			v_printf("S3: Write on CRT_D 0x%02x of 0x%02x\n",
					dosemu_regs.regs[CRTI], value);
			dosemu_regs.xregs[x_reg] = value;
			return;
		}
		break;
	case BASE_8514_1:
	case BASE_8514_2:
		break;
		switch(port & 0xfc00)
		{
			case 0x8000:	*((unsigned char *) (dosemu_regs.xregs16)) = value;
					v_printf("S3: Write on 8514 reg 0x%04x of 0x%02x\n",
							port, value);
					break;
			case 0x8400:	*((unsigned char *) (&dosemu_regs.xregs16[1])) = value;
					v_printf("S3: Write on 8514 reg 0x%04x of 0x%02x\n",
							port, value);
					break;
			case 0x8800:	*((unsigned char *) (&dosemu_regs.xregs16[2])) = value;
					v_printf("S3: Write on 8514 reg 0x%04x of 0x%02x\n",
							port, value);
					break;
			case 0x8c00:	*((unsigned char *) (&dosemu_regs.xregs16[3])) = value;
					v_printf("S3: Write on 8514 reg 0x%04x of 0x%02x\n",
							port, value);
					break;
		}
		break;
	case BASE_8514_1 + 1:
	case BASE_8514_2 + 1:
		break;
	}
	v_printf("S3: Bad Write on port 0x%04x with value 0x%02x\n", port, value);
}

#if 0
/* code calling this function is #if 0'ed out too. Don't know why -- Bart */
/*
 * this function was taken from XFree86, s3BtCursor.c
 */
static unsigned char s3InBtReg(unsigned short reg)
{
	unsigned char tmp, ret;
	tmp = in_crt(0x55) & 0xFC;
	out_crt(0x55, tmp | ((reg & 0x0C) >> 2));
	ret = port_in(s3BtLowBits[reg & 0x03]);
	out_crt(0x55, tmp);
	return ret;
}

static void s3OutBtReg(unsigned short reg, unsigned char mask, unsigned char data)
{
	unsigned char tmp;
	unsigned char tmp1 = 0x00;

	tmp = in_crt(0x55) & 0xFC;
	out_crt(0x55, tmp | ((reg & 0x0C) >> 2));

	if (mask != 0x00)
		tmp1 = port_in(s3BtLowBits[reg & 0x03]) & mask;
	port_out(tmp1 | data, s3BtLowBits[reg & 0x03]);

	/* Now clear 2 high-order bits so that other things work */
	out_crt(0x55, tmp);
}

static unsigned char s3InBtStatReg(void)
{
   unsigned char tmp, ret;

   tmp = s3InBtReg(BT_COMMAND_REG_0);
   if ((tmp & 0x80) == 0x80) {
      /* Behind Command Register 3 */
      tmp = s3InBtReg(BT_WRITE_ADDR);
      s3OutBtReg(BT_WRITE_ADDR, 0x00, 0x00);
      ret = s3InBtReg(BT_STATUS_REG);
      s3OutBtReg(BT_WRITE_ADDR, 0x00, tmp);
   } else {
      ret = s3InBtReg(BT_STATUS_REG);
   }
   return(ret);
}
#endif

void vga_init_s3(void)
{
	emu_iodev_t io_device;
	int mode, mode2, subtype;
	char *name = NULL;

	save_ext_regs = s3_save_ext_regs;
	restore_ext_regs = s3_restore_ext_regs;
	set_bank_read = s3_set_bank;
	set_bank_write = s3_set_bank;
	ext_video_port_in = s3_ext_video_port_in;
	ext_video_port_out = s3_ext_video_port_out;

	mode = in_crt(0x38);
	out_crt(0x38, 0x48);
	s3_chip = in_crt(0x30);
	s3_chiprev = s3_chip & 0x0F;

	name = NULL;

	switch(s3_chip & 0xF0)
	{
		case 0x80:
			s3_series = S3_911;
			switch(s3_chiprev)
			{
				case 0x01:	name = "911";
						break;
				case 0x02:	name = "911A/924";
						break;
			}
			break;
		case 0x90:	/* 928 */
			s3_series = S3_928;
			switch(s3_chiprev)
			{
				case 0x00:	name = "928C";
						break;
				case 0x01:	name = "928D";
						break;
				case 0x04:
				case 0x05:	name = "928E";
						break;
			}
			break;
		case 0xA0:	/* 801 / 805 */
			subtype = in_crt(0x36) & 0x03;
			if ((subtype & 0x02) == 0)
			{
				/* EISA or VLB => 805 */
				s3_series = S3_805;
				switch (s3_chiprev)
				{
					case 0x00:	name = "805A/B";
							break;
					case 0x02:
					case 0x03:
					case 0x04:	name = "805C";
							break;
					case 0x05:	name = "805D";
							break;
					case 0x08:	name = "805I";
							break;
				}
			}
			else if (subtype == 3)
			{
				/* ISA => 801 */
				s3_series = S3_801;
				switch(s3_chiprev)
				{
					case 0x00:	name = "801A/B";
							break;
					case 0x02:
					case 0x03:
					case 0x04:	name = "801C";
							break;
					case 0x05:	name = "801D";
							break;
					case 0x08:	name = "801I";
							break;
				}
			}
			break;

		case 0xB0:	name = "928";
				s3_series = S3_928;
				break;
		case 0xC0:	name = "864";
				s3_series = S3_928;
				break;
		case 0xD0:	name = "964";
				s3_series = S3_928;
				break;
		case 0xE0:
		case 0xF0:
			s3_chip |= (in_crt(0x2E) << 8);
			s3_chiprev |= (in_crt(0x2F) << 4);
			switch (s3_chip & 0xFFF0) {
				case 0x10E0: name = "TRIO32"; break;
				case 0x11E0: name = "TRIO64"; break;
				case 0x80E0: name = "866"; break;
				case 0x90E0: name = "868"; break;
				case 0xF0E0: name = "968"; break;
			}
			s3_series = S3_928;
			break;
	}

	if (name)
		v_printf("S3 chip 86c%s rev %d detected.\n", name, s3_chiprev);
	else
		v_printf("Unknown S3-chip, type = 0x%x\n", s3_chip);

#if 1
	s3_Ramdac = S3_NORMAL_DAC;
#else
	s3_Ramdac = 0;
	if (s3_series != S3_928)
		s3_Ramdac = S3_NORMAL_DAC;

	if (!s3_Ramdac)
	{
		/* probe for Ramdac Bt485/AT&T20C505 */
		unsigned char tmp1, tmp2;
		out_crt(0x39, 0xA5);
		tmp1 = port_in(S3_DAC_INDEX_REG);
		port_out(0x0f, S3_DAC_INDEX_REG);
		tmp2 = s3InBtStatReg();
		if ((tmp2 & 0x80) == 0x80)
		{
			if ((tmp2 & 0xF0) == 0xD0)
				s3_Ramdac = S3_ATT20C505_DAC;
			else
				s3_Ramdac = S3_BT485_DAC;
		}
		port_out(tmp1, S3_DAC_INDEX_REG);
	}

	if (!s3_Ramdac)
	{
		/* Probe for a Ti3020 */
		unsigned char tmp1, tmp2;
		tmp1 = in_crt(0x55);
		out_crt(0x55, (tmp1 & 0xfc) | 0x01);
		tmp2 = port_in(S3_DAC_INDEX_REG);
		port_out(S3_TI3020_ID2, S3_DAC_INDEX_REG);
		if (port_in(S3_DAC_DATA_REG) == S3_TI3020_ID)
			s3_Ramdac = S3_TI3020_DAC;
		port_out(tmp2, S3_DAC_INDEX_REG);
		out_crt(0x55, tmp1);
	}

	if (!s3_Ramdac)
		s3_Ramdac = S3_NORMAL_DAC;

	v_printf("Detected a ");
	switch(s3_Ramdac)
	{
		case S3_NORMAL_DAC:	v_printf("normal");
					break;
		case S3_ATT20C505_DAC:	v_printf("AT&T 20C505");
					break;
		case S3_BT485_DAC:	v_printf("BrookTree Bt485");
					break;
		case S3_TI3020_DAC:	v_printf("TI ViewPoint 3020");
					break;
	}
	v_printf(" RAMDAC\n");
#endif


	mode2 = in_crt(0x39);
        out_crt(0x39, 0xa5);
	if (in_crt(0x43) & 0x10)
		s3_8514_base = BASE_8514_2;
	else
		s3_8514_base = BASE_8514_1;
	out_crt(0x39, mode2);

	if (in_crt(0x36) & 0x20)
		s3_memsize = 512;
	else
	{
		if (s3_chip > S3_911) switch((in_crt(0x36) & 0xc0) >> 6)
		{
			case 0:	s3_memsize = 4096; break;
			case 1: s3_memsize = 3072; break;
			case 2: s3_memsize = 2048; break;
			case 3: s3_memsize = 1024; break;
		}
		else
			s3_memsize = 1024;
	}

	v_printf("S3 base address: 0x%x\n", s3_8514_base);

	/* register high S3 video ports */
	io_device.irq = EMU_NO_IRQ;
	io_device.fd = -1;
	io_device.handler_name = "std port io";

	io_device.start_addr = 0x8000 | s3_8514_base;
	io_device.end_addr = (0x8000 | s3_8514_base) + 1;
	port_register_handler(io_device, 0);

	io_device.start_addr = 0x8400 | s3_8514_base;
	io_device.end_addr = (0x8400 | s3_8514_base) + 1;
	port_register_handler(io_device, 0);

	io_device.start_addr = 0x8800 | s3_8514_base;
	io_device.end_addr = (0x8800 | s3_8514_base) + 1;
	port_register_handler(io_device, 0);

	io_device.start_addr = 0x8c00 | s3_8514_base;
	io_device.end_addr = (0x8c00 | s3_8514_base) + 1;
	port_register_handler(io_device, 0);

	v_printf("S3 memory size : %d kbyte\n", s3_memsize);

	if (config.gfxmemsize < 0) config.gfxmemsize = s3_memsize;
	v_8514_base = s3_8514_base;
	out_crt(0x38, mode);
}

#undef S3_C
