/* NVClock 0.8 - Linux overclocker for NVIDIA cards
 *
 * site: http://nvclock.sourceforge.net
 *
 * Copyright(C) 2001-2006 Roderick Colenbrander
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
 */

#include <stdio.h>
#include "nvclock.h"
#include "backend.h"


static float nv30_get_fanspeed()
{
	/* Bit 30-16 of register 0x10f0 are used to store the fanspeed and bit 14-0 contain the pwm divider */
	int pwm_divider = nv_card->PMC[0x10f0/4] & 0x7fff;
	return ((nv_card->PMC[0x10f0/4] >> 16) & 0x7fff) * 100.0/pwm_divider;
}

static void nv30_set_fanspeed(float speed)
{
	int value;
	int pwm_divider = nv_card->PMC[0x10f0/4] & 0x7fff;

	/* Don't set the fan lower than 10% for safety reasons */
	if(speed < 10 || speed > 100)
		return;

	value = 0x80000000 + (((int)(speed * pwm_divider/100) & 0x7fff) << 16) + (nv_card->PMC[0x10f0/4] & 0xffff);
	nv_card->PMC[0x10f0/4] = value;
}

static int CalcSpeed(int base_freq, int m, int n, int p)
{
	return (int)((float)n/(float)m * base_freq) >> p;
}

static int CalcSpeed_nv30(int base_freq, int m1, int m2, int n1, int n2, int p)
{
	return (int)((float)(n1*n2)/(m1*m2) * base_freq) >> p;
}

float GetClock_nv30(int base_freq, unsigned int pll)
{
	/* All GeforceFX cards except the FX5200/FX5600/FX5700 use this algorithm */
	/* If bit 7 is set use the new algorithm */
	if(pll & 0x80)
	{
		int m1, m2, n1, n2, p;

		m1 = NV30_PLL_M1(pll);
		m2 = NV30_PLL_M2(pll);
		n1 = NV30_PLL_N1(pll);
		n2 = NV30_PLL_N2(pll);
		p = NV30_PLL_P(pll);

		/* Perhaps the 0x1f for n2 is wrong .. ? */
	    
		if(nv_card->debug)
			printf("m1=%d m2=%d n1=%d n2=%d p=%d\n", m1, m2, n1, n2, p);
	    
		return (float)CalcSpeed_nv30(base_freq, m1, m2, n1, n2, p)/1000;
	}
	else
	{
		int m, n, p;

		m = NV30_PLL_M1(pll);
		n = NV30_PLL_N1(pll);
		p = NV30_PLL_P(pll);

		if(nv_card->debug)
			printf("m=%d n=%d p=%d\n", m, n, p);

		return (float)CalcSpeed(base_freq, m, n, p)/1000;
	}
}

static void ClockSelect_nv30(int clockIn, int p_current, unsigned int *pllOut)
{
	unsigned diff, diffOld;
	unsigned VClk, Freq;
	unsigned m, m2, n, n2, p = 0;
	int base_freq = 27000;

	diffOld = 0xFFFFFFFF;

	if(clockIn < 125)
		p = 3;
	else if(clockIn < 250)
		p = 2;
	else if(clockIn < 500)
		p = 1;
	else
		p = 0;
	
	VClk = (unsigned)clockIn;

	Freq = VClk;
	if ((Freq >= 75000) && (Freq <= 1100000))
	{
		for(m = 1; m <= 4; m++)
		{
			for (m2 = 1; m2 <= 4; m2++)
			{
				for(n = 1; n <= 31; n++)
				{
					n2 = (int)((float)((VClk << p) * m * m2) / (float)(base_freq * n)+.5);

					if((n2 < 24) && (n >= n2) && (m >= m2))
					{
						Freq = ((base_freq * n * n2) / (m * m2)) >> p;
						if (Freq > VClk)
							diff = Freq - VClk;
						else
							diff = VClk - Freq;

						/* When the difference is 0 or less than .5% accept the speed */
						if( (float)diff/(float)clockIn <= 0.005)
						{
							*pllOut = m + (m2<<4) + (n<<8) + ((n2 & 0x7) << 19) + ((n2 & 0x18)<<21) + (p<<16) + (1<<7);
							return;
						}
						if(diff < diffOld)
						{
							*pllOut = m + (m2<<4) + (n<<8) + ((n2 & 0x7) << 19) + ((n2 & 0x18)<<21) + (p<<16) + (1<<7);
							diffOld  = diff;
						}
					}
				}
			}
		}
	}
}

static float nv30_get_gpu_speed()
{
	int nvpll = nv_card->PRAMDAC[0x500/4];
	if(nv_card->debug == 1)
	{
		printf("NVPLL_COEFF=%08x\n", nvpll);
	}

	return (float)GetClock_nv30(nv_card->base_freq, nvpll);
}

static void nv30_set_gpu_speed(unsigned int nvclk)
{
	unsigned int PLL=0;
	int p;
	nvclk *= 1000;

	p = NV30_PLL_P(nv_card->PRAMDAC[0x500/4]);
	ClockSelect_nv30(nvclk, p, &PLL);

	if(nv_card->debug)
		printf("NVPLL_COEFF: %08x\n", PLL);

	/* Unlock the programmable NVPLL/MPLL */
	nv_card->PRAMDAC[0x50c/4] |= 0x500;
	
	if(PLL)
		nv_card->PRAMDAC[0x500/4] = PLL;
}

