#include <assert.h>
#include <math.h>
#include <algorithm>
#include <vector>
#include "FrameBuffer.h"
#include "DepthBuffer.h"
#include "N64.h"
#include "RSP.h"
#include "RDP.h"
#include "VI.h"
#include "Textures.h"
#include "Combiner.h"
#include "Types.h"
#include "Config.h"
#include "Debugger.h"
#include "DebugDump.h"
#include "PostProcessor.h"
#include "FrameBufferInfo.h"
#include "Log.h"
#include "MemoryStatus.h"

#include "BufferCopy/ColorBufferToRDRAM.h"
#include "BufferCopy/DepthBufferToRDRAM.h"
#include "BufferCopy/RDRAMtoColorBuffer.h"

#include <Graphics/Context.h>
#include <Graphics/Parameters.h>
#include <Graphics/ColorBufferReader.h>
#include "DisplayWindow.h"

using namespace std;
using namespace graphics;

FrameBuffer::FrameBuffer()
	: m_copyFBO(ObjectHandle::defaultFramebuffer)
	, m_ColorBufferFBO(0)
	, m_pColorBufferTexture(nullptr)
{
	m_pTexture = textureCache().addFrameBufferTexture(config.video.multisampling != 0 ?
		textureTarget::TEXTURE_2D_MULTISAMPLE : textureTarget::TEXTURE_2D);
	m_FBO = gfxContext.createFramebuffer();

	if (config.frameBufferEmulation.copyDepthToMainDepthBuffer != 0)
		m_depthFBO = gfxContext.createFramebuffer();
}

FrameBuffer::~FrameBuffer()
{
	gfxContext.deleteFramebuffer(m_FBO);
	gfxContext.deleteFramebuffer(m_depthFBO);
	gfxContext.deleteFramebuffer(m_resolveFBO);
	gfxContext.deleteFramebuffer(m_SubFBO);
	gfxContext.deleteFramebuffer(m_copyFBO);

	textureCache().removeFrameBufferTexture(m_pTexture);
	textureCache().removeFrameBufferTexture(m_pDepthTexture);
	textureCache().removeFrameBufferTexture(m_pResolveTexture);
	textureCache().removeFrameBufferTexture(m_pSubTexture);
	textureCache().removeFrameBufferTexture(m_pFrameBufferCopyTexture);

	_destroyColorFBTexure();
}

static
void _initFrameBufferTexture(u32 _address, u16 _width, u16 _height, f32 _scale, u16 _format, u16 _size, CachedTexture *_pTexture)
{
	const FramebufferTextureFormats & fbTexFormats = gfxContext.getFramebufferTextureFormats();

	_pTexture->width = static_cast<u16>(static_cast<u32>(static_cast<f32>(_width) * _scale));
	_pTexture->height = static_cast<u16>(static_cast<u32>(static_cast<f32>(_height) * _scale));
	_pTexture->format = _format;
	_pTexture->size = _size;
	_pTexture->clampS = 1;
	_pTexture->clampT = 1;
	_pTexture->address = _address;
	_pTexture->clampWidth = _width;
	_pTexture->clampHeight = _height;
	_pTexture->frameBufferTexture = CachedTexture::fbOneSample;
	_pTexture->maskS = 0;
	_pTexture->maskT = 0;
	_pTexture->mirrorS = 0;
	_pTexture->mirrorT = 0;
	_pTexture->textureBytes = _pTexture->width * _pTexture->height;
	_pTexture->hdRatioS = _scale;
	_pTexture->hdRatioT = _scale;
	if (_size > G_IM_SIZ_8b)
		_pTexture->textureBytes *= fbTexFormats.colorFormatBytes;
	else
		_pTexture->textureBytes *= fbTexFormats.monochromeFormatBytes;
}

void FrameBuffer::_initTexture(u16 _width, u16 _height, u16 _format, u16 _size, CachedTexture *_pTexture)
{
	_initFrameBufferTexture(m_startAddress, _width, _height, m_scale, _format, _size, _pTexture);
}

static
void _setAndAttachBufferTexture(ObjectHandle _fbo, CachedTexture *_pTexture, u32 _t, bool _multisampling)
{
	const FramebufferTextureFormats & fbTexFormat = gfxContext.getFramebufferTextureFormats();
	Context::InitTextureParams initParams;
	initParams.handle = _pTexture->name;
	initParams.textureUnitIndex = textureIndices::Tex[_t];
	if (_multisampling)
		initParams.msaaLevel = config.video.multisampling;
	initParams.width = _pTexture->width;
	initParams.height = _pTexture->height;
	if (_pTexture->size > G_IM_SIZ_8b) {
		initParams.internalFormat = fbTexFormat.colorInternalFormat;
		initParams.format = fbTexFormat.colorFormat;
		initParams.dataType = fbTexFormat.colorType;
	} else {
		initParams.internalFormat = fbTexFormat.monochromeInternalFormat;
		initParams.format = fbTexFormat.monochromeFormat;
		initParams.dataType = fbTexFormat.monochromeType;
	}
	gfxContext.init2DTexture(initParams);

	if (!_multisampling) {
		Context::TexParameters texParams;
		texParams.handle = _pTexture->name;
		texParams.target = textureTarget::TEXTURE_2D;
		texParams.textureUnitIndex = textureIndices::Tex[_t];
		texParams.minFilter = textureParameters::FILTER_NEAREST;
		texParams.magFilter = textureParameters::FILTER_NEAREST;
		gfxContext.setTextureParameters(texParams);
	}

	Context::FrameBufferRenderTarget bufTarget;
	bufTarget.bufferHandle = _fbo;
	bufTarget.bufferTarget = bufferTarget::FRAMEBUFFER;
	bufTarget.attachment = bufferAttachment::COLOR_ATTACHMENT0;
	bufTarget.textureTarget = _multisampling ? textureTarget::TEXTURE_2D_MULTISAMPLE : textureTarget::TEXTURE_2D;
	bufTarget.textureHandle = _pTexture->name;
	gfxContext.addFrameBufferRenderTarget(bufTarget);
	assert(!gfxContext.isFramebufferError());
}

void FrameBuffer::_setAndAttachTexture(ObjectHandle _fbo, CachedTexture *_pTexture, u32 _t, bool _multisampling)
{
	_setAndAttachBufferTexture(_fbo, _pTexture, _t, _multisampling);
}

bool FrameBuffer::isAuxiliary() const
{
	return m_width != VI.width || m_size < G_IM_SIZ_16b;
}

void FrameBuffer::init(u32 _address, u16 _format, u16 _size, u16 _width, bool _cfb)
{
	m_startAddress = _address;
	m_width = _width;
	m_height = _cfb ? VI.height : 1;
//	m_height = VI.height;
	m_size = _size;
	updateEndAddress();
	if (isAuxiliary() && config.frameBufferEmulation.copyAuxToRDRAM != 0) {
		m_scale = 1.0f;
	} else if (config.frameBufferEmulation.nativeResFactor != 0 && config.frameBufferEmulation.enable != 0) {
		m_scale = static_cast<float>(config.frameBufferEmulation.nativeResFactor);
	} else {
		m_scale = std::max(dwnd().getScaleX(), 1.0f);
	}
	m_cfb = _cfb;
	m_cleared = false;
	m_fingerprint = false;
	m_swapCount = dwnd().getBuffersSwapCount();

	const u16 maxHeight = VI_GetMaxBufferHeight(_width);
	_initTexture(_width, maxHeight, _format, _size, m_pTexture);

	if (config.video.multisampling != 0) {
		_setAndAttachTexture(m_FBO, m_pTexture, 0, true);
		m_pTexture->frameBufferTexture = CachedTexture::fbMultiSample;

		m_pResolveTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
		_initTexture(_width, maxHeight, _format, _size, m_pResolveTexture);
		m_resolveFBO = gfxContext.createFramebuffer();
		_setAndAttachTexture(m_resolveFBO, m_pResolveTexture, 0, false);
		assert(!gfxContext.isFramebufferError());

		gfxContext.bindFramebuffer(bufferTarget::FRAMEBUFFER, m_FBO);
	} else
		_setAndAttachTexture(m_FBO, m_pTexture, 0, false);

//	gfxContext.clearColorBuffer(0.0f, 0.0f, 0.0f, 0.0f);
}

void FrameBuffer::updateEndAddress()
{
	const u32 height = max(1U, m_height);
	m_endAddress = min(RDRAMSize, m_startAddress + (((m_width * height) << m_size >> 1) - 1));
}

inline
u32 _cutHeight(u32 _address, u32 _height, u32 _stride)
{
	if (_address > RDRAMSize)
		return 0;
	if (_address + _stride * _height > (RDRAMSize + 1))
		return (RDRAMSize + 1 - _address) / _stride;
	return _height;
}

void FrameBuffer::setBufferClearParams(u32 _fillcolor, s32 _ulx, s32 _uly, s32 _lrx, s32 _lry)
{
	m_cleared = true;
	m_clearParams.fillcolor = _fillcolor;
	m_clearParams.ulx = _ulx;
	m_clearParams.lrx = _lrx;
	m_clearParams.uly = _uly;
	m_clearParams.lry = _lry;
}

