#include <u.h>
#include <libc.h>
#include <bio.h>

#include "pci.h"
#include "vga.h"

/*
 * 3Dfx.
 */
enum {
	dramInit0		= 0x018/4,
	dramInit1		= 0x01C/4,
	vgaInit0		= 0x028/4,
	pllCtrl0		= 0x040/4,
	pllCtrl1		= 0x044/4,
	pllCtrl2		= 0x048/4,
	dacMode			= 0x04C/4,
	vidProcCfg		= 0x05C/4,
	vidScreenSize		= 0x098/4,
	vidDesktopOverlayStride	= 0x0E8/4,

	Nior			= 0x100/4,
};

typedef struct Tdfx {
	ulong	io;
	Pcidev*	pci;

	ulong	r[Nior];
} Tdfx;

static ulong
io32r(Tdfx* tdfx, int r)
{
	return inportl(tdfx->io+(r*4));
}

static void
io32w(Tdfx* tdfx, int r, ulong l)
{
	outportl(tdfx->io+(r*4), l);
}

static void
snarf(Vga* vga, Ctlr* ctlr)
{
	int i;
	ulong v;
	Tdfx *tdfx;

	if(vga->private == nil){
		tdfx = alloc(sizeof(Tdfx));
		tdfx->pci = vga->pci;
		if(tdfx->pci == nil)
			tdfx->pci = pcimatch(0, 0x121A, 0);
		if(tdfx->pci == nil || tdfx->pci->vid != 0x121A)
			error("%s: not found\n", ctlr->name);
		switch(tdfx->pci->did){
		default:
			error("%s: unknown chip - DID %4.4uX\n",
				ctlr->name, tdfx->pci->did);
			break;
		case 0x0003:		/* Banshee */
			vga->f[1] = 270000000;
			break;
		case 0x0005:		/* Avenger (a.k.a. Voodoo3) */
			vga->f[1] = 300000000;
			break;
		case 0x0009:		/* Voodoo5 */
			vga->f[1] = 350000000;
			break;
		}
		/*
		 * Frequency output of PLL's is given by
		 *	fout = RefFreq*(n+2)/((m+2)*2^p)
		 * where there are 6 bits for m, 8 bits for n
		 * and 2 bits for p (k).
		 */
		vga->m[1] = 64;
		vga->n[1] = 256;
		vga->p[1] = 4;

		v = tdfx->pci->mem[2].bar & ~0x3;
		if(v == 0)
			error("%s: I/O not mapped\n", ctlr->name);
		tdfx->io = v;

		vga->private = tdfx;
	}
	tdfx = vga->private;

	vga->crt[0x1A] = vgaxi(Crtx, 0x1A);
	vga->crt[0x1B] = vgaxi(Crtx, 0x1B);
	for(i = 0; i < Nior; i++)
		tdfx->r[i] = io32r(tdfx, i);

	/*
	 * If SDRAM then there's 16MB memory else it's SGRAM
	 * and can count it based on the power-on straps -
	 * chip size can be 8Mb or 16Mb, and there can be 4 or
	 * 8 of them.
	 */
	vga->vma = tdfx->pci->mem[1].size;
	if(tdfx->r[dramInit1] & 0x40000000)
		vga->vmz = 16*1024*1024;
	else{
		if(tdfx->r[dramInit0] & 0x08000000)
			i = 16*1024*1024/8;
		else
			i = 8*1024*1024/8;
		if(tdfx->r[dramInit0] & 0x04000000)
			i *= 8;
		else
			i *= 4;
		vga->vmz = i;
	}

	ctlr->flag |= Fsnarf;
}

static void
options(Vga*, Ctlr* ctlr)
{
	ctlr->flag |= Hlinear|Hclk2|Foptions;
}

static void
tdfxclock(Vga* vga, Ctlr*)
{
	int d, dmin;
	uint f, m, n, p;

	dmin = vga->f[0];
	for(m = 1; m < vga->m[1]; m++){
		for(n = 1; n < vga->n[1]; n++){
			f = (RefFreq*(n+2))/(m+2);
			for(p = 0; p < vga->p[1]; p++){
				d = vga->f[0] - (f/(1<<p));
				if(d < 0)
					d = -d;
				if(d >= dmin)
					continue;
				dmin = d;
				vga->m[0] = m;
				vga->n[0] = n;
				vga->p[0] = p;
			}
		}
	}
}

