// VI - video interface (TV stuff).
#include "pch.h"

/* ---------------------------------------------------------------------------

	Timing
	------

	small note for VI frame timing :

	 ---------------------------------------------------
	|       |         PAL         | NTSC, MPAL, EURGB60 |
	| value +---------------------|---------------------|
	|       |   NIN    |   INT    |   NIN    |   INT    |
	|=======+==========+==========+==========+==========|
	|  hz   |   50     |    25    |    60    |    30    |
	|-------+----------+----------+----------+----------|
	| lines |  312.5   |   625    |  262.5   |   525    |
	|-------+----------+----------+----------+----------|
	| active|   262    |   574    |   218    |   480    |
	 -------+----------+----------+----------+----------

	NIN - non-intelaced (double-strike) mode (both odd and even lines in one frame)
	INT - interlaced mode (odd and even lines in alternating frames)
	25/50 hz refer to the frequency of which a full videoframe (==all lines of framebuffer) is displayed
	progressive = double-strike = non-interlaced;
	"field" is the tv-frame as in "odd- and even- field" makes one frame

	Registers
	---------

	VI registers block takes 0x76 bytes (from reversing of VI library) :

	static struct
	{
		uint16_t         regs[59];       // regs are copied to shdwRegs
		uint16_t         shdwRegs[59];   // shdwRegs are copied to hardware registers
		VIHorVer    HorVer;         // used for temporary calculations
	} vi;

	useful VI regs : VI_DISP_CR, VI_TFBL, VI_DISP_POS, VI_INT0.
	dont care about others. Nintendo promised to add HCOUNT (line) interrupt,
	but until it is not used by IPL, we will not use it too.

--------------------------------------------------------------------------- */

using namespace Debug;

// VI state (registers and other data)
VIControl vi;

// ---------------------------------------------------------------------------
// drawing of XFB

// YUV to RGB conversion
#define yuv2rs(y, u, v) ( (uint32_t)bound((76283*(y - 16) + 104595*(v - 128))>>16) )
#define yuv2gs(y, u, v) ( (uint32_t)bound((76283*(y - 16) - 53281 *(v - 128) - 25624*(u - 128))>>16) << 8 )
#define yuv2bs(y, u, v) ( (uint32_t)bound((76283*(y - 16) + 132252*(u - 128))>>16) << 16 )

// clamping routine
static inline int bound(int x)
{
	if (x < 0) x = 0;
	if (x > 255) x = 255;
	return x;
}

// copy XFB to screen
void YUVBlit(uint8_t* yuvbuf, RGB* dib)
{
	uint32_t* rgbbuf = (uint32_t*)dib;
	int count = 320 * 480;

	if (!yuvbuf || !rgbbuf) return;

	// simple blitting, without effects
	while (count--)
	{
		int y1 = *yuvbuf++,
			v = *yuvbuf++,
			y2 = *yuvbuf++,
			u = *yuvbuf++;

		*rgbbuf++ = yuv2bs(y1, u, v) | yuv2gs(y1, u, v) | yuv2rs(y1, u, v);
		*rgbbuf++ = yuv2bs(y2, u, v) | yuv2gs(y2, u, v) | yuv2rs(y2, u, v);
	}

	VideoOutRefresh();
}

// ---------------------------------------------------------------------------
// frame timing

// reset VI timing
static void vi_set_timing()
{
	uint16_t reg = vi.disp_cr;
	vi.inter = (reg & VI_CR_NIN) ? 0 : 1;
	vi.mode = VI_CR_FMT(reg);
	if (vi.mode == 2) vi.mode = VI_NTSC_LIKE; // MPAL same as NTSC
	vi.vtime = Core->GetTicks();

	switch (vi.mode)
	{
		case VI_NTSC_LIKE:
			vi.one_frame = vi.one_second / 30;
			vi.vcount = (vi.inter) ? VI_NTSC_INTER : VI_NTSC_NON_INTER;
			break;
		case VI_PAL_LIKE:
			vi.one_frame = vi.one_second / 25;
			vi.vcount = (vi.inter) ? VI_PAL_INTER : VI_PAL_NON_INTER;
			break;
	}
}