void FrameBuffer::copyRdram()
{
	const u32 stride = m_width << m_size >> 1;
	const u32 height = _cutHeight(m_startAddress, m_height, stride);
	if (height == 0)
		return;

	m_cleared = false;

	const u32 dataSize = stride * height;

	// Auxiliary frame buffer
	if (isAuxiliary() && config.frameBufferEmulation.copyAuxToRDRAM == 0) {
		// Write small amount of data to the start of the buffer.
		// This is necessary for auxilary buffers: game can restore content of RDRAM when buffer is not needed anymore
		// Thus content of RDRAM on moment of buffer creation will be the same as when buffer becomes obsolete.
		// Validity check will see that the RDRAM is the same and thus the buffer is valid, which is false.
		const u32 twoPercent = max(4U, dataSize / 200);
		u32 start = m_startAddress >> 2;
		u32 * pData = reinterpret_cast<u32*>(RDRAM);
		for (u32 i = 0; i < twoPercent; ++i) {
			if (i < 4)
				pData[start++] = fingerprint[i];
			else
				pData[start++] = 0;
		}
		m_fingerprint = true;
		return;
	}
	m_RdramCopy.resize(dataSize);
	memcpy(m_RdramCopy.data(), RDRAM + m_startAddress, dataSize);
}

void FrameBuffer::setDirty()
{
	m_cleared = false;
	m_RdramCopy.clear();
}

bool FrameBuffer::isValid(bool _forceCheck) const
{
	if (!_forceCheck) {
		if (m_validityChecked == dwnd().getBuffersSwapCount())
			return true; // Already checked
		m_validityChecked = dwnd().getBuffersSwapCount();
	}

	const u32 * const pData = reinterpret_cast<const u32*>(RDRAM);

	if (m_cleared) {
		const u32 testColor = m_clearParams.fillcolor & 0xFFFEFFFE;
		const u32 stride = m_width << m_size >> 1;
		const s32 lry = static_cast<s32>(_cutHeight(m_startAddress, static_cast<u32>(m_clearParams.lry), stride));
		if (lry == 0)
			return false;

		const u32 ci_width_in_dwords = m_width >> (3 - m_size);
		const u32 start = (m_startAddress >> 2) + static_cast<u32>(m_clearParams.uly) * ci_width_in_dwords;
		const u32 * dst = pData + start;
		u32 wrongPixels = 0;
		for (s32 y = m_clearParams.uly; y < lry; ++y) {
			for (s32 x = m_clearParams.ulx; x < m_clearParams.lrx; ++x) {
				if ((dst[x] & 0xFFFEFFFE) != testColor)
					++wrongPixels;
			}
			dst += ci_width_in_dwords;
		}
		return wrongPixels < (m_endAddress - m_startAddress) / 400; // threshold level 1% of dwords
	} else if (m_fingerprint) {
			//check if our fingerprint is still there
			u32 start = m_startAddress >> 2;
			for (u32 i = 0; i < 4; ++i)
				if ((pData[start++] & 0xFFFEFFFE) != (fingerprint[i] & 0xFFFEFFFE))
					return false;
			return true;
	} else if (!m_RdramCopy.empty()) {
		const u32 * const pCopy = reinterpret_cast<const u32* >(m_RdramCopy.data());
		const u32 size = static_cast<u32>(m_RdramCopy.size());
		const u32 size_dwords = size >> 2;
		u32 start = m_startAddress >> 2;
		u32 wrongPixels = 0;
		for (u32 i = 0; i < size_dwords; ++i) {
			if ((pData[start++] & 0xFFFEFFFE) != (pCopy[i] & 0xFFFEFFFE))
				++wrongPixels;
		}
		return wrongPixels < size / 400; // threshold level 1% of dwords
	}
	return true; // No data to decide
}

void FrameBuffer::resolveMultisampledTexture(bool _bForce)
{
	if (!Context::Multisampling)
		return;

	if (m_resolved && !_bForce)
		return;

	if (!m_pResolveTexture)
		return;

	Context::BlitFramebuffersParams blitParams;
	blitParams.readBuffer = m_FBO;
	blitParams.drawBuffer = m_resolveFBO;
	blitParams.srcX0 = 0;
	blitParams.srcY0 = 0;
	blitParams.srcX1 = m_pTexture->width;
	blitParams.srcY1 = m_pTexture->height;
	blitParams.dstX0 = 0;
	blitParams.dstY0 = 0;
	blitParams.dstX1 = m_pResolveTexture->width;
	blitParams.dstY1 = m_pResolveTexture->height;
	blitParams.mask = blitMask::COLOR_BUFFER;
	blitParams.filter = textureParameters::FILTER_NEAREST;

	gfxContext.blitFramebuffers(blitParams);

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);

	frameBufferList().setCurrentDrawBuffer();
	m_resolved = true;
}

void FrameBuffer::copyDepthTexture()
{
	if (config.frameBufferEmulation.copyDepthToMainDepthBuffer != 0)
		DepthBuffer::copyDepthBufferTexture(this, m_pDepthTexture, m_depthFBO);
}

bool FrameBuffer::_initSubTexture(u32 _t)
{
	if (!m_SubFBO.isNotNull())
		m_SubFBO = gfxContext.createFramebuffer();

	gDPTile * pTile = gSP.textureTile[_t];
	if (pTile->lrs < pTile->uls || pTile->lrt < pTile->ult)
		return false;
	const u32 width = pTile->lrs - pTile->uls + 1;
	const u32 height = pTile->lrt - pTile->ult + 1;

	if (m_pSubTexture != nullptr) {
		if (m_pSubTexture->size == m_pTexture->size &&
			m_pSubTexture->clampWidth == width &&
			m_pSubTexture->clampHeight == height)
			return true;
		textureCache().removeFrameBufferTexture(m_pSubTexture);
	}

	m_pSubTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
	_initTexture(static_cast<u16>(width), static_cast<u16>(height), m_pTexture->format, m_pTexture->size, m_pSubTexture);

	m_pSubTexture->clampS = pTile->clamps;
	m_pSubTexture->clampT = pTile->clampt;
	m_pSubTexture->offsetS = 0.0f;
	m_pSubTexture->offsetT = 0.0f;
	m_pSubTexture->hdRatioS = m_pTexture->hdRatioS;
	m_pSubTexture->hdRatioT = m_pTexture->hdRatioT;


	_setAndAttachTexture(m_SubFBO, m_pSubTexture, _t, false);

	return true;
}

CachedTexture * FrameBuffer::_getSubTexture(u32 _t)
{
	if (!Context::BlitFramebuffer)
		return m_pTexture;

	if (!_initSubTexture(_t))
		return m_pTexture;

	s32 x0 = static_cast<s32>(m_pTexture->offsetS * m_scale);
	s32 y0 = static_cast<s32>(m_pTexture->offsetT * m_scale);
	s32 copyWidth = m_pSubTexture->width;
	if (x0 + copyWidth > m_pTexture->width)
		copyWidth = m_pTexture->width - x0;
	s32 copyHeight = m_pSubTexture->height;
	if (y0 + copyHeight > m_pTexture->height)
		copyHeight = m_pTexture->height - y0;

	ObjectHandle readFBO = m_FBO;
	if (Context::WeakBlitFramebuffer &&
			m_pTexture->frameBufferTexture == CachedTexture::fbMultiSample) {
		resolveMultisampledTexture(true);
		readFBO = m_resolveFBO;
	}

	Context::BlitFramebuffersParams blitParams;
	blitParams.readBuffer = readFBO;
	blitParams.drawBuffer = m_SubFBO;
	blitParams.srcX0 = x0;
	blitParams.srcY0 = y0;
	blitParams.srcX1 = x0 + copyWidth;
	blitParams.srcY1 = y0 + copyHeight;
	blitParams.dstX0 = 0;
	blitParams.dstY0 = 0;
	blitParams.dstX1 = copyWidth;
	blitParams.dstY1 = copyHeight;
	blitParams.mask = blitMask::COLOR_BUFFER;
	blitParams.filter = textureParameters::FILTER_NEAREST;

	gfxContext.blitFramebuffers(blitParams);

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);

	frameBufferList().setCurrentDrawBuffer();

	return m_pSubTexture;
}

void FrameBuffer::_initCopyTexture()
{
	m_copyFBO = gfxContext.createFramebuffer();
	m_pFrameBufferCopyTexture = textureCache().addFrameBufferTexture(config.video.multisampling != 0 ?
		textureTarget::TEXTURE_2D_MULTISAMPLE : textureTarget::TEXTURE_2D);
	_initTexture(static_cast<u16>(m_width), VI_GetMaxBufferHeight(static_cast<u16>(m_width)),
				 m_pTexture->format, m_pTexture->size, m_pFrameBufferCopyTexture);
	_setAndAttachTexture(m_copyFBO, m_pFrameBufferCopyTexture, 0, config.video.multisampling != 0);
	if (config.video.multisampling != 0)
		m_pFrameBufferCopyTexture->frameBufferTexture = CachedTexture::fbMultiSample;
}

CachedTexture * FrameBuffer::_copyFrameBufferTexture()
{
	if (m_copied)
		return m_pFrameBufferCopyTexture;

	if (m_pFrameBufferCopyTexture == nullptr)
		_initCopyTexture();

	Context::BlitFramebuffersParams blitParams;
	blitParams.readBuffer = m_FBO;
	blitParams.drawBuffer = m_copyFBO;
	blitParams.srcX0 = 0;
	blitParams.srcY0 = 0;
	blitParams.srcX1 = m_pTexture->width;
	blitParams.srcY1 = m_pTexture->height;
	blitParams.dstX0 = 0;
	blitParams.dstY0 = 0;
	blitParams.dstX1 = m_pTexture->width;
	blitParams.dstY1 = m_pTexture->height;
	blitParams.mask = blitMask::COLOR_BUFFER;
	blitParams.filter = textureParameters::FILTER_NEAREST;

	gfxContext.blitFramebuffers(blitParams);

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
	frameBufferList().setCurrentDrawBuffer();

	m_copied = true;
	return m_pFrameBufferCopyTexture;
}