static void
init(Vga* vga, Ctlr* ctlr)
{
	int x;
	Mode *mode;
	Tdfx *tdfx;

	mode = vga->mode;
	tdfx = vga->private;

	if(vga->linear && (ctlr->flag & Hlinear))
		ctlr->flag |= Ulinear;

	/*
	 * Clock bits. If the desired video clock is
	 * one of the two standard VGA clocks or 50MHz it can just be
	 * set using bits <3:2> of vga->misc, otherwise we
	 * need to programme the PLL.
	 */
	if(vga->f[0] == 0)
		vga->f[0] = mode->frequency;
	vga->misc &= ~0x0C;
	if(vga->f[0] == VgaFreq0){
		/* nothing to do */;
	}
	else if(vga->f[0] == VgaFreq1)
		vga->misc |= 0x04;
	else if(vga->f[0] == 50000000)
		vga->misc |= 0x08;
	else{
		if(vga->f[0] > vga->f[1])
			error("%s: invalid pclk - %lud\n",
				ctlr->name, vga->f[0]);
		if(vga->f[0] > 135000000 && (ctlr->flag & Hclk2)){
			if(mode->x%16)
				error("%s: f > 135MHz requires (x%%16) == 0\n",
					ctlr->name);
			ctlr->flag |= Uclk2;
		}
		tdfxclock(vga, ctlr);

		tdfx->r[pllCtrl0] = (vga->n[0]<<8)|(vga->m[0]<<2)|vga->p[0];
		vga->misc |= 0x0C;
	}

	/*
	 * Pixel format and memory stride.
	 */
	tdfx->r[vidScreenSize] = (mode->y<<12)|mode->x;
	tdfx->r[vidProcCfg] = 0x00000081;
	switch(mode->z){
	default:
		error("%s: %d-bit mode not supported\n", ctlr->name, mode->z);
		break;
	case 8:
		tdfx->r[vidDesktopOverlayStride] = mode->x;
		break;
	case 16:
		tdfx->r[vidDesktopOverlayStride] = mode->x*2;
		tdfx->r[vidProcCfg] |= 0x00040400;
		break;
	case 32:
		tdfx->r[vidDesktopOverlayStride] = mode->x*4;
		tdfx->r[vidProcCfg] |= 0x000C0400;
		break;
	}
	tdfx->r[vgaInit0] = 0x140;

	/*
	 * Adjust horizontal timing if doing two screen pixels per clock.
	 */
	tdfx->r[dacMode] = 0;
	if(ctlr->flag & Uclk2){
		vga->crt[0x00] = ((mode->ht/2)>>3)-5;
		vga->crt[0x01] = ((mode->x/2)>>3)-1;
		vga->crt[0x02] = ((mode->shb/2)>>3)-1;
	
		x = (mode->ehb/2)>>3;
		vga->crt[0x03] = 0x80|(x & 0x1F);
		vga->crt[0x04] = (mode->shs/2)>>3;
		vga->crt[0x05] = ((mode->ehs/2)>>3) & 0x1F;
		if(x & 0x20)
			vga->crt[0x05] |= 0x80;

		tdfx->r[dacMode] |= 0x01;
		tdfx->r[vidProcCfg] |= 0x04000000;
	}

	/*
	 * Overflow.
	 */
	vga->crt[0x1A] = 0x00;
	if(vga->crt[0x00] & 0x100)
		vga->crt[0x1A] |= 0x01;
	if(vga->crt[0x01] & 0x100)
		vga->crt[0x1A] |= 0x04;
	if(vga->crt[0x03] & 0x100)
		vga->crt[0x1A] |= 0x10;
	x = mode->ehb;
	if(ctlr->flag & Uclk2)
		x /= 2;
	if((x>>3) & 0x40)
		vga->crt[0x1A] |= 0x20;
	if(vga->crt[0x04] & 0x100)
		vga->crt[0x1A] |= 0x40;
	x = mode->ehs;
	if(ctlr->flag & Uclk2)
		x /= 2;
	if((x>>3) & 0x20)
		vga->crt[0x1A] |= 0x80;

	vga->crt[0x1B] = 0x00;
	if(vga->crt[0x06] & 0x400)
		vga->crt[0x1B] |= 0x01;
	if(vga->crt[0x12] & 0x400)
		vga->crt[0x1B] |= 0x04;
	if(vga->crt[0x15] & 0x400)
		vga->crt[0x1B] |= 0x10;
	if(vga->crt[0x10] & 0x400)
		vga->crt[0x1B] |= 0x40;

	vga->attribute[0x11] = Pblack;

	ctlr->flag |= Finit;
}

static void
load(Vga* vga, Ctlr* ctlr)
{
	Tdfx *tdfx;

	vgaxo(Crtx, 0x1A, vga->crt[0x1A]);
	vgaxo(Crtx, 0x1B, vga->crt[0x1B]);

	tdfx = vga->private;
	io32w(tdfx, dacMode, tdfx->r[dacMode]);
	io32w(tdfx, vidScreenSize, tdfx->r[vidScreenSize]);
	io32w(tdfx, vidDesktopOverlayStride, tdfx->r[vidDesktopOverlayStride]);
	io32w(tdfx, vidProcCfg, tdfx->r[vidProcCfg]);
	io32w(tdfx, vgaInit0, tdfx->r[vgaInit0]);

	if((vga->misc & 0x0C) == 0x0C)
		io32w(tdfx, pllCtrl0, tdfx->r[pllCtrl0]);

	ctlr->flag |= Fload;
}

static uint
pllctrl(Tdfx* tdfx, int pll)
{
	uint k, m, n, r;

	r = tdfx->r[pllCtrl0+pll];
	k = r & 0x03;
	m = (r>>2) & 0x3F;
	n = (r>>8) & 0xFF;

	return (RefFreq*(n+2))/((m+2)*(1<<k));
}

static void
dump(Vga* vga, Ctlr* ctlr)
{
	int i;
	Tdfx *tdfx;

	if((tdfx = vga->private) == nil)
		return;

	printitem(ctlr->name, "Crt1A");
	printreg(vga->crt[0x1A]);
	printreg(vga->crt[0x1B]);

	Bprint(&stdout, "\n");
	for(i = 0; i < Nior; i++)
		Bprint(&stdout, "%s %2.2uX\t%.8luX\n",
			ctlr->name, i*4, tdfx->r[i]);

	printitem(ctlr->name, "pllCtrl");
	Bprint(&stdout, "%9ud %8ud\n", pllctrl(tdfx, 0), pllctrl(tdfx, 1));
}

Ctlr tdfx = {
	"3dfx",				/* name */
	snarf,				/* snarf */
	options,			/* options */
	init,				/* init */
	load,				/* load */
	dump,				/* dump */
};

Ctlr tdfxhwgc = {
	"3dfxhwgc",			/* name */
	0,				/* snarf */
	0,				/* options */
	0,				/* init */
	0,				/* load */
	0,				/* dump */
};