// step line counter(s), update GUI and poll controller
void VIUpdate()
{
	if ((Core->GetTicks() - vi.vtime) >= (vi.one_frame / vi.vcount))
	{
		vi.vtime = Core->GetTicks();

		uint32_t currentBeamPos = VI_POS_VCT(vi.pos);
		uint32_t triggerBeamPos = VI_INT_VCT(vi.int0);

		// generate VIINT ?
		currentBeamPos++;
		if (currentBeamPos == triggerBeamPos)
		{
			vi.int0 |= VI_INT_INT;
			if (vi.int0 & VI_INT_ENB)
			{
				PIAssertInt(PI_INTERRUPT_VI);
			}
		}

		// vertical counter
		if (currentBeamPos >= vi.vcount)
		{
			currentBeamPos = 1;

			// draw XFB
			if (vi.xfb)
			{
				YUVBlit(vi.xfbbuf, vi.gfxbuf);
				vi.frames++;
			}
		}

		vi.pos &= ~0x07ff0000;
		vi.pos |= (currentBeamPos & 0x7ff) << 16;
	}
}

// ---------------------------------------------------------------------------
// accessing VI registers.

static void vi_read8(uint32_t addr, uint32_t* reg)
{
	// TODO
	Report(Channel::VI, "VI READ8\n");
	*reg = 0;
}

static void vi_write8(uint32_t addr, uint32_t data)
{
	Report(Channel::VI, "VI WRITE8\n");
}

static void vi_read16(uint32_t addr, uint32_t* reg)
{
	switch (addr & 0x7f)
	{
		case 0x02:      // display control
			*reg = vi.disp_cr & ~1;
			*reg |= vi.videoEncoderFuse & 1;
			return;
		case 0x1C:      // video buffer hi (TOP)
			*reg = vi.tfbl >> 16;
			return;
		case 0x1E:      // video buffer low (TOP)
			*reg = (uint16_t)vi.tfbl;
			return;
		case 0x24:      // video buffer hi (BOTTOM)
			*reg = vi.bfbl >> 16;
			return;
		case 0x26:      // video buffer low (BOTTOM)
			*reg = (uint16_t)vi.bfbl;
			return;
		case 0x2C:      // beam position hi
			*reg = vi.pos >> 16;
			return;
		case 0x2E:      // beam position low
			*reg = (uint16_t)vi.pos;
			return;
		case 0x30:      // int0 control hi
			*reg = vi.int0 >> 16;
			return;
		case 0x32:      // int0 control low
			*reg = (uint16_t)vi.int0;
			return;
	}
	*reg = 0;
}

static void vi_write16(uint32_t addr, uint32_t data)
{
	switch (addr & 0x7f)
	{
		case 0x02:      // display control
			vi.disp_cr = (uint16_t)data;
			vi_set_timing();
			return;
		case 0x1C:      // video buffer hi (TOP)
			vi.tfbl &= 0x0000ffff;
			vi.tfbl |= data << 16;
			if (vi.log)
			{
				Report(Channel::VI, "TFBL set to %08X (xof=%i)\n", vi.tfbl, (vi.tfbl >> 24) & 0xf);
			}
			vi.tfbl &= 0xffffff;
			vi.xfbbuf = (uint8_t*)MIGetMemoryPointerForVI(vi.tfbl);
			return;
		case 0x1E:      // video buffer low (TOP)
			vi.tfbl &= 0xffff0000;
			vi.tfbl |= (uint16_t)data;
			if (vi.log)
			{
				Report(Channel::VI, "TFBL set to %08X (xof=%i)\n", vi.tfbl, (vi.tfbl >> 24) & 0xf);
			}
			vi.tfbl &= 0xffffff;
			vi.xfbbuf = (uint8_t*)MIGetMemoryPointerForVI(vi.tfbl);
			return;
		case 0x24:      // video buffer hi (BOTTOM)
			vi.bfbl &= 0x0000ffff;
			vi.bfbl |= data << 16;
			vi.bfbl &= 0xffffff;
			if (vi.log)
			{
				Report(Channel::VI, "BFBL set to %08X\n", vi.bfbl);
			}
			//if(vi.bfbl >= RAMSIZE) vi.xfbbuf = NULL;
			//else vi.xfbbuf = &RAM[vi.bfbl];
			return;
		case 0x26:      // video buffer low (BOTTOM)
			vi.bfbl &= 0xffff0000;
			vi.bfbl |= (uint16_t)data;
			vi.bfbl &= 0xffffff;
			if (vi.log)
			{
				Report(Channel::VI, "BFBL set to %08X\n", vi.bfbl);
			}
			//if(vi.bfbl >= RAMSIZE) vi.xfbbuf = NULL;
			//else vi.xfbbuf = &RAM[vi.bfbl];
			return;
		case 0x2C:      // beam position hi
			vi.pos &= 0x0000ffff;
			vi.pos |= data << 16;
			return;
		case 0x2E:      // beam position low
			vi.pos &= 0xffff0000;
			vi.pos |= (uint16_t)data;
			return;
		case 0x30:      // int0 control hi
			vi.int0 &= 0x0000ffff;
			vi.int0 |= data << 16;
			if ((vi.int0 & VI_INT_INT) == 0)
			{
				PIClearInt(PI_INTERRUPT_VI);
			}
			return;
		case 0x32:      // int0 control low
			vi.int0 &= 0xffff0000;
			vi.int0 |= (uint16_t)data;
			return;
	}
}