CachedTexture * FrameBuffer::getTexture(u32 _t)
{
	const bool getDepthTexture = m_isDepthBuffer &&
								 gDP.colorImage.address == gDP.depthImageAddress &&
								 m_pDepthBuffer != nullptr &&
								 (config.generalEmulation.hacks & hack_ZeldaMM) == 0;
	CachedTexture *pTexture = getDepthTexture ? m_pDepthBuffer->m_pDepthBufferTexture : m_pTexture;

	if (this == frameBufferList().getCurrent()) {
		if (Context::TextureBarrier)
			gfxContext.textureBarrier();
		else if (Context::BlitFramebuffer)
			pTexture = getDepthTexture ? m_pDepthBuffer->copyDepthBufferTexture(this) : _copyFrameBufferTexture();
	}

	const u32 shift = (gSP.textureTile[_t]->imageAddress - m_startAddress) >> (m_size - 1);
	const u32 factor = m_width;
	if (m_loadType == LOADTYPE_TILE) {
		pTexture->offsetS = static_cast<f32>(m_loadTileOrigin.uls + (shift % factor));
		pTexture->offsetT = static_cast<f32>(m_loadTileOrigin.ult + shift / factor);
	} else {
		pTexture->offsetS = static_cast<f32>(shift % factor);
		pTexture->offsetT = static_cast<f32>(shift / factor);
	}
	pTexture->hdRatioS = m_pTexture->hdRatioS;
	pTexture->hdRatioT = m_pTexture->hdRatioT;

	if (!getDepthTexture && (gSP.textureTile[_t]->clamps == 0 || gSP.textureTile[_t]->clampt == 0))
		pTexture = _getSubTexture(_t);

	pTexture->scaleS = m_scale / static_cast<f32>(pTexture->width);
	pTexture->scaleT = m_scale / static_cast<f32>(pTexture->height);

	pTexture->shiftScaleS = calcShiftScaleS(*gSP.textureTile[_t]);
	pTexture->shiftScaleT = calcShiftScaleT(*gSP.textureTile[_t]);

	return pTexture;
}

CachedTexture * FrameBuffer::getTextureBG()
{
	CachedTexture *pTexture = m_pTexture;

	if (this == frameBufferList().getCurrent()) {
		if (Context::TextureBarrier)
			gfxContext.textureBarrier();
		else if (Context::BlitFramebuffer)
			pTexture = _copyFrameBufferTexture();
	}

	pTexture->scaleS = m_scale / static_cast<f32>(pTexture->width);
	pTexture->scaleT = m_scale / static_cast<f32>(pTexture->height);

	pTexture->shiftScaleS = 1.0f;
	pTexture->shiftScaleT = 1.0f;

	pTexture->offsetS = gSP.bgImage.imageX;
	pTexture->offsetT = gSP.bgImage.imageY;
	return pTexture;
}

CachedTexture * FrameBuffer::getColorFbTexture()
{
	if(m_pColorBufferTexture == nullptr ||
			m_pColorBufferTexture->width != m_width ||
			m_pColorBufferTexture->height != VI_GetMaxBufferHeight(m_width))
	{
		_destroyColorFBTexure();
		_initColorFBTexture(m_width);
	}

	return m_pColorBufferTexture;
}

graphics::ObjectHandle FrameBuffer::getColorFbFbo()
{
	return m_ColorBufferFBO;
}

const u8 * FrameBuffer::readPixels(s32 _x0, s32 _y0, u32 _width, u32 _height, u32 _size, bool _sync)
{
	return m_bufferReader->readPixels(_x0, _y0, _width, _height, _size, _sync);
}

void FrameBuffer::cleanUp()
{
	m_bufferReader->cleanUp();
}

void FrameBuffer::_initColorFBTexture(int _width)
{
	m_ColorBufferFBO = gfxContext.createFramebuffer();

	const FramebufferTextureFormats & fbTexFormat = gfxContext.getFramebufferTextureFormats();

	m_pColorBufferTexture = textureCache().addFrameBufferTexture(Context::EglImage ? textureTarget::TEXTURE_EXTERNAL : textureTarget::TEXTURE_2D);
	m_pColorBufferTexture->format = G_IM_FMT_RGBA;
	m_pColorBufferTexture->size = 2;
	m_pColorBufferTexture->clampS = 1;
	m_pColorBufferTexture->clampT = 1;
	m_pColorBufferTexture->frameBufferTexture = CachedTexture::fbOneSample;
	m_pColorBufferTexture->maskS = 0;
	m_pColorBufferTexture->maskT = 0;
	m_pColorBufferTexture->mirrorS = 0;
	m_pColorBufferTexture->mirrorT = 0;
	m_pColorBufferTexture->width = _width;
	m_pColorBufferTexture->height = VI_GetMaxBufferHeight(_width);
	m_pColorBufferTexture->textureBytes = m_pColorBufferTexture->width * m_pColorBufferTexture->height * fbTexFormat.colorFormatBytes;

	m_bufferReader.reset(gfxContext.createColorBufferReader(m_pColorBufferTexture));

	// Skip this since texture is initialized in the EGL color buffer reader
	if (!Context::EglImage)
	{
		Context::InitTextureParams params;
		params.handle = m_pColorBufferTexture->name;
		params.target = textureTarget::TEXTURE_2D;
		params.width = m_pColorBufferTexture->width;
		params.height = m_pColorBufferTexture->height;
		params.internalFormat = fbTexFormat.colorInternalFormat;
		params.format = fbTexFormat.colorFormat;
		params.dataType = fbTexFormat.colorType;
		gfxContext.init2DTexture(params);
	}

	{
		Context::TexParameters params;
		params.handle = m_pColorBufferTexture->name;
		params.target = Context::EglImage ? textureTarget::TEXTURE_EXTERNAL : textureTarget::TEXTURE_2D;
		params.textureUnitIndex = textureIndices::Tex[0];
		params.minFilter = textureParameters::FILTER_LINEAR;
		params.magFilter = textureParameters::FILTER_LINEAR;
		gfxContext.setTextureParameters(params);
	}
	{
		Context::FrameBufferRenderTarget bufTarget;
		bufTarget.bufferHandle = ObjectHandle(m_ColorBufferFBO);
		bufTarget.bufferTarget = bufferTarget::DRAW_FRAMEBUFFER;
		bufTarget.attachment = bufferAttachment::COLOR_ATTACHMENT0;
		bufTarget.textureTarget = Context::EglImageFramebuffer ? textureTarget::TEXTURE_EXTERNAL : textureTarget::TEXTURE_2D;
		bufTarget.textureHandle = m_pColorBufferTexture->name;
		gfxContext.addFrameBufferRenderTarget(bufTarget);
	}

	// check if everything is OK
	assert(!gfxContext.isFramebufferError());

	gfxContext.bindFramebuffer(graphics::bufferTarget::DRAW_FRAMEBUFFER, graphics::ObjectHandle::defaultFramebuffer);
}

void FrameBuffer::_destroyColorFBTexure()
{
	m_bufferReader.reset();

	if (m_pColorBufferTexture != nullptr) {
		textureCache().removeFrameBufferTexture(m_pColorBufferTexture);
		m_pColorBufferTexture = nullptr;
	}

	if (m_ColorBufferFBO.isNotNull()) {
		gfxContext.deleteFramebuffer(m_ColorBufferFBO);
		m_ColorBufferFBO.reset();
	}
}

FrameBufferList & FrameBufferList::get()
{
	static FrameBufferList frameBufferList;
	return frameBufferList;
}

void FrameBufferList::init()
{
	 m_pCurrent = nullptr;
	 m_pCopy = nullptr;
	 gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
	 m_prevColorImageHeight = 0;
	 m_overscan.init();
	 m_rdpUpdate.init();
}

void FrameBufferList::destroy() {
	gfxContext.bindFramebuffer(bufferTarget::FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
	m_list.clear();
	m_pCurrent = nullptr;
	m_pCopy = nullptr;
	m_overscan.destroy();
}

void FrameBufferList::setBufferChanged(f32 _maxY)
{
	gDP.colorImage.changed = TRUE;
	gDP.colorImage.height = max(gDP.colorImage.height, static_cast<u32>(_maxY));
	gDP.colorImage.height = min(gDP.colorImage.height, static_cast<u32>(gDP.scissor.lry));
	if (m_pCurrent != nullptr) {
		if (m_pCurrent->m_isMainBuffer)
			m_pCurrent->m_height = max(m_pCurrent->m_height, min(gDP.colorImage.height, VI.height));
		else
			m_pCurrent->m_height = max(m_pCurrent->m_height, gDP.colorImage.height);
		m_pCurrent->m_cfb = false;
		m_pCurrent->m_changed = true;
		m_pCurrent->m_copiedToRdram = false;
	}
}

void FrameBufferList::clearBuffersChanged()
{
	gDP.colorImage.changed = FALSE;
	FrameBuffer * pBuffer = frameBufferList().findBuffer(*REG.VI_ORIGIN & 0xffffff);
	if (pBuffer != nullptr)
		pBuffer->m_changed = false;
}

void FrameBufferList::setCurrentDrawBuffer() const
{
	if (m_pCurrent != nullptr)
		gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, m_pCurrent->m_FBO);
	else if (!m_list.empty())
		gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, m_list.back().m_FBO);
}

FrameBuffer * FrameBufferList::findBuffer(u32 _startAddress)
{
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter) {
		if (iter->m_startAddress <= _startAddress && iter->m_endAddress >= _startAddress) // [  {  ]
			return &(*iter);
	}
	return nullptr;
}

FrameBuffer * FrameBufferList::getBuffer(u32 _startAddress)
{
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter) {
		if (iter->m_startAddress == _startAddress)
			return &(*iter);
	}
	return nullptr;
}

inline
bool isOverlapping(const FrameBuffer * _buf1, const FrameBuffer * _buf2)
{
	if (_buf1->m_endAddress < _buf2->m_endAddress && _buf1->m_width == _buf2->m_width && _buf1->m_size == _buf2->m_size) {
		const u32 diff = _buf1->m_endAddress - _buf2->m_startAddress + 1;
		const u32 stride = _buf1->m_width << _buf1->m_size >> 1;
		if ((diff % stride == 0) && (diff / stride < 5))
			return true;
		else
			return false;
	}
	return false;
}

void FrameBufferList::removeIntersections()
{
	assert(!m_list.empty());

	FrameBuffers::iterator iter = m_list.end();
	do {
		--iter;
		if (&(*iter) == m_pCurrent)
			continue;
		if (iter->m_startAddress <= m_pCurrent->m_startAddress && iter->m_endAddress >= m_pCurrent->m_startAddress) { // [  {  ]
			if (isOverlapping(&(*iter), m_pCurrent)) {
				iter->m_endAddress = m_pCurrent->m_startAddress - 1;
				continue;
			}
			iter = m_list.erase(iter);
		} else if (m_pCurrent->m_startAddress <= iter->m_startAddress && m_pCurrent->m_endAddress >= iter->m_startAddress) { // {  [  }
			if (isOverlapping(m_pCurrent, &(*iter))) {
				m_pCurrent->m_endAddress = iter->m_startAddress - 1;
				continue;
			}
			iter = m_list.erase(iter);
		}
	} while (iter != m_list.begin());
}

FrameBuffer * FrameBufferList::findTmpBuffer(u32 _address)
{
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter)
		if (iter->m_startAddress > _address || iter->m_endAddress < _address)
				return &(*iter);
	return nullptr;
}

void FrameBufferList::updateCurrentBufferEndAddress()
{
	if (m_pCurrent == nullptr)
		return;
	m_pCurrent->updateEndAddress();
	removeIntersections();
}

void FrameBufferList::_createScreenSizeBuffer()
{
	if (VI.height == 0)
		return;
	m_list.emplace_front();
	FrameBuffer & buffer = m_list.front();
	buffer.init(VI.width * 2, G_IM_FMT_RGBA, G_IM_SIZ_16b, static_cast<u16>(VI.width), false);
}

void FrameBufferList::saveBuffer(u32 _address, u16 _format, u16 _size, u16 _width, bool _cfb)
{
	if (_width > 640)
		return;

	if (_width == 512 && (config.generalEmulation.hacks & hack_RE2) != 0)
		_width = static_cast<u16>(*REG.VI_WIDTH);

	if (config.frameBufferEmulation.enable == 0) {
		if (m_list.empty())
			_createScreenSizeBuffer();
		return;
	}

	if (m_pCurrent != nullptr &&
		config.frameBufferEmulation.copyAuxToRDRAM != 0 &&
		(config.generalEmulation.hacks & hack_Snap) == 0) {
		if (m_pCurrent->isAuxiliary()) {
			FrameBuffer_CopyToRDRAM(m_pCurrent->m_startAddress, true);
			removeBuffer(m_pCurrent->m_startAddress);
		}
	}

	DisplayWindow & wnd = dwnd();
	bool bPrevIsDepth = false;

	if (m_pCurrent != nullptr) {
		bPrevIsDepth = m_pCurrent->m_isDepthBuffer;
		m_pCurrent->m_readable = true;
		m_pCurrent->updateEndAddress();

		if (!m_pCurrent->m_isDepthBuffer &&
			!m_pCurrent->m_copiedToRdram &&
			!m_pCurrent->m_cfb &&
			!m_pCurrent->m_cleared &&
			m_pCurrent->m_RdramCopy.empty() &&
			m_pCurrent->m_height > 1) {
			m_pCurrent->copyRdram();
		}

		removeIntersections();
	}

	const float scaleX = config.frameBufferEmulation.nativeResFactor == 0 ?
		wnd.getScaleX() :
		static_cast<float>(config.frameBufferEmulation.nativeResFactor);

	if (m_pCurrent == nullptr || m_pCurrent->m_startAddress != _address || m_pCurrent->m_width != _width)
		m_pCurrent = findBuffer(_address);

	auto isSubBuffer = [_address, _width, _size, &wnd](const FrameBuffer * _pBuffer) -> bool
	{
		if (_pBuffer->m_swapCount == wnd.getBuffersSwapCount() &&
			!_pBuffer->m_cfb &&
			_pBuffer->m_width == _width &&
			_pBuffer->m_size == _size)
		{
			const u32 stride = _width << _size >> 1;
			const u32 diffFromStart = _address - _pBuffer->m_startAddress;
			if (diffFromStart % stride != 0)
				return true;
			const u32 diffFromEnd = _pBuffer->m_endAddress - _address + 1;
			if ((diffFromEnd / stride > 5))
				return true;
		}
		return false;
	};

	auto isOverlappingBuffer = [_address, _width, _size](const FrameBuffer * _pBuffer) -> bool
	{
		if (_pBuffer->m_width == _width && _pBuffer->m_size == _size) {
			const u32 stride = _width << _size >> 1;
			const u32 diffEnd = _pBuffer->m_endAddress - _address + 1;
			if ((diffEnd / stride < 5))
				return true;
		}
		return false;
	};

	if (m_pCurrent != nullptr) {
		m_pCurrent->m_originX = m_pCurrent->m_originY = 0;
		if ((m_pCurrent->m_startAddress != _address)) {
			if (isSubBuffer(m_pCurrent)) {
				const u32 stride = _width << _size >> 1;
				const u32 addrOffset = _address - m_pCurrent->m_startAddress;
				m_pCurrent->m_originX = (addrOffset % stride) >> (_size - 1);
				m_pCurrent->m_originY = addrOffset / stride;
				gSP.changed |= CHANGED_VIEWPORT;
				gDP.changed |= CHANGED_SCISSOR;
				return;
			} else if (isOverlappingBuffer(m_pCurrent)) {
				m_pCurrent->m_endAddress = _address - 1;
				m_pCurrent = nullptr;
			} else {
				removeBuffer(m_pCurrent->m_startAddress);
				m_pCurrent = nullptr;
			}
		} else if ((m_pCurrent->m_width != _width) ||
					(m_pCurrent->m_size < _size) ||
					(m_pCurrent->m_scale != scaleX)) {
			removeBuffer(m_pCurrent->m_startAddress);
			m_pCurrent = nullptr;
		} else {
			m_pCurrent->m_resolved = false;
			gfxContext.bindFramebuffer(bufferTarget::FRAMEBUFFER, m_pCurrent->m_FBO);
			if (m_pCurrent->m_size != _size) {
				f32 fillColor[4];
				gDPGetFillColor(fillColor);
				wnd.getDrawer().clearColorBuffer(fillColor);
				m_pCurrent->m_size = _size;
				m_pCurrent->m_pTexture->format = _format;
				m_pCurrent->m_pTexture->size = _size;
				if (m_pCurrent->m_pResolveTexture != nullptr) {
					m_pCurrent->m_pResolveTexture->format = _format;
					m_pCurrent->m_pResolveTexture->size = _size;
				}
				if (m_pCurrent->m_copiedToRdram)
					m_pCurrent->copyRdram();
			}
		}
	}
	const bool bNew = m_pCurrent == nullptr;
	if  (bNew) {
		// Wasn't found or removed, create a new one
		m_list.emplace_front();
		FrameBuffer & buffer = m_list.front();
		buffer.init(_address, _format, _size, _width, _cfb);
		m_pCurrent = &buffer;
		RDRAMtoColorBuffer::get().copyFromRDRAM(m_pCurrent);
		if (_cfb)
			m_pCurrent->copyRdram();
	}

	if (_address == gDP.depthImageAddress)
		depthBufferList().saveBuffer(_address);
	else
		attachDepthBuffer();

	DebugMsg( DEBUG_NORMAL, "FrameBuffer_SaveBuffer( 0x%08X )\n", _address);

	if (m_pCurrent->isAuxiliary() &&
		m_pCurrent->m_pDepthBuffer != nullptr &&
		bPrevIsDepth &&
		(config.generalEmulation.hacks&hack_LoadDepthTextures) == 0) {
		// N64 games may use partial depth buffer clear for aux buffers
		// It will not work for GL, so we have to force clear depth buffer for aux buffer
		wnd.getDrawer().clearDepthBuffer();
	}

	if ((config.generalEmulation.hacks & hack_subscreen) != 0u &&
		_format == G_IM_FMT_I && _size == G_IM_SIZ_8b)
		gDP.m_subscreen = gDP.otherMode._u64 == 0x00000cf00f0a0004;

	m_pCurrent->m_isDepthBuffer = _address == gDP.depthImageAddress;
	m_pCurrent->m_isPauseScreen = m_pCurrent->m_isOBScreen = false;
	m_pCurrent->m_copied = false;
	m_pCurrent->m_swapCount = wnd.getBuffersSwapCount();
}

void FrameBufferList::copyAux()
{
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter) {
		if (iter->isAuxiliary())
			FrameBuffer_CopyToRDRAM(iter->m_startAddress, true);
	}
}

void FrameBufferList::removeAux()
{
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter) {
		while (iter->isAuxiliary()) {
			if (&(*iter) == m_pCurrent) {
				m_pCurrent = nullptr;
				gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
			}
			iter = m_list.erase(iter);
			if (iter == m_list.end())
				return;
		}
	}
}