static float nv30_get_memory_speed()
{
	int mpll = nv_card->PRAMDAC[0x504/4];
	if(nv_card->debug == 1)
	{
		printf("MPLL_COEFF=%08x\n", mpll);
	}
	return (float)GetClock_nv30(nv_card->base_freq, mpll);
}

static void nv30_set_memory_speed(unsigned int memclk)
{
	unsigned int PLL=0;
	int p;
	memclk *= 1000;

	p = NV30_PLL_P(nv_card->PRAMDAC[0x504/4]);
	ClockSelect_nv30(memclk, p, &PLL);

	if(nv_card->debug)
		printf("MPLL_COEFF: %08x\n", PLL);

	/* Unlock the programmable NVPLL/MPLL */
	nv_card->PRAMDAC[0x50c/4] |= 0x500;
	if(PLL)
		nv_card->PRAMDAC[0x504/4] = PLL;
}

static void nv30_reset_gpu_speed()
{
	/* FIXME: we need to use our bios info */
	/* Unlock the programmable NVPLL/MPLL */
	nv_card->PRAMDAC[0x50c/4] |= 0x500;

	/* Set the gpu speed */    
	nv_card->PRAMDAC[0x500/4] = nv_card->nvpll;
}

static void nv30_reset_memory_speed()
{
	/* FIXME: we need to use our bios info */
	/* Unlock the programmable NVPLL/MPLL */
	nv_card->PRAMDAC[0x50c/4] |= 0x500;

	/* Set the memory speed */    
	nv_card->PRAMDAC[0x504/4] = nv_card->mpll;
}

static void nv30_set_state(int state)
{
	nv_card->state = state;

#ifdef HAVE_NVCONTROL
	if(state & (STATE_2D | STATE_3D))
	{
		nv_card->get_gpu_speed = nvcontrol_get_gpu_speed;
		nv_card->get_memory_speed = nvcontrol_get_memory_speed;
		nv_card->set_gpu_speed = nvcontrol_set_gpu_speed;
		nv_card->set_memory_speed = nvcontrol_set_memory_speed;
		nv_card->reset_gpu_speed = nvcontrol_reset_gpu_speed;
		nv_card->reset_memory_speed = nvcontrol_reset_memory_speed;	
	}
	else
#endif
	{
		nv_card->get_gpu_speed = nv30_get_gpu_speed;
		nv_card->get_memory_speed = nv30_get_memory_speed;
		nv_card->set_memory_speed = nv30_set_memory_speed;
		nv_card->set_gpu_speed = nv30_set_gpu_speed;
		nv_card->reset_gpu_speed = nv30_reset_gpu_speed;
		nv_card->reset_memory_speed = nv30_reset_memory_speed;
	}
}

void nv30_init(void)
{
	nv_card->base_freq = 27000;

	nv_card->set_state = nv30_set_state;
	nv_card->set_state(nv_card->state); /* Set the clock function pointers */

	/* HW monitoring; bit 31 is an indication if fanspeed monitoring is available
	/  Note this bit isn't very reliable as it is set on cards with advanced sensors too.
	/
	/  Only support this on NV30/NV35/NV38 hardware for now as it works differently on other NV3x boards
	*/
	if((nv_card->PMC[0x10f0/4] & 0x80000000) && (nv_card->arch & (NV30 | NV35)) && !(nv_card->caps & I2C_FANSPEED_MONITORING))
	{
		nv_card->caps |= GPU_FANSPEED_MONITORING;
		nv_card->get_fanspeed = nv30_get_fanspeed;
		nv_card->set_fanspeed = nv30_set_fanspeed;
	}
	
	/* Mobile GPU check; we don't want to overclock those unless the user wants it */
	if(nv_card->gpu == MOBILE)
	{
		nv_card->caps = ~(~nv_card->caps | GPU_OVERCLOCKING | MEM_OVERCLOCKING);
	}
	else
		nv_card->caps |= (GPU_OVERCLOCKING | MEM_OVERCLOCKING);
		
	/* Set the speed range */
	if(nv_card->bios)
	{
		/* GeforceFX models have different clocks in 2d and 3d; above hack doesn't work for those */
		nv_card->memclk_min = (short)(nv_card->bios->perf_lst[0].memclk * .75);
		nv_card->memclk_max = (short)(nv_card->bios->perf_lst[2].memclk * 1.25);
		nv_card->nvclk_min = (short)(nv_card->bios->perf_lst[0].nvclk * .75);
		nv_card->nvclk_max = (short)(nv_card->bios->perf_lst[2].nvclk * 1.25);
	}
	else
	{
		float memclk = GetClock_nv30(nv_card->base_freq, nv_card->mpll);
		float nvclk = GetClock_nv30(nv_card->base_freq, nv_card->nvpll);

		/* Not great but better than nothing .. */
		nv_card->memclk_min = (short)(memclk * .75);
		nv_card->memclk_max = (short)(memclk * 1.5);
		nv_card->nvclk_min = (short)(nvclk * .75);
		nv_card->nvclk_max = (short)(nvclk * 1.5);
	}
}


syntax highlighted by Code2HTML, v. 0.9.1