
// Timing and behavior of PPU

// Nes_Emu 0.7.0. http://www.slack.net/~ant/

#include "Nes_Ppu.h"

#include <string.h>
#include "Nes_State.h"
#include "Nes_Mapper.h"
#include "Nes_Core.h"

/* Copyright (C) 2004-2006 Shay Green. This module is free software; you
can redistribute it and/or modify it under the terms of the GNU Lesser
General Public License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. This
module 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 Lesser General Public License for
more details. You should have received a copy of the GNU Lesser General
Public License along with this module; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */

// to do: remove unnecessary run_until() calls

#include "blargg_source.h"

// Timing

ppu_time_t const scanline_len = Nes_Ppu::scanline_len;

// if non-zero, report sprite max at fixed time rather than calculating it
nes_time_t const fixed_sprite_max_time = 0; // 1 * ((21 + 164) * scanline_len + 100) / ppu_overclock;
int const sprite_max_cpu_offset = 2420 + 3;

ppu_time_t const t_to_v_time = 20 * scanline_len + 302;
ppu_time_t const even_odd_time = 20 * scanline_len + 328;

ppu_time_t const first_scanline_time = 21 * scanline_len + 60; // this can be varied
ppu_time_t const first_hblank_time = 21 * scanline_len + 252;

ppu_time_t const earliest_sprite_max = sprite_max_cpu_offset * ppu_overclock;
ppu_time_t const earliest_sprite_hit = 21 * scanline_len + 339; // needs to be 22 * scanline_len when fixed_sprite_max_time is set

nes_time_t const vbl_end_time = 2272;
ppu_time_t const max_frame_length = 262 * scanline_len;
//ppu_time_t const max_frame_length = 320 * scanline_len; // longer frame for testing movie resync
nes_time_t const earliest_vbl_end_time = max_frame_length / ppu_overclock - 10;

// Scanline rendering

void Nes_Ppu::render_bg_until_( nes_time_t cpu_time )
{
	ppu_time_t time = ppu_time( cpu_time );
	ppu_time_t const frame_duration = scanline_len * 261;
	if ( time > frame_duration )
		time = frame_duration;
	
	// one-time events
	if ( frame_phase <= 1 )
	{
		if ( frame_phase < 1 )
		{
			// vtemp->vaddr
			frame_phase = 1;
			if ( w2001 & 0x08 )
				vram_addr = vram_temp;
		}
		
		// variable-length scanline
		if ( time <= even_odd_time )
		{
			next_bg_time = nes_time( even_odd_time );
			return;
		}
		frame_phase = 2;
		if ( !(w2001 & 0x08) || emu.nes.frame_count & 1 )
		{
			if ( --frame_length_extra < 0 )
			{
				frame_length_extra = 2;
				frame_length_++;
			}
			burst_phase--;
		}
		burst_phase = (burst_phase + 2) % 3;
	}
	
	// scanlines
	if ( scanline_time < time )
	{
		int count = (time - scanline_time + scanline_len) / scanline_len;
		
		// hblank before next scanline
		if ( hblank_time < scanline_time )
		{
			hblank_time += scanline_len;
			run_hblank( 1 );
		}
		
		scanline_time += count * scanline_len;
		
		hblank_time += scanline_len * (count - 1);
		int saved_vaddr = vram_addr;
		
		int start = scanline_count;
		scanline_count += count;
		draw_background( start, count );
		
		vram_addr = saved_vaddr; // to do: this is cheap
		run_hblank( count - 1 );
	}
	
	// hblank after current scanline
	ppu_time_t next_ppu_time = hblank_time;
	if ( hblank_time < time )
	{
		hblank_time += scanline_len;
		run_hblank( 1 );
		next_ppu_time = scanline_time; // scanline will run next
	}
	// either hblank or scanline comes next
	next_bg_time = nes_time( next_ppu_time );
}

void Nes_Ppu::render_until_( nes_time_t time )
{
	// render bg scanlines then render sprite scanlines up to wherever bg was rendered to
	
	render_bg_until( time );
	next_sprites_time = nes_time( scanline_time );
	if ( host_pixels )
	{
		int start = next_sprites_scanline;
		int count = scanline_count - start;
		if ( count > 0 )
		{
			next_sprites_scanline += count;
			draw_sprites( start, count );
		}
	}
}

// Frame events

inline void Nes_Ppu::end_vblank()
{
	// clear VBL, sprite hit, and max sprites flags first time after 20 scanlines
	r2002 &= end_vbl_mask;
	end_vbl_mask = ~0;
}

inline void Nes_Ppu::run_end_frame( nes_time_t time )
{
	if ( !frame_ended )
	{
		// update frame_length
		render_bg_until( time );

		// set VBL when end of frame is reached
		nes_time_t len = frame_length();
		if ( time >= len )
		{
			r2002 |= 0x80;
			frame_ended = true;
			if ( w2000 & 0x80 )
				nmi_time_ = len + 2 - (frame_length_extra >> 1);
		}
	}
}

// Sprite max

inline void Nes_Ppu::invalidate_sprite_max_()
{
	next_sprite_max_run = earliest_sprite_max / ppu_overclock;
	sprite_max_set_time = 0;
}

void Nes_Ppu::run_sprite_max_( nes_time_t cpu_time )
{
	end_vblank(); // might get run outside $2002 handler
	
	// 577.0 / 0x10000 ~= 1.0 / 113.581, close enough to accurately calculate which scanline it is
	int start_scanline = next_sprite_max_scanline;
	next_sprite_max_scanline = unsigned ((cpu_time - sprite_max_cpu_offset) * 577) / 0x10000u;
	
	if ( !sprite_max_set_time )
	{
		if ( !(w2001 & 0x18) )
			return;
		
		long t = recalc_sprite_max( start_scanline );
		sprite_max_set_time = indefinite_time;
		if ( t > 0 )
			sprite_max_set_time = t / 3 + sprite_max_cpu_offset;
		next_sprite_max_run = sprite_max_set_time;
		//dprintf( "sprite_max_set_time: %d\n", sprite_max_set_time );
	}
	
	if ( cpu_time > sprite_max_set_time )
	{
		r2002 |= 0x20;
		//dprintf( "Sprite max flag set: %d\n", sprite_max_set_time );
		next_sprite_max_run = indefinite_time;
	}
}

inline void Nes_Ppu::run_sprite_max( nes_time_t t )
{
	if ( !fixed_sprite_max_time && t > next_sprite_max_run )
		run_sprite_max_( t );
}

inline void Nes_Ppu::invalidate_sprite_max( nes_time_t t )
{
	if ( !fixed_sprite_max_time && !(r2002 & 0x20) )
	{
		run_sprite_max( t );
		invalidate_sprite_max_();
	}
}

// Sprite 0 hit

inline int Nes_Ppu_Impl::first_opaque_sprite_line()
{
	// advance earliest time if sprite has blank lines at beginning
	uint8_t const* p = map_chr( sprite_tile_index( spr_ram ) * 16 );
	int twice = w2000 >> 5 & 1; // loop twice if double height is set
	int line = 0;
	do
	{
		for ( int n = 8; n--; p++ )
		{
			if ( p [0] | p [8] )
				return line;
			line++;
		}
		
		p += 8;
	}
	while ( !--twice );
	return line;
}

void Nes_Ppu::update_sprite_hit( nes_time_t cpu_time )
{
	ppu_time_t earliest = earliest_sprite_hit + spr_ram [0] * scanline_len + spr_ram [3];
	//ppu_time_t latest = earliest + sprite_height() * scanline_len;
	
	earliest += first_opaque_sprite_line() * scanline_len;
	
	ppu_time_t time = ppu_time( cpu_time );
	next_sprite_hit_check = indefinite_time;
	
	if ( false )
		if ( earliest < time )
		{
			r2002 |= 0x40;
			return;
		}
	
	if ( time < earliest )
	{
		next_sprite_hit_check = nes_time( earliest );
		return;
	}
	
	// within possible range; render scanline and compare pixels
	int count_needed = 2 + (time - earliest_sprite_hit - spr_ram [3]) / scanline_len;
	if ( count_needed > 240 )
		count_needed = 240;
	while ( scanline_count < count_needed )
		render_bg_until( max( cpu_time, next_bg_time + 1 ) );
	
	if ( sprite_hit_found < 0 )
		return; // sprite won't hit
	
	if ( !sprite_hit_found )
	{
		// check again next scanline
		next_sprite_hit_check = nes_time( earliest_sprite_hit + spr_ram [3] +
				(scanline_count - 1) * scanline_len );
	}
	else
	{
		// hit found
		ppu_time_t hit_time = earliest_sprite_hit + sprite_hit_found - scanline_len;
		
		if ( time < hit_time )
		{
			next_sprite_hit_check = nes_time( hit_time );
			return;
		}
		
		//dprintf( "Sprite hit x: %d, y: %d, scanline_count: %d\n",
		//      sprite_hit_found % 341, sprite_hit_found / 341, scanline_count );
		
		r2002 |= 0x40;
	}
}

// $2002

inline void Nes_Ppu::query_until( nes_time_t time )
{
	end_vblank();
	
	// sprite hit
	if ( time > next_sprite_hit_check )
		update_sprite_hit( time );
	
	// sprite max
	if ( !fixed_sprite_max_time )
		run_sprite_max( time );
	else if ( time >= fixed_sprite_max_time )
		r2002 |= (w2001 << 1 & 0x20) | (w2001 << 2 & 0x20);
}

int Nes_Ppu::read_2002( nes_time_t time )
{
	nes_time_t next = next_status_event;
	next_status_event = vbl_end_time;
	int extra_clock = extra_clocks ? (extra_clocks - 1) >> 2 & 1 : 0;
	if ( time > next && time > vbl_end_time + extra_clock )
	{
		query_until( time );
		
		next_status_event = next_sprite_hit_check;
		nes_time_t const next_max = fixed_sprite_max_time ?
				fixed_sprite_max_time : next_sprite_max_run;
		if ( next_status_event > next_max )
			next_status_event = next_max;

		if ( time > earliest_open_bus_decay() )
		{
			next_status_event = earliest_open_bus_decay();
			update_open_bus( time );
		}
		
		if ( time > earliest_vbl_end_time )
		{
			if ( next_status_event > earliest_vbl_end_time )
				next_status_event = earliest_vbl_end_time;
			run_end_frame( time );
			
			// special vbl behavior when read is just before or at clock when it's set
			if ( extra_clocks != 1 )
			{
				if ( time == frame_length() )
				{
					nmi_time_ = indefinite_time;
					//dprintf( "Suppressed NMI\n" );
				}
			}
			else if ( time == frame_length() - 1 )
			{
				r2002 &= ~0x80;
				frame_ended = true;
				nmi_time_ = indefinite_time;
				//dprintf( "Suppressed NMI\n" );
			}
		}
	}
	emu.set_ppu_2002_time( next_status_event );
	
	int result = r2002;
	second_write = false;
	r2002 = result & ~0x80;
	poke_open_bus( time, result, 0xE0 );
	update_open_bus( time );
	return ( result & 0xE0 ) | ( open_bus & 0x1F );
}

void Nes_Ppu::dma_sprites( nes_time_t time, void const* in )
{
	//dprintf( "%d sprites written\n", time );
	render_until( time );
	
	invalidate_sprite_max( time );
	
	memcpy( spr_ram + w2003, in, 0x100 - w2003 );
	memcpy( spr_ram, (char*) in + 0x100 - w2003, w2003 );
}

// Read