void FrameBufferList::removeBuffer(u32 _address )
{
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter)
		if (iter->m_startAddress == _address) {
			if (&(*iter) == m_pCurrent) {
				m_pCurrent = nullptr;
				gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
			}
			m_list.erase(iter);
			return;
		}
}

void FrameBufferList::removeBuffers(u32 _width)
{
	m_pCurrent = nullptr;
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter) {
		while (iter->m_width == _width) {
			if (&(*iter) == m_pCurrent) {
				m_pCurrent = nullptr;
				gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
			}
			iter = m_list.erase(iter);
			if (iter == m_list.end())
				return;
		}
	}
}

void FrameBufferList::depthBufferCopyRdram()
{
	FrameBuffer * pCurrentDepthBuffer = findBuffer(gDP.depthImageAddress);
	if (pCurrentDepthBuffer != nullptr)
		pCurrentDepthBuffer->copyRdram();
}

void FrameBufferList::fillBufferInfo(void * _pinfo, u32 _size)
{
	FBInfo::FrameBufferInfo* pInfo = reinterpret_cast<FBInfo::FrameBufferInfo*>(_pinfo);

	u32 idx = 0;
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter) {
		if (iter->m_width == VI.width && !iter->m_cfb && !iter->m_isDepthBuffer) {
			pInfo[idx].addr = iter->m_startAddress;
			pInfo[idx].width = iter->m_width;
			pInfo[idx].height = iter->m_height;
			pInfo[idx++].size = iter->m_size;
			if (idx >= _size)
				return;
		}
	}
}

void FrameBufferList::attachDepthBuffer()
{
	FrameBuffer * pCurrent = config.frameBufferEmulation.enable == 0 ? &m_list.back() : m_pCurrent;
	if (pCurrent == nullptr)
		return;

	DepthBuffer * pDepthBuffer = pCurrent->m_isDepthBuffer ? depthBufferList().findBuffer(pCurrent->m_startAddress) : depthBufferList().getCurrent();

	if (pCurrent->m_FBO.isNotNull() && pDepthBuffer != nullptr) {
		pDepthBuffer->initDepthImageTexture(pCurrent);
		pDepthBuffer->initDepthBufferTexture(pCurrent);

		bool goodDepthBufferTexture = false;
		if (Context::DepthFramebufferTextures) {
			if (Context::WeakBlitFramebuffer)
				goodDepthBufferTexture = pDepthBuffer->m_pDepthBufferTexture->width == pCurrent->m_pTexture->width;
			else
				goodDepthBufferTexture = pDepthBuffer->m_pDepthBufferTexture->width >= pCurrent->m_pTexture->width ||
											std::abs(static_cast<s32>(pCurrent->m_width) - static_cast<s32>(pDepthBuffer->m_width)) < 2;
		} else {
			goodDepthBufferTexture = pDepthBuffer->m_depthRenderbufferWidth == pCurrent->m_pTexture->width;
		}

		if (goodDepthBufferTexture) {
			pCurrent->m_pDepthBuffer = pDepthBuffer;
			pDepthBuffer->setDepthAttachment(pCurrent->m_FBO, bufferTarget::DRAW_FRAMEBUFFER);
			if (config.frameBufferEmulation.N64DepthCompare != Config::dcDisable)
				pDepthBuffer->bindDepthImageTexture(pCurrent->m_FBO);
		} else
			pCurrent->m_pDepthBuffer = nullptr;
	} else
		pCurrent->m_pDepthBuffer = nullptr;

	assert(!gfxContext.isFramebufferError());
}

void FrameBufferList::clearDepthBuffer(DepthBuffer * _pDepthBuffer)
{
	for (auto iter = m_list.begin(); iter != m_list.end(); ++iter) {
		if (iter->m_pDepthBuffer == _pDepthBuffer) {
			iter->m_pDepthBuffer = nullptr;
		}
	}
}

void FrameBuffer_Init()
{
	frameBufferList().init();
	if (config.frameBufferEmulation.enable != 0) {
	ColorBufferToRDRAM::get().init();
	DepthBufferToRDRAM::get().init();
	RDRAMtoColorBuffer::get().init();
	}
}

void FrameBuffer_Destroy()
{
	RDRAMtoColorBuffer::get().destroy();
	DepthBufferToRDRAM::get().destroy();
	ColorBufferToRDRAM::get().destroy();
	frameBufferList().destroy();
}

void FrameBufferList::_renderScreenSizeBuffer()
{
	if (m_list.empty())
		return;

	DisplayWindow & wnd = dwnd();
	GraphicsDrawer & drawer = wnd.getDrawer();
	FrameBuffer *pBuffer = &m_list.back();
	PostProcessor & postProcessor = PostProcessor::get();
	FrameBuffer * pFilteredBuffer = pBuffer;
	for (const auto & f : postProcessor.getPostprocessingList())
		pFilteredBuffer = f(postProcessor, pFilteredBuffer);
	CachedTexture * pBufferTexture = pFilteredBuffer->m_pTexture;

	const u32 wndWidth = wnd.getWidth();
	const u32 wndHeight = wnd.getHeight();
	s32 srcCoord[4] = { 0, 0, static_cast<s32>(wndWidth), static_cast<s32>(wndHeight) };

	const u32 screenWidth = wnd.getScreenWidth();
	const u32 screenHeight = wnd.getScreenHeight();
	const u32 wndHeightOffset = wnd.getHeightOffset();
	const s32 hOffset = (screenWidth - wndWidth) / 2;
	const s32 vOffset = (screenHeight - wndHeight) / 2 + wndHeightOffset;
	s32 dstCoord[4] = { hOffset, vOffset, hOffset + static_cast<s32>(wndWidth), vOffset + static_cast<s32>(wndHeight) };

	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);

	gfxContext.clearColorBuffer(0.0f, 0.0f, 0.0f, 0.0f);

	GraphicsDrawer::BlitOrCopyRectParams blitParams;
	blitParams.srcX0 = srcCoord[0];
	blitParams.srcY0 = srcCoord[3];
	blitParams.srcX1 = srcCoord[2];
	blitParams.srcY1 = srcCoord[1];
	blitParams.srcWidth = wndWidth;
	blitParams.srcHeight = wndHeight;
	blitParams.dstX0 = dstCoord[0];
	blitParams.dstY0 = dstCoord[1];
	blitParams.dstX1 = dstCoord[2];
	blitParams.dstY1 = dstCoord[3];
	blitParams.dstWidth = screenWidth;
	blitParams.dstHeight = screenHeight + wndHeightOffset;
	const bool downscale = blitParams.srcWidth >= blitParams.dstWidth || blitParams.srcHeight >= blitParams.dstHeight;
	blitParams.filter = downscale || config.generalEmulation.enableHybridFilter > 0 ?
		textureParameters::FILTER_LINEAR :
		textureParameters::FILTER_NEAREST; //upscale; hybridFilter disabled
	blitParams.mask = blitMask::COLOR_BUFFER;
	blitParams.tex[0] = pBufferTexture;
	blitParams.combiner = downscale ? CombinerInfo::get().getTexrectDownscaleCopyProgram() :
		CombinerInfo::get().getTexrectUpscaleCopyProgram();
	blitParams.readBuffer = pFilteredBuffer->m_FBO;

	drawer.blitOrCopyTexturedRect(blitParams);

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);

	wnd.swapBuffers();
	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, pBuffer->m_FBO);
	if (config.frameBufferEmulation.forceDepthBufferClear != 0) {
		drawer.clearDepthBuffer();
	}
	gDP.changed |= CHANGED_SCISSOR;
}

void FrameBufferList::RdpUpdate::init()
{
	oldvstart = 0U;
	prevvicurrent = 0U;
	prevwasblank = false;
	prevserrate = false;
	oldlowerfield = false;
	emucontrolsvicurrent = -1;
}