static void vi_read32(uint32_t addr, uint32_t* reg)
{
	switch (addr & 0x7f)
	{
		case 0x00:      // display control
			*reg = (uint32_t)(vi.disp_cr & ~1);
			*reg |= vi.videoEncoderFuse & 1;
			return;
		case 0x1C:      // video buffer (TOP)
			*reg = vi.tfbl;
			return;
		case 0x24:      // video buffer (BOTTOM)
			*reg = vi.bfbl;
			return;
		case 0x2C:      // beam position
			*reg = vi.pos;
			return;
		case 0x30:      // int0 control
			*reg = vi.int0;
			return;
	}
	*reg = 0;
}

static void vi_write32(uint32_t addr, uint32_t data)
{
	switch (addr & 0x7f)
	{
		case 0x00:      // display control
			vi.disp_cr = (uint16_t)data;
			vi_set_timing();
			return;
		case 0x1C:      // video buffer (TOP)
			vi.tfbl = data & 0xffffff;
			if (vi.log)
			{
				Report(Channel::VI, "TFBL set to %08X (xof=%i)\n", vi.tfbl, (data >> 24) & 0xf);
			}
			vi.xfbbuf = (uint8_t *)MIGetMemoryPointerForVI(vi.tfbl);
			return;
		case 0x24:      // video buffer (BOTTOM)
			vi.bfbl = data & 0xffffff;
			if (vi.log)
			{
				Report(Channel::VI, "BFBL set to %08X\n", vi.bfbl);
			}
			//if(vi.bfbl >= RAMSIZE) vi.xfbbuf = NULL;
			//else vi.xfbbuf = &RAM[vi.bfbl];
			return;
		case 0x2C:      // beam position
			vi.pos = data;
			return;
		case 0x30:      // int0 control
			vi.int0 = data;
			if ((vi.int0 & VI_INT_INT) == 0)
			{
				PIClearInt(PI_INTERRUPT_VI);
			}
			return;
	}
}

// show VI info
void VIStats()
{
	uint32_t currentBeamPos = VI_POS_VCT(vi.pos);
	uint32_t triggerBeamPos = VI_INT_VCT(vi.int0);

	Report(Channel::Norm, "    VI interrupt : [%i x x x]\n", vi.int0 >> 31);
	Report(Channel::Norm, "    VI int mask  : [%i x x x]\n", (vi.int0 >> 28) & 1);
	Report(Channel::Norm, "    VI int pos   : %i == %i, x == x, x == x, x == x (line)\n", currentBeamPos, triggerBeamPos);
	Report(Channel::Norm, "    VI XFB       : T%08X B%08X (phys), enabled: %i\n", vi.tfbl, vi.bfbl, vi.xfb);
}

// ---------------------------------------------------------------------------
// init

void VIOpen(HWConfig* config)
{
	Report(Channel::VI, "Video-out hardware interface\n");

	// clear VI regs
	memset(&vi, 0, sizeof(VIControl));

	vi.one_second = Core->OneSecond();

	// read VI settings
	vi.log = config->vi_log;
	vi.xfb = config->vi_xfb;
	vi.videoEncoderFuse = config->videoEncoderFuse;

	// reset VI timing
	vi_set_timing();

	// XFB is not yet specified
	vi.gfxbuf = NULL;
	vi.xfbbuf = NULL;

	// open GDI (if need)
	if (vi.xfb)
	{
		bool res = VideoOutOpen(config, 640, 480, &vi.gfxbuf);
		if (!res)
		{
			Report(Channel::VI, "VI cant startup VideoOut backend!\n");
			vi.xfb = false;
		}
	}

	// set traps to VI registers
	for (uint32_t ofs = 0; ofs < 0x80; ofs++)
	{
		PISetTrap(8, 0x0C002000 + ofs, vi_read8, vi_write8);
		if ((ofs % 2) == 0) PISetTrap(16, 0x0C002000 + ofs, vi_read16, vi_write16);
		if ((ofs % 4) == 0) PISetTrap(32, 0x0C002000 + ofs, vi_read32, vi_write32);
	}
}

void VIClose()
{
	// XFB can be enabled during emulation,
	// so we must be sure, that GDI is closed
	// even if XFB wasn't enabled, before start
	VideoOutClose();
}

void VISetEncoderFuse(int value)
{
	vi.videoEncoderFuse = value;
}