inline int Nes_Ppu_Impl::read_2007( int addr )
{
	int result = r2007;
	if ( addr < 0x2000 )
	{
		r2007 = *map_chr( addr );
	}
	else
	{
		r2007 = get_nametable( addr ) [addr & 0x3ff];
		if ( addr >= 0x3f00 )
		{
			return palette [map_palette( addr )] | ( open_bus & 0xC0 );
		}
	}
	return result;
}

int Nes_Ppu::read( unsigned addr, nes_time_t time )
{
	switch ( addr & 7 )
	{
		// status
		case 2: // handled inline
			return read_2002( time );
		
		// sprite ram
		case 4: {
			int result = spr_ram [w2003];
			if ( (w2003 & 3) == 2 )
				result &= 0xe3;
			poke_open_bus( time, result, ~0 );
			return result;
		}
		
		// video ram
		case 7: {
			render_bg_until( time );
			int addr = vram_addr;
			int new_addr = addr + addr_inc;
			vram_addr = new_addr;
			if ( ~addr & new_addr & vaddr_clock_mask )
			{
				emu.mapper->a12_clocked();
				addr = vram_addr - addr_inc; // avoid having to save across func call
			}
			int result = read_2007( addr & 0x3fff );
			poke_open_bus( time, result, ( ( addr & 0x3fff ) >= 0x3f00 ) ? 0x3F : ~0 );
			return result;
		}
		
		default:
			/* Read from unimplemented PPU register */
			break;
	}

	update_open_bus( time );
	
	return open_bus;
}

// Write

void Nes_Ppu::write( nes_time_t time, unsigned addr, int data )
{
	switch ( addr & 7 )
	{
		case 0:{// control
			int changed = w2000 ^ data;
			
			if ( changed & 0x28 )
				render_until( time ); // obj height or pattern addr changed
			else if ( changed & 0x10 )
				render_bg_until( time ); // bg pattern addr changed
			else if ( ((data << 10) ^ vram_temp) & 0x0C00 )
				render_bg_until( time ); // nametable changed
			
			if ( changed & 0x80 )
			{
				if ( time > vbl_end_time + ((extra_clocks - 1) >> 2 & 1) )
					end_vblank(); // to do: clean this up
				
				if ( data & 0x80 & r2002 )
				{
					nmi_time_ = time + 2;
					emu.event_changed();
				}
				if ( time >= earliest_vbl_end_time )
					run_end_frame( time - 1 + (extra_clocks & 1) );
			}
			
			// nametable select
			vram_temp = (vram_temp & ~0x0C00) | ((data & 3) * 0x400);
			
			if ( changed & 0x20 ) // sprite height changed
				invalidate_sprite_max( time );
			w2000 = data;
			addr_inc = data & 4 ? 32 : 1;

			break;
		}
		
		case 1:{// sprites, bg enable
			int changed = w2001 ^ data;
			
			if ( changed & 0xE1 )
			{
				render_until( time + 1 ); // emphasis/monochrome bits changed
				palette_changed = 0x18;
			}
			
			if ( changed & 0x14 )
				render_until( time + 1 ); // sprite enable/clipping changed
			else if ( changed & 0x0A )
				render_bg_until( time + 1 ); // bg enable/clipping changed
			
			if ( changed & 0x08 ) // bg enabled changed
				emu.mapper->run_until( time );
			
			if ( !(w2001 & 0x18) != !(data & 0x18) )
				invalidate_sprite_max( time ); // all rendering just turned on or off
			
			w2001 = data;
			
			if ( changed & 0x08 )
				emu.irq_changed();

			break;
		}
		
		case 3: // spr addr
			w2003 = data;
			poke_open_bus( time, w2003, ~0 );
			break;
		
		case 4:
			//dprintf( "%d sprites written\n", time );
			if ( time > first_scanline_time / ppu_overclock )
			{
				render_until( time );
				invalidate_sprite_max( time );
			}
			spr_ram [w2003] = data;
			w2003 = (w2003 + 1) & 0xff;
			break;
		
		case 5:
			render_bg_until( time );
			if ( (second_write ^= 1) )
			{
				pixel_x = data & 7;
				vram_temp = (vram_temp & ~0x1f) | (data >> 3);
			}
			else
			{
				vram_temp = (vram_temp & ~0x73e0) |
						(data << 12 & 0x7000) | (data << 2 & 0x03e0);
			}
			break;
		
		case 6:
			render_bg_until( time );
			if ( (second_write ^= 1) )
			{
				vram_temp = (vram_temp & 0xff) | (data << 8 & 0x3f00);
			}
			else
			{
				int changed = ~vram_addr & vram_temp;
				vram_addr = vram_temp = (vram_temp & 0xff00) | data;
				if ( changed & vaddr_clock_mask )
					emu.mapper->a12_clocked();
			}
			break;
		
		default:
			/* Wrote to unimplemented PPU register */
			break;
	}

	poke_open_bus( time, data, ~0 );
}