/* This function was taken from angrylion's code and adopted for my needs */
bool FrameBufferList::RdpUpdate::update(RdpUpdateResult & _result)
{
	static const s32 PRESCALE_WIDTH = 640U;
	static const s32 PRESCALE_HEIGHT = 625U;

	const s32 x_add = _SHIFTR(*REG.VI_X_SCALE, 0, 12);
	const s32 y_add = _SHIFTR(*REG.VI_Y_SCALE, 0, 12);
	const u32 v_sync = _SHIFTR(*REG.VI_V_SYNC, 0, 10);
	const bool ispal = (v_sync > 550);
	const s32 x1 = _SHIFTR( *REG.VI_H_START, 16, 10 );
	const s32 y1 = _SHIFTR( *REG.VI_V_START, 16, 10 );
	const s32 x2 = _SHIFTR( *REG.VI_H_START, 0, 10 );
	s32 y2 = _SHIFTR( *REG.VI_V_START, 0, 10 );
	if (y2 < y1)
		y2 = ispal ? 44 + 576 : 34 + 480;

	const s32 delta_x = x2 - x1;
	const s32 delta_y = y2 - y1;
	const u32 vitype = _SHIFTR( *REG.VI_STATUS, 0, 2 );

	const bool serration_pulses = (*REG.VI_STATUS & VI_STATUS_SERRATE_ENABLED) != 0;
	const bool validinterlace = ((vitype & 2) != 0 ) && serration_pulses;
	if (validinterlace && prevserrate && emucontrolsvicurrent < 0)
		emucontrolsvicurrent = (*REG.VI_V_CURRENT_LINE & 1) != prevvicurrent ? 1 : 0;

	bool lowerfield = 0;
	if (validinterlace) {
		if (emucontrolsvicurrent == 1)
			lowerfield = (*REG.VI_V_CURRENT_LINE & 1) == 0;
		else if (!emucontrolsvicurrent) {
			if (y1 == oldvstart)
				lowerfield = !oldlowerfield;
			else
				lowerfield = y1 < oldvstart;
		}
	}

	oldlowerfield = lowerfield;

	if (validinterlace) {
		prevserrate = true;
		prevvicurrent = *REG.VI_V_CURRENT_LINE & 1;
		oldvstart = y1;
	} else
		prevserrate = false;

	s32 hres = delta_x;
	s32 vres = delta_y;
	s32 h_start = x1 - (ispal ? 128 : 108);
	s32 v_start = (y1 - (ispal ? 44 : 34)) / 2;
	s32 x_start = _SHIFTR(*REG.VI_X_SCALE, 16, 12);
	s32 y_start = _SHIFTR(*REG.VI_Y_SCALE, 16, 12);

	bool h_start_clamped = h_start < 0;
	if (h_start < 0) {
		x_start -= x_add * h_start;
		hres += h_start;

		h_start = 0;
	}

	if (v_start < 0) {
		y_start += (y_add * (-v_start));
		v_start = 0;
	}

	vres >>= 1;

	const bool hres_clamped = hres + h_start > PRESCALE_WIDTH;
	if (hres_clamped)
		hres = PRESCALE_WIDTH - h_start;
	if (vres + v_start > PRESCALE_HEIGHT)
		vres = PRESCALE_HEIGHT - v_start;

	s32 vactivelines = static_cast<s32>(v_sync - (ispal ? 44 : 34));
	if (vactivelines > PRESCALE_HEIGHT) {
		LOG(LOG_VERBOSE, "VI_V_SYNC_REG too big");
		return false;
	}

	if (vactivelines < 0) {
		LOG(LOG_VERBOSE, "vactivelines lesser than 0");
		return false;
	}

	if (hres <= 0 || vres <= 0 || ((vitype & 2) == 0 && prevwasblank)) /* early return. */
		return false;

	if ((vitype & 2) == 0) {
		prevwasblank = true;
		return false;
	}

	prevwasblank = false;

	_result.vi_hres = static_cast<u32>(hres);
	_result.vi_vres = static_cast<u32>(vres);
	_result.vi_ispal = ispal;
	_result.vi_h_start = static_cast<u32>(h_start);
	_result.vi_v_start = static_cast<u32>(v_start);
	_result.vi_x_start = static_cast<u32>(x_start);
	_result.vi_y_start = static_cast<u32>(y_start);
	_result.vi_x_add = static_cast<u32>(x_add);
	_result.vi_y_add = static_cast<u32>(y_add);
	_result.vi_minhpass = h_start_clamped ? 0 : 8;
	_result.vi_maxhpass = hres_clamped ? 0 : 7;
	_result.vi_width = _SHIFTR(*REG.VI_WIDTH, 0, 12);
	_result.vi_lowerfield = lowerfield;
	_result.vi_origin = _SHIFTR(*REG.VI_ORIGIN, 0, 24);
	_result.vi_fsaa = (*REG.VI_STATUS & 512) == 0;
	_result.vi_divot = (*REG.VI_STATUS & VI_STATUS_DIVOT_ENABLED) != 0;
	return true;

#if 0
	{
		int pixels;
		int prevy, y_start;
		int cur_x, line_x;
		register int i;
		const int VI_width = *GET_GFX_INFO(VI_WIDTH) & 0x00000FFF;
		const int x_add = *GET_GFX_INFO(VI_X_SCALE) & 0x00000FFF;
		const int y_add = *GET_GFX_INFO(VI_Y_SCALE) & 0x00000FFF;

		y_start = *GET_GFX_INFO(VI_Y_SCALE) >> 16 & 0x0FFF;

		//while (--vres >= 0)
		{
			x_start = *GET_GFX_INFO(VI_X_SCALE) >> 16 & 0x0FFF;
			prescale_ptr += line_count;

			prevy = y_start >> 10;
			pixels = VI_width * prevy;

			//for (i = 0; i < hres; i++)
			{
				unsigned long pix;
				unsigned long addr;

				line_x = x_start >> 10;
				cur_x = pixels + line_x;

				x_start += x_add;
				addr = frame_buffer + 4 * cur_x;
				pix = *(int32_t *)(RDRAM + addr);
			}
			y_start += y_add;
		}
	}
#endif
}

s32 FrameBufferList::OverscanBuffer::getHOffset() const
{
	if (m_enabled)
		return 0;

	return m_hOffset;
}

s32 FrameBufferList::OverscanBuffer::getVOffset() const
{
	if (m_enabled)
		return 0;

	return m_vOffset;
}

f32 FrameBufferList::OverscanBuffer::getScaleX() const
{
	if (m_enabled)
		return m_scale;
	return dwnd().getScaleX();
}

f32 FrameBufferList::OverscanBuffer::getScaleY(u32 _fullHeight) const
{
	if (m_enabled)
		return m_scale;

	return static_cast<f32>(dwnd().getHeight()) / static_cast<f32>(_fullHeight);
}

void FrameBufferList::OverscanBuffer::init()
{
	m_enabled = config.frameBufferEmulation.enableOverscan != 0;
	if (m_enabled)
		m_FBO = gfxContext.createFramebuffer();

	DisplayWindow & wnd = dwnd();
	m_hOffset = (wnd.getScreenWidth() - wnd.getWidth()) / 2;
	m_vOffset = (wnd.getScreenHeight() - wnd.getHeight()) / 2;
	m_scale = wnd.getScaleX();
	m_drawingWidth = wnd.getWidth();
	m_bufferWidth = wnd.getScreenWidth();
	m_bufferHeight = wnd.getScreenHeight() + wnd.getHeightOffset();
}

void FrameBufferList::OverscanBuffer::destroy()
{
	gfxContext.deleteFramebuffer(m_FBO);
	m_FBO = graphics::ObjectHandle::null;
	textureCache().removeFrameBufferTexture(m_pTexture);
	m_pTexture = nullptr;
	textureCache().removeFrameBufferTexture(m_pDepthTexture);
	m_pDepthTexture = nullptr;
#if defined(OS_WINDOWS)
	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
	gfxContext.clearDepthBuffer();
#endif
}

void FrameBufferList::OverscanBuffer::setInputBuffer(const FrameBuffer *  _pBuffer)
{
	if (!m_enabled) {
		return;
	}

	if (m_pTexture != nullptr &&
		m_pTexture->width == _pBuffer->m_pTexture->width &&
		m_pTexture->height == _pBuffer->m_pTexture->height &&
		m_scale == _pBuffer->m_scale) {
		return;
	}

	textureCache().removeFrameBufferTexture(m_pTexture);
	m_pTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
	const CachedTexture * pSrcTexture = _pBuffer->m_pTexture;
	_initFrameBufferTexture(0,
		_pBuffer->m_width,
		VI_GetMaxBufferHeight(_pBuffer->m_width),
		_pBuffer->m_scale,
		pSrcTexture->format,
		pSrcTexture->size,
		m_pTexture);
	_setAndAttachBufferTexture(m_FBO, m_pTexture, 0, false);
	m_scale = _pBuffer->m_scale;
	m_drawingWidth = m_bufferWidth = m_pTexture->width;
	m_bufferHeight = m_pTexture->height;

	if (config.frameBufferEmulation.copyDepthToMainDepthBuffer == 0)
		return;

	// Init depth texture
	textureCache().removeFrameBufferTexture(m_pDepthTexture);
	m_pDepthTexture = textureCache().addFrameBufferTexture(textureTarget::TEXTURE_2D);
	DepthBuffer::_initDepthBufferTexture(_pBuffer, m_pDepthTexture, false);
	Context::FrameBufferRenderTarget params;
	params.attachment = bufferAttachment::DEPTH_ATTACHMENT;
	params.bufferHandle = m_FBO;
	params.bufferTarget = bufferTarget::DRAW_FRAMEBUFFER;
	params.textureHandle = m_pDepthTexture->name;
	params.textureTarget = textureTarget::TEXTURE_2D;
	gfxContext.addFrameBufferRenderTarget(params);
}

void FrameBufferList::OverscanBuffer::activate()
{
	if (!m_enabled) {
		gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
		return;
	}

	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, m_FBO);
}

void FrameBufferList::OverscanBuffer::draw(u32 _fullHeight, bool _PAL)
{
	if (!m_enabled)
		return;

	DisplayWindow & wnd = dwnd();
	GraphicsDrawer & drawer = wnd.getDrawer();

	gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
#if defined(OS_WINDOWS)
	gfxContext.clearDepthBuffer();
#endif
	GraphicsDrawer::BlitOrCopyRectParams blitParams;
	const auto & overscan = _PAL ? config.frameBufferEmulation.overscanPAL : config.frameBufferEmulation.overscanNTSC;
	const s32 left = static_cast<s32>(overscan.left * m_scale);
	const s32 right = static_cast<s32>(overscan.right * m_scale);
	const s32 top = static_cast<s32>(overscan.top * m_scale);
	const s32 bottom = static_cast<s32>(overscan.bottom * m_scale);
	blitParams.srcX0 = left;
	blitParams.srcY0 = static_cast<s32>(_fullHeight * m_scale) - bottom;
	blitParams.srcX1 = static_cast<s32>(m_bufferWidth) - right;
	blitParams.srcY1 = top;
	blitParams.srcWidth = m_pTexture->width;
	blitParams.srcHeight = m_pTexture->height;
	blitParams.dstX0 = m_hOffset;
	blitParams.dstY0 = m_vOffset + static_cast<s32>(wnd.getHeightOffset());
	blitParams.dstX1 = m_hOffset + static_cast<s32>(wnd.getWidth());
	blitParams.dstY1 = m_vOffset + static_cast<s32>(wnd.getHeight() + wnd.getHeightOffset());
	blitParams.dstWidth = wnd.getScreenWidth();
	blitParams.dstHeight = wnd.getScreenHeight() + wnd.getHeightOffset();
	blitParams.mask = blitMask::COLOR_BUFFER;
	blitParams.tex[0] = m_pTexture;
	const bool downscale = blitParams.srcWidth >= blitParams.dstWidth || blitParams.srcHeight >= blitParams.dstHeight;
	blitParams.filter = downscale || config.generalEmulation.enableHybridFilter > 0 ?
		textureParameters::FILTER_LINEAR :
		textureParameters::FILTER_NEAREST; //upscale; hybridFilter disabled
	if (config.frameBufferEmulation.copyDepthToMainDepthBuffer != 0) {
		blitParams.tex[1] = m_pDepthTexture;
		blitParams.combiner = downscale ? CombinerInfo::get().getTexrectColorAndDepthDownscaleCopyProgram() :
			CombinerInfo::get().getTexrectColorAndDepthUpscaleCopyProgram();
	}
	if (blitParams.combiner == nullptr) {
		// copyDepthToMainDepthBuffer not set or not supported
		blitParams.combiner = downscale ? CombinerInfo::get().getTexrectDownscaleCopyProgram() :
			CombinerInfo::get().getTexrectUpscaleCopyProgram();
	}

	blitParams.readBuffer = m_FBO;
	blitParams.invertY = false;

	gfxContext.clearColorBuffer(0.0f, 0.0f, 0.0f, 0.0f);
//	drawer.blitOrCopyTexturedRect(blitParams);
	drawer.copyTexturedRect(blitParams);
}

void FrameBufferList::renderBuffer()
{
	if (g_debugger.isDebugMode()) {
		g_debugger.draw();
		return;
	}

	if (config.frameBufferEmulation.enable == 0) {
		_renderScreenSizeBuffer();
		return;
	}

	RdpUpdateResult rdpRes;
	if (!m_rdpUpdate.update(rdpRes)) {
		gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
		gfxContext.clearColorBuffer(0.0f, 0.0f, 0.0f, 0.0f);
		dwnd().swapBuffers();
		if (m_pCurrent != nullptr)
			gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, m_pCurrent->m_FBO);
		return;
	}

	FrameBuffer *pBuffer = findBuffer(rdpRes.vi_origin);
	if (pBuffer == nullptr)
		return;
	pBuffer->m_isMainBuffer = true;
	m_overscan.setInputBuffer(pBuffer);

	DisplayWindow & wnd = dwnd();
	GraphicsDrawer & drawer = wnd.getDrawer();
	s32 srcY0, srcY1;
	s32 dstX0, dstX1, dstY0, dstY1;
	s32 srcWidth, srcHeight;
	s32 XoffsetLeft = 0, XoffsetRight = 0;
	s32 srcPartHeight = 0;
	s32 dstPartHeight = 0;

	dstY0 = static_cast<s32>(rdpRes.vi_v_start);

	const u32 vFullHeight = rdpRes.vi_ispal ? 288 : 240;
	const f32 dstScaleY = m_overscan.getScaleY(vFullHeight);

	const u32 addrOffset = ((rdpRes.vi_origin - pBuffer->m_startAddress) << 1 >> pBuffer->m_size);
	srcY0 = static_cast<s32>(addrOffset / pBuffer->m_width);
	if ((addrOffset != 0) && (pBuffer->m_width == addrOffset * 2))
		srcY0 = 1;

	if ((rdpRes.vi_width != addrOffset * 2) && (addrOffset % rdpRes.vi_width != 0))
		XoffsetRight = static_cast<s32>(rdpRes.vi_width - addrOffset % rdpRes.vi_width);
	if (XoffsetRight == static_cast<s32>(pBuffer->m_width)) {
		XoffsetRight = 0;
	} else if (XoffsetRight > static_cast<s32>(pBuffer->m_width / 2)) {
		XoffsetRight = 0;
		XoffsetLeft = static_cast<s32>(addrOffset % rdpRes.vi_width);
	}

	if (!rdpRes.vi_lowerfield) {
		if (srcY0 > 0 && (pBuffer->m_width > 320 || pBuffer->m_height > 240))
			--srcY0;
		if (dstY0 > 0)
			--dstY0;
	}

	if ((config.generalEmulation.hacks & hack_LodeRunner) != 0) {
		srcY0 = 1;
		XoffsetRight = XoffsetLeft = 0;
	}

	srcWidth = static_cast<s32>(min(rdpRes.vi_width, (rdpRes.vi_hres * rdpRes.vi_x_add) >> 10));
	srcHeight = static_cast<s32>(rdpRes.vi_width * ((rdpRes.vi_vres*rdpRes.vi_y_add + rdpRes.vi_y_start) >> 10) / pBuffer->m_width);

	const u32 stride = pBuffer->m_width << pBuffer->m_size >> 1;
	FrameBuffer *pNextBuffer = findBuffer(rdpRes.vi_origin + stride * min(u32(srcHeight) - 1, pBuffer->m_height - 1) - 1);
	if (pNextBuffer == pBuffer)
		pNextBuffer = nullptr;

	if (pNextBuffer != nullptr) {
		dstPartHeight = srcY0;
		srcPartHeight = srcY0;
		srcY1 = srcHeight;
		dstY1 = dstY0 + static_cast<s32>(rdpRes.vi_vres) - dstPartHeight;
	} else {
		dstY1 = dstY0 + static_cast<s32>(rdpRes.vi_vres);
		srcY1 = srcY0 + srcHeight;
	}
	PostProcessor & postProcessor = PostProcessor::get();
	FrameBuffer * pFilteredBuffer = pBuffer;
	for (const auto & f : postProcessor.getPostprocessingList())
		pFilteredBuffer = f(postProcessor, pFilteredBuffer);

	const f32 viScaleX = _FIXED2FLOAT(_SHIFTR(*REG.VI_X_SCALE, 0, 12), 10);
	const f32 srcScaleX = pFilteredBuffer->m_scale;
	const f32 dstScaleX = m_overscan.getDrawingWidth() / (640 * viScaleX);
	const s32 hx0 = static_cast<s32>(rdpRes.vi_h_start + rdpRes.vi_minhpass);
	const s32 h0 = (rdpRes.vi_ispal ? 128 : 108);
	const s32 hEnd = _SHIFTR(*REG.VI_H_START, 0, 10);
	const s32 hx1 = max(0, h0 + 640 - hEnd + static_cast<s32>(rdpRes.vi_maxhpass));
	//const s32 hx1 = hx0 + rdpRes.vi_hres;
	dstX0 = static_cast<s32>((hx0 * viScaleX + f32(XoffsetRight)) * dstScaleX);
	dstX1 = static_cast<s32>(m_overscan.getDrawingWidth()) - static_cast<s32>(hx1 * viScaleX * dstScaleX);

	const f32 srcScaleY = pFilteredBuffer->m_scale;
	CachedTexture * pBufferTexture = pFilteredBuffer->m_pTexture;
	const s32 cutleft = static_cast<s32>(rdpRes.vi_minhpass * viScaleX * srcScaleX);
	const s32 cutright = static_cast<s32>(rdpRes.vi_maxhpass * viScaleX * srcScaleX);
	s32 srcCoord[4] = { static_cast<s32>((XoffsetLeft) * srcScaleX) + cutleft,
						static_cast<s32>(srcY0*srcScaleY),
						static_cast<s32>((srcWidth + XoffsetLeft - XoffsetRight) * srcScaleX) - cutright,
						min(static_cast<s32>(srcY1*srcScaleY), static_cast<s32>(pBufferTexture->height)) };
	if (srcCoord[2] > pBufferTexture->width || srcCoord[3] > pBufferTexture->height) {
		removeBuffer(pBuffer->m_startAddress);
		return;
	}

	const s32 hOffset = m_overscan.getHOffset();
	const s32 vOffset = m_overscan.getVOffset();
	s32 dstCoord[4] = { dstX0 + hOffset,
						vOffset + static_cast<s32>(dstY0*dstScaleY),
						hOffset + dstX1,
						vOffset + static_cast<s32>(dstY1*dstScaleY) };

	ObjectHandle readBuffer;

	if (pFilteredBuffer->m_pTexture->frameBufferTexture == CachedTexture::fbMultiSample) {
		pFilteredBuffer->resolveMultisampledTexture(true);
		readBuffer = pFilteredBuffer->m_resolveFBO;
		pBufferTexture = pFilteredBuffer->m_pResolveTexture;
	} else {
		readBuffer = pFilteredBuffer->m_FBO;
	}

	m_overscan.activate();
	gfxContext.clearColorBuffer(0.0f, 0.0f, 0.0f, 0.0f);
#if defined(OS_WINDOWS)
	gfxContext.clearDepthBuffer();