// Frame begin/end

nes_time_t Nes_Ppu::begin_frame( ppu_time_t timestamp )
{
	// current time
	int cpu_timestamp = timestamp / ppu_overclock;
	extra_clocks = timestamp - cpu_timestamp * ppu_overclock;
	
	// frame end
	ppu_time_t const frame_end = max_frame_length - 1 - extra_clocks;
	frame_length_ = (frame_end + (ppu_overclock - 1)) / ppu_overclock;
	frame_length_extra = frame_length_ * ppu_overclock - frame_end;
	
	// nmi
	nmi_time_ = indefinite_time;
	if ( w2000 & 0x80 & r2002 )
		nmi_time_ = 2 - (extra_clocks >> 1);
	
	// bg rendering
	frame_phase = 0;
	scanline_count = 0;
	hblank_time = first_hblank_time;
	scanline_time = first_scanline_time;
	next_bg_time = nes_time( t_to_v_time );
	
	// sprite rendering
	next_sprites_scanline = 0;
	next_sprites_time = 0;
	
	// status register
	frame_ended = false;
	end_vbl_mask = ~0xE0;
	next_status_event = 0;
	sprite_hit_found = 0;
	next_sprite_hit_check = 0;
	next_sprite_max_scanline = 0;
	invalidate_sprite_max_();

	decay_low += cpu_timestamp;
	decay_high += cpu_timestamp;
	
	base::begin_frame();
	
	//dprintf( "cpu_timestamp: %d\n", cpu_timestamp );
	return cpu_timestamp;
}

ppu_time_t Nes_Ppu::end_frame( nes_time_t end_time )
{
	render_bg_until( end_time );
	render_until( end_time );
	query_until( end_time );
	run_end_frame( end_time );

	update_open_bus( end_time );
	decay_low -= end_time;
	decay_high -= end_time;
	
	// to do: do more PPU RE to get exact behavior
	if ( w2001 & 0x08 )
	{
		unsigned a = vram_addr + 2;
		if ( (vram_addr & 0xff) >= 0xfe )
			a = (vram_addr ^ 0x400) - 0x1e;
		vram_addr = a;
	}
	
	if ( w2001 & 0x10 )
		w2003 = 0;
	
	suspend_rendering();
	
	return (end_time - frame_length_) * ppu_overclock + frame_length_extra;
}

void Nes_Ppu::poke_open_bus( nes_time_t time, int data, int mask )
{
	open_bus = ( open_bus & ~mask ) | ( data & mask );
	if ( mask & 0x1F ) decay_low = time + scanline_len * 100 / ppu_overclock;
	if ( mask & 0xE0 ) decay_high = time + scanline_len * 100 / ppu_overclock;
}

nes_time_t Nes_Ppu::earliest_open_bus_decay()
{
	return ( decay_low < decay_high ) ? decay_low : decay_high;
}