#endif

	GraphicsDrawer::BlitOrCopyRectParams blitParams;
	blitParams.srcX0 = srcCoord[0];
	blitParams.srcY0 = srcCoord[1];
	blitParams.srcX1 = srcCoord[2];
	blitParams.srcY1 = srcCoord[3];
	blitParams.srcWidth = pBufferTexture->width;
	blitParams.srcHeight = pBufferTexture->height;
	blitParams.dstX0 = dstCoord[0];
	blitParams.dstY0 = dstCoord[1];
	blitParams.dstX1 = dstCoord[2];
	blitParams.dstY1 = dstCoord[3];
	blitParams.dstWidth = m_overscan.getBufferWidth();
	blitParams.dstHeight = m_overscan.getBufferHeight();
	blitParams.mask = blitMask::COLOR_BUFFER;
	blitParams.tex[0] = pBufferTexture;
	const bool downscale = blitParams.srcWidth >= blitParams.dstWidth || blitParams.srcHeight >= blitParams.dstHeight;
	blitParams.filter = downscale || config.generalEmulation.enableHybridFilter > 0 ?
		textureParameters::FILTER_LINEAR :
		textureParameters::FILTER_NEAREST; //upscale; hybridFilter disabled
	if (config.frameBufferEmulation.copyDepthToMainDepthBuffer != 0) {
		blitParams.tex[1] = pBuffer->m_pDepthTexture;
		blitParams.combiner = downscale ? CombinerInfo::get().getTexrectColorAndDepthDownscaleCopyProgram() :
			CombinerInfo::get().getTexrectColorAndDepthUpscaleCopyProgram();
	}
	if (blitParams.combiner == nullptr) {
		// copyDepthToMainDepthBuffer not set or not supported
		blitParams.combiner = downscale ? CombinerInfo::get().getTexrectDownscaleCopyProgram() :
			CombinerInfo::get().getTexrectUpscaleCopyProgram();
	}
	blitParams.readBuffer = readBuffer;
	blitParams.invertY = config.frameBufferEmulation.enableOverscan == 0;

	drawer.copyTexturedRect(blitParams);

	if (pNextBuffer != nullptr) {
		pNextBuffer->m_isMainBuffer = true;
		pFilteredBuffer = pNextBuffer;
		for (const auto & f : postProcessor.getPostprocessingList())
			pFilteredBuffer = f(postProcessor, pFilteredBuffer);
		srcY1 = srcPartHeight;
		dstY0 = dstY1;
		dstY1 = dstY0 + dstPartHeight;
		if (pFilteredBuffer->m_pTexture->frameBufferTexture == CachedTexture::fbMultiSample) {
			pFilteredBuffer->resolveMultisampledTexture();
			readBuffer = pFilteredBuffer->m_resolveFBO;
			pBufferTexture = pFilteredBuffer->m_pResolveTexture;
		} else {
			readBuffer = pFilteredBuffer->m_FBO;
			pBufferTexture = pFilteredBuffer->m_pTexture;
		}

		blitParams.srcY0 = 0;
		blitParams.srcY1 = min(static_cast<s32>(srcY1*srcScaleY), static_cast<s32>(pFilteredBuffer->m_pTexture->height));
		blitParams.srcWidth = pBufferTexture->width;
		blitParams.srcHeight = pBufferTexture->height;
		blitParams.dstY0 = vOffset + static_cast<s32>(dstY0*dstScaleY);
		blitParams.dstY1 = vOffset + static_cast<s32>(dstY1*dstScaleY);
		blitParams.dstWidth = m_overscan.getBufferWidth();
		blitParams.dstHeight = m_overscan.getBufferHeight();
		blitParams.tex[0] = pBufferTexture;
		blitParams.tex[1] = pNextBuffer->m_pDepthTexture;
		blitParams.mask = blitMask::COLOR_BUFFER;
		blitParams.readBuffer = readBuffer;

		drawer.copyTexturedRect(blitParams);
	}

	gfxContext.bindFramebuffer(bufferTarget::READ_FRAMEBUFFER, ObjectHandle::defaultFramebuffer);
	m_overscan.draw(vFullHeight, rdpRes.vi_ispal);

	wnd.swapBuffers();
	if (m_pCurrent != nullptr) {
		gfxContext.bindFramebuffer(bufferTarget::DRAW_FRAMEBUFFER, m_pCurrent->m_FBO);
	}
	if (config.frameBufferEmulation.forceDepthBufferClear != 0) {
		drawer.clearDepthBuffer();
	}

	const s32 X = hOffset;
	const s32 Y = static_cast<s32>(wnd.getHeightOffset());
	const s32 W = static_cast<s32>(wnd.getWidth());
	const s32 H = static_cast<s32>(wnd.getHeight());

	gfxContext.setScissor(X, Y, W, H);
	gDP.changed |= CHANGED_SCISSOR;
}

void FrameBufferList::fillRDRAM(s32 ulx, s32 uly, s32 lrx, s32 lry)
{
	if (m_pCurrent == nullptr)
		return;

	if (config.frameBufferEmulation.copyFromRDRAM !=0 && !m_pCurrent->m_isDepthBuffer)
		// Do not write to RDRAM color buffer if copyFromRDRAM enabled.
		return;

	ulx = static_cast<s32>(min(max(static_cast<f32>(ulx), gDP.scissor.ulx), gDP.scissor.lrx));
	lrx = static_cast<s32>(min(max(static_cast<f32>(lrx), gDP.scissor.ulx), gDP.scissor.lrx));
	uly = static_cast<s32>(min(max(static_cast<f32>(uly), gDP.scissor.uly), gDP.scissor.lry));
	lry = static_cast<s32>(min(max(static_cast<f32>(lry), gDP.scissor.uly), gDP.scissor.lry));

	const u32 stride = gDP.colorImage.width << gDP.colorImage.size >> 1;
	const u32 lowerBound = gDP.colorImage.address + static_cast<u32>(lry)*stride;
	if (lowerBound > RDRAMSize)
		lry -= (lowerBound - RDRAMSize) / stride;
	u32 ci_width_in_dwords = gDP.colorImage.width >> (3 - gDP.colorImage.size);
	ulx >>= (3 - gDP.colorImage.size);
	lrx >>= (3 - gDP.colorImage.size);
	u32 * dst = reinterpret_cast<u32*>(RDRAM + gDP.colorImage.address);
	dst += static_cast<u32>(uly) * ci_width_in_dwords;
	if (!isMemoryWritable(dst, lowerBound - gDP.colorImage.address))
		return;
	for (s32 y = uly; y < lry; ++y) {
		for (s32 x = ulx; x < lrx; ++x) {
			dst[x] = gDP.fillColor.color;
		}
		dst += ci_width_in_dwords;
	}

	m_pCurrent->setBufferClearParams(gDP.fillColor.color, ulx, uly, lrx, lry);
}

void FrameBuffer_ActivateBufferTexture(u32 t, u32 _frameBufferAddress)
{
	FrameBuffer * pBuffer = frameBufferList().getBuffer(_frameBufferAddress);
	if (pBuffer == nullptr)
		return;

	CachedTexture *pTexture = pBuffer->getTexture(t);
	if (pTexture == nullptr)
		return;

//	frameBufferList().renderBuffer(pBuffer->m_startAddress);
	textureCache().activateTexture(t, pTexture);
	gDP.changed |= CHANGED_FB_TEXTURE;
}

void FrameBuffer_ActivateBufferTextureBG(u32 t, u32 _frameBufferAddress)
{
	FrameBuffer * pBuffer = frameBufferList().getBuffer(_frameBufferAddress);
	if (pBuffer == nullptr)
		return;

	CachedTexture *pTexture = pBuffer->getTextureBG();
	if (pTexture == nullptr)
		return;

//	frameBufferList().renderBuffer(pBuffer->m_startAddress);
	textureCache().activateTexture(t, pTexture);
	gDP.changed |= CHANGED_FB_TEXTURE;
}

void FrameBuffer_CopyToRDRAM(u32 _address, bool _sync)
{
	ColorBufferToRDRAM::get().copyToRDRAM(_address, _sync);
}

void FrameBuffer_CopyChunkToRDRAM(u32 _address)
{
	ColorBufferToRDRAM::get().copyChunkToRDRAM(_address);
}

bool FrameBuffer_CopyDepthBuffer( u32 address )
{
	FrameBufferList & fblist = frameBufferList();
	FrameBuffer * pCopyBuffer = fblist.getCopyBuffer();
	if (pCopyBuffer != nullptr) {
		// This code is mainly to emulate Zelda MM camera.
		ColorBufferToRDRAM::get().copyToRDRAM(pCopyBuffer->m_startAddress, true);
		pCopyBuffer->m_RdramCopy.resize(0); // To disable validity check by RDRAM content. CPU may change content of the buffer for some unknown reason.
		fblist.setCopyBuffer(nullptr);
		return true;
	}

	if (DepthBufferToRDRAM::get().copyToRDRAM(address)) {
		fblist.depthBufferCopyRdram();
		return true;
	}

	return false;
}

bool FrameBuffer_CopyDepthBufferChunk(u32 address)
{
	return DepthBufferToRDRAM::get().copyChunkToRDRAM(address);
}

void FrameBuffer_CopyFromRDRAM(u32 _address, bool _bCFB)
{
	RDRAMtoColorBuffer::get().copyFromRDRAM(_address, _bCFB);
}

void FrameBuffer_AddAddress(u32 address, u32 _size)
{
	RDRAMtoColorBuffer::get().addAddress(address, _size);
}

u32 cutHeight(u32 _address, u32 _height, u32 _stride)
{
	return _cutHeight(_address, _height, _stride);
}

void calcCoordsScales(const FrameBuffer * _pBuffer, f32 & _scaleX, f32 & _scaleY)
{
	const u32 bufferWidth = _pBuffer != nullptr ? _pBuffer->m_width : VI.width;
	const u32 bufferHeight = VI_GetMaxBufferHeight(static_cast<u16>(bufferWidth));
	_scaleX = 1.0f / f32(bufferWidth);
	_scaleY = 1.0f / f32(bufferHeight);
}
