#include "pch.h"
#include "GBA/GbaPpu.h"
#include "GBA/GbaTypes.h"
#include "GBA/GbaConsole.h"
#include "GBA/GbaMemoryManager.h"
#include "GBA/GbaDmaController.h"
#include "GBA/Debugger/GbaPpuTools.h"
#include "Debugger/DebugTypes.h"
#include "Shared/Emulator.h"
#include "Shared/EmuSettings.h"
#include "Shared/BaseControlManager.h"
#include "Shared/RewindManager.h"
#include "Shared/MessageManager.h"
#include "Shared/RenderedFrame.h"
#include "Shared/Video/VideoDecoder.h"
#include "Shared/Video/VideoRenderer.h"
#include "Shared/EventType.h"
#include "Shared/NotificationManager.h"
#include "Utilities/BitUtilities.h"
#include "Utilities/Serializer.h"
#include "Utilities/StaticFor.h"

void GbaPpu::Init(Emulator* emu, GbaConsole* console, GbaMemoryManager* memoryManager)
{
	_emu = emu;
	_console = console;
	_memoryManager = memoryManager;

	_state = {};

	_paletteRam = (uint16_t*)_emu->GetMemory(MemoryType::GbaPaletteRam).Memory;
	_vram = (uint8_t*)_emu->GetMemory(MemoryType::GbaVideoRam).Memory;
	_vram16 = (uint16_t*)_emu->GetMemory(MemoryType::GbaVideoRam).Memory;
	_oam = (uint32_t*)_emu->GetMemory(MemoryType::GbaSpriteRam).Memory;

	_outputBuffers[0] = new uint16_t[GbaConstants::PixelCount];
	_outputBuffers[1] = new uint16_t[GbaConstants::PixelCount];
	memset(_outputBuffers[0], 0, GbaConstants::PixelCount * sizeof(uint16_t));
	memset(_outputBuffers[1], 0, GbaConstants::PixelCount * sizeof(uint16_t));
	_currentBuffer = _outputBuffers[0];

	_oamReadOutput = _oamOutputBuffers[0];
	_oamWriteOutput = _oamOutputBuffers[1];

	if(_emu->GetSettings()->GetGbaConfig().SkipBootScreen) {
		//BIOS leaves PPU registers in this state, some games expect this
		_state.Control = 0x80;
		_state.ForcedBlank = true;
		_state.Transform[0].Matrix[0] = 0x100;
		_state.Transform[0].Matrix[3] = 0x100;
		_state.Transform[1].Matrix[0] = 0x100;
		_state.Transform[1].Matrix[3] = 0x100;
	}

	//All layers are always active when no window is enabled
	for(int i = 0; i < 6; i++) {
		_state.WindowActiveLayers[4][i] = true;
	}
	
	StaticFor<0, 128>::Apply([=](auto i) {
		_colorMathFunc[i] = &GbaPpu::ProcessColorMath<(GbaPpuBlendEffect)(i >> 5), (bool)(i & 0x01), (bool)(i & 0x02), (bool)(i & 0x04), (bool)(i & 0x08), (bool)(i & 0x10)>;
	});
}

GbaPpu::~GbaPpu()
{
	delete[] _outputBuffers[0];
	delete[] _outputBuffers[1];
}

void GbaPpu::ProcessHBlank()
{
	if(_state.Scanline < 160) {
		RenderScanline();
		_console->GetDmaController()->TriggerDma(GbaDmaTrigger::HBlank);
	}

	if(_state.HblankIrqEnabled) {
		_console->GetMemoryManager()->SetDelayedIrqSource(GbaIrqSource::LcdHblank, 4);
	}
}

void GbaPpu::ProcessEndOfScanline()
{
	ProcessSprites();
	ProcessWindow();

	if(!_skipRender && _emu->IsDebugging()) {
		DebugProcessMemoryAccessView();
	}

	memset(_memoryAccess, GbaPpuMemAccess::None, sizeof(_memoryAccess));

	_state.Cycle = 0;
	_state.Scanline++;

	//Reset renderer data for next scanline
	_lastRenderCycle = -1;
	_lastWindowCycle = -1;
	_oamLastCycle = -1;
	std::fill(_layerOutput[0], _layerOutput[0] + 240, GbaPixelData {});
	std::fill(_layerOutput[1], _layerOutput[1] + 240, GbaPixelData {});
	std::fill(_layerOutput[2], _layerOutput[2] + 240, GbaPixelData {});
	std::fill(_layerOutput[3], _layerOutput[3] + 240, GbaPixelData {});

	for(int i = 0; i < 2; i++) {
		if(_state.Transform[i].PendingUpdateX) {
			_state.Transform[i].LatchOriginX = (_state.Transform[i].OriginX << 4) >> 4; //sign extend
			_state.Transform[i].PendingUpdateX = false;
		}
		if(_state.Transform[i].PendingUpdateY) {
			_state.Transform[i].LatchOriginY = (_state.Transform[i].OriginY << 4) >> 4; //sign extend
			_state.Transform[i].PendingUpdateY = false;
		}
	}

	for(int i = 0; i < 4; i++) {
		if(_state.BgLayers[i].EnableTimer && --_state.BgLayers[i].EnableTimer == 0) {
			_state.BgLayers[i].Enabled = true;
		}

		//Unverified: Latch X scroll value at the start of each scanline
		//This fixes display issues in the Fire Emblem Sacred Stones menu
		_state.BgLayers[i].ScrollXLatch = _state.BgLayers[i].ScrollX;
	}

	if(_state.Scanline >= 2 && _state.Scanline < 162 && _triggerSpecialDma) {
		//"Video Capture Mode" dma, channel 3 only - auto-stops on scanline 161
		_console->GetDmaController()->TriggerDmaChannel(GbaDmaTrigger::Special, 3, _state.Scanline == 161);
	} else if(_state.Scanline == 162) {
		_triggerSpecialDma = _console->GetDmaController()->IsVideoCaptureDmaEnabled();
	}

	if(_state.Scanline == 160) {
		_oamScanline = 0;
		SendFrame();
		if(_state.VblankIrqEnabled) {
			_console->GetMemoryManager()->SetIrqSource(GbaIrqSource::LcdVblank);
		}
		_console->GetDmaController()->TriggerDma(GbaDmaTrigger::VBlank);
	} else if(_state.Scanline == 228) {
		_state.Scanline = 0;

		//Transform values latched at the start of the frame
		_state.Transform[0].LatchOriginX = (int32_t)_state.Transform[0].OriginX;
		_state.Transform[0].LatchOriginY = (int32_t)_state.Transform[0].OriginY;
		_state.Transform[1].LatchOriginX = (int32_t)_state.Transform[1].OriginX;
		_state.Transform[1].LatchOriginY = (int32_t)_state.Transform[1].OriginY;

		if(_emu->GetSettings()->GetGbaConfig().DisableSprites) {
			std::fill(_oamOutputBuffers[0], _oamOutputBuffers[0] + 240, GbaPixelData {});
			std::fill(_oamOutputBuffers[1], _oamOutputBuffers[1] + 240, GbaPixelData {});
		}

		_emu->ProcessEvent(EventType::StartFrame, CpuType::Gba);

		EmuSettings* settings = _emu->GetSettings();
		_skipRender = (
			!settings->GetGbaConfig().DisableFrameSkipping &&
			!_emu->GetRewindManager()->IsRewinding() &&
			!_emu->GetVideoRenderer()->IsRecording() &&
			(settings->GetEmulationSpeed() == 0 || settings->GetEmulationSpeed() > 150) &&
			_frameSkipTimer.GetElapsedMS() < 15
		);
		if(!_skipRender) {
			_currentBuffer = _currentBuffer == _outputBuffers[0] ? _outputBuffers[1] : _outputBuffers[0];
		}
	}

	if(_state.ScanlineIrqEnabled && _state.Scanline == _state.Lyc) {
		_console->GetMemoryManager()->SetIrqSource(GbaIrqSource::LcdScanlineMatch);
	}

	InitializeWindows();
}

void GbaPpu::SendFrame()
{
	_emu->ProcessEvent(EventType::EndFrame, CpuType::Gba);
	_emu->GetNotificationManager()->SendNotification(ConsoleNotificationType::PpuFrameDone);

	RenderedFrame frame(_currentBuffer, GbaConstants::ScreenWidth, GbaConstants::ScreenHeight, 1.0, _state.FrameCount, _console->GetControlManager()->GetPortStates());
	bool rewinding = _emu->GetRewindManager()->IsRewinding();
	_emu->GetVideoDecoder()->UpdateFrame(frame, rewinding, rewinding);

	_emu->ProcessEndOfFrame();
	_console->ProcessEndOfFrame();

	_state.FrameCount++;

	if(!_skipRender) {
		_frameSkipTimer.Reset();
	}
}

void GbaPpu::DebugSendFrame()
{
	RenderScanline(true);

	int lastDrawnPixel = std::clamp((_state.Cycle - 46) / 4, 0, 239);

	int offset = lastDrawnPixel + 1 + _state.Scanline * GbaConstants::ScreenWidth;
	int pixelsToClear = GbaConstants::ScreenWidth * GbaConstants::ScreenHeight - offset;
	if(pixelsToClear > 0) {
		memset(_currentBuffer + offset, 0, pixelsToClear * sizeof(uint16_t));
	}

	RenderedFrame frame(_currentBuffer, GbaConstants::ScreenWidth, GbaConstants::ScreenHeight, 1.0, _state.FrameCount);
	_emu->GetVideoDecoder()->UpdateFrame(frame, false, false);
}

void GbaPpu::RenderScanline(bool forceRender)
{
	if(_skipRender && !forceRender) {
		return;
	}

	ProcessSprites();
	ProcessWindow();

	if(_state.Scanline >= 160 || _lastRenderCycle >= 1006) {
		return;
	}

	if(_state.ForcedBlank) {
		uint16_t* rowStart = _currentBuffer + (_state.Scanline * GbaConstants::ScreenWidth);
		std::fill(rowStart, rowStart + GbaConstants::ScreenWidth, 0x7FFF);
		return;
	}
	
	uint8_t activeLayers = 0;
	switch(_state.BgMode) {
		case 0:
			RenderTilemap<0>();
			RenderTilemap<1>();
			RenderTilemap<2>();
			RenderTilemap<3>();
			activeLayers = _state.Control2 & 0x0F;
			break;

		case 1:
			RenderTilemap<0>();
			RenderTilemap<1>();
			RenderTransformTilemap<2>();
			activeLayers = _state.Control2 & 0x07;
			break;

		case 2:
			RenderTransformTilemap<2>();
			RenderTransformTilemap<3>();
			activeLayers = _state.Control2 & 0x0C;
			break;

		case 3: RenderBitmapMode<3>(); activeLayers = _state.Control2 & 0x04; break;
		case 4: RenderBitmapMode<4>(); activeLayers = _state.Control2 & 0x04; break;
		case 5: RenderBitmapMode<5>(); activeLayers = _state.Control2 & 0x04; break;

		default: break;
	}

	if(_state.Cycle >= 46) {
		bool windowEnabled = _state.Window0Enabled || _state.Window1Enabled || _state.ObjWindowEnabled;
		(this->*_colorMathFunc[((int)_state.BlendEffect << 5) | ((int)windowEnabled << 4) | activeLayers])();
	}

	_lastRenderCycle = _state.Cycle;
}

template<GbaPpuBlendEffect effect, bool bg0Enabled, bool bg1Enabled, bool bg2Enabled, bool bg3Enabled, bool windowEnabled>
void GbaPpu::ProcessColorMath()
{
	uint16_t* dst = _skipRender ? _skippedOutput : (_currentBuffer + (_state.Scanline * GbaConstants::ScreenWidth));
	uint8_t mainCoeff = std::min<uint8_t>(16, _state.BlendMainCoefficient);
	uint8_t subCoeff = std::min<uint8_t>(16, _state.BlendSubCoefficient);
	
	GbaPixelData main = {};
	GbaPixelData sub = {};
	uint8_t brightness = std::min<uint8_t>(16, _state.Brightness);
	uint16_t blendColor = effect == GbaPpuBlendEffect::IncreaseBrightness ? GbaPpu::WhiteColor : GbaPpu::BlackColor;

	uint8_t wnd = 0;

	int start = _lastRenderCycle < 46 ? 0 : std::max(0, ((_lastRenderCycle - 46) / 4) + 1);
	int end = std::min((_state.Cycle - 46) / 4, 239);

	GbaPixelData sprPixel = {};

	for(int x = start; x <= end; x++) {

		if constexpr(windowEnabled) {
			wnd = _activeWindow[x];
			if(_state.WindowActiveLayers[wnd][GbaPpu::SpriteLayerIndex]) {
				main = _oamReadOutput[x];
			} else {
				main = {};
			}
		} else {
			wnd = GbaPpu::NoWindow;
			main = _oamReadOutput[x];
		}
		
		if(!(main.Color & GbaPpu::SpriteMosaicFlag) || !(sprPixel.Color & GbaPpu::SpriteMosaicFlag) || x % (_state.ObjMosaicSizeX + 1) == 0) {
			sprPixel = main;
		} else {
			main = sprPixel;
		}

		sub = {};

		if constexpr(bg0Enabled) {
			ProcessLayerPixel<0, windowEnabled>(x, wnd, main, sub);
		}
		if constexpr(bg1Enabled) {
			ProcessLayerPixel<1, windowEnabled>(x, wnd, main, sub);
		}
		if constexpr(bg2Enabled) {
			ProcessLayerPixel<2, windowEnabled>(x, wnd, main, sub);
		}
		if constexpr(bg3Enabled) {
			ProcessLayerPixel<3, windowEnabled>(x, wnd, main, sub);
		}

		if((main.Color & (GbaPpu::SpriteBlendFlag | GbaPpu::DirectColorFlag)) == GbaPpu::SpriteBlendFlag && _state.BlendSub[sub.Layer]) {
			//Sprite transparency is applied before anything else
			BlendColors(dst, x, ReadColor<false>(x, main.Color), mainCoeff, ReadColor<true>(x, sub.Color), subCoeff);
		} else {
			if constexpr(effect == GbaPpuBlendEffect::None) {
				dst[x] = ReadColor<false>(x, main.Color);
			} else if constexpr(effect == GbaPpuBlendEffect::AlphaBlend) {
				if(_state.BlendSub[sub.Layer]) {
					if(!_state.BlendMain[main.Layer] || !_state.WindowActiveLayers[wnd][GbaPpu::EffectLayerIndex]) {
						dst[x] = ReadColor<false>(x, main.Color);
					} else {
						BlendColors(dst, x, ReadColor<false>(x, main.Color), mainCoeff, ReadColor<true>(x, sub.Color), subCoeff);
					}
				} else {
					dst[x] = ReadColor<false>(x, main.Color);
				}
			} else {
				if(brightness == 0 || !_state.BlendMain[main.Layer] || !_state.WindowActiveLayers[wnd][GbaPpu::EffectLayerIndex]) {
					dst[x] = ReadColor<false>(x, main.Color);
				} else {
					BlendColors(dst, x, ReadColor<false>(x, main.Color), 16 - brightness, blendColor, brightness);
				}
			}
		}
	}

	if(_state.GreenSwapEnabled) {
		for(int x = start & ~1; x + 1 <= end; x+=2) {
			uint16_t gLeft = dst[x] & 0x3E0;
			uint16_t gRight = dst[x+1] & 0x3E0;
			dst[x] = (dst[x] & ~0x3E0) | gRight;
			dst[x+1] = (dst[x+1] & ~0x3E0) | gLeft;
		}
	}
}

template<bool isSubColor>
uint16_t GbaPpu::ReadColor(int x, uint16_t addr)
{
	if(addr & GbaPpu::DirectColorFlag) {
		return addr & 0x7FFF;
	} else {
		_memoryAccess[(x << 2) + (isSubColor ? 48 : 46)] |= GbaPpuMemAccess::Palette;
		return _paletteRam[(addr >> 1) & 0x1FF] & 0x7FFF;
	}
}

void GbaPpu::BlendColors(uint16_t* dst, int x, uint16_t main, uint8_t aCoeff, uint16_t sub, uint8_t bCoeff)
{
	uint8_t aR = main & 0x1F;
	uint8_t aG = (main >> 5) & 0x1F;
	uint8_t aB = (main >> 10) & 0x1F;

	uint8_t bR = sub & 0x1F;
	uint8_t bG = (sub >> 5) & 0x1F;
	uint8_t bB = (sub >> 10) & 0x1F;

	uint32_t r = std::min(31, (aR * aCoeff + bR * bCoeff) >> 4);
	uint32_t g = std::min(31, (aG * aCoeff + bG * bCoeff) >> 4);
	uint32_t b = std::min(31, (aB * aCoeff + bB * bCoeff) >> 4);

	dst[x] = r | (g << 5) | (b << 10);
}

void GbaPpu::InitializeWindows()
{
	//Windows are enabled/disabled when the scanline reaches the start/end scanlines
	//See window_midframe test - unsure about behavior when top==bottom
	if(_state.Scanline == _state.Window[0].TopY) {
		_window0ActiveY = _state.Window0Enabled;
	}
	if(_state.Scanline == _state.Window[0].BottomY) {
		_window0ActiveY = false;
	}

	if(_state.Scanline == _state.Window[1].TopY) {
		_window1ActiveY = _state.Window1Enabled;
	}
	if(_state.Scanline == _state.Window[1].BottomY) {
		_window1ActiveY = false;
	}

	memset(_activeWindow, GbaPpu::OutsideWindow, sizeof(_activeWindow));
}

void GbaPpu::ProcessWindow()
{
	//Using the same logic as for window Y above, enable/disable windows when
	//the current pixel matches the left/right X value for the window
	//Some games set left > right, essentially making the window
	//wrap around to the beginning of the next scanline.
	//(MM&B does this for speech bubbles)
	//TODOGBA is there any test rom for this (window x)?
	int x = (_lastWindowCycle + 1) / 4;
	int end = std::min(_state.Cycle / 4, 255) + 1;
	if(x >= end) {
		return;
	}
	
	if(_state.Window0Enabled || _state.Window1Enabled) {
		for(int i = -1; i < 4; i++) {
			uint16_t changePos = i < 0 ? 0 : _windowChangePos[i];
			uint16_t nextChange = i < 3 ? _windowChangePos[i + 1] : 256;
			if(nextChange == changePos || (changePos < x && nextChange < x)) {
				continue;
			}

			if(x == _state.Window[0].LeftX) {
				_window0ActiveX = _state.Window0Enabled;
			}
			if(x == _state.Window[0].RightX) {
				_window0ActiveX = false;
			}

			if(x == _state.Window[1].LeftX) {
				_window1ActiveX = _state.Window1Enabled;
			}
			if(x == _state.Window[1].RightX) {
				_window1ActiveX = false;
			}

			int length = std::min(nextChange - x, end - x);
			if(_window0ActiveX && _window0ActiveY) {
				memset(_activeWindow + x, GbaPpu::Window0, length);
			} else if(_window1ActiveX && _window1ActiveY) {
				memset(_activeWindow + x, GbaPpu::Window1, length);
			}

			x += length;
			if(x >= end) {
				break;
			}
		}
	} else {
		_window0ActiveX = false;
		_window1ActiveX = false;
	}

	_lastWindowCycle = _state.Cycle;
}

void GbaPpu::SetWindowX(uint8_t& regValue, uint8_t newValue)
{
	if(regValue != newValue) {
		regValue = newValue;
		UpdateWindowChangePoints();
	}
}

void GbaPpu::UpdateWindowChangePoints()
{
	//Generate a sorted list of the positions where the windows start/end
	//Used in ProcessWindow to process the changes in chunks
	_windowChangePos[0] = _state.Window[0].LeftX;
	_windowChangePos[1] = _state.Window[0].RightX;
	_windowChangePos[2] = _state.Window[1].LeftX;
	_windowChangePos[3] = _state.Window[1].RightX;
	std::sort(_windowChangePos, _windowChangePos + 4);
}

void GbaPpu::SetPixelData(GbaPixelData& pixel, uint16_t color, uint8_t priority, uint8_t layer)
{
	pixel.Color = color;
	pixel.Priority = priority;
	pixel.Layer = layer;
}

template<int i, bool mosaic, bool bpp8>
void GbaPpu::PushBgPixels()
{
	if(_layerData[i].HoriMirror) {
		PushBgPixels<i, mosaic, bpp8, true>();
	} else {
		PushBgPixels<i, mosaic, bpp8, false>();
	}
}

template<int i, bool mosaic, bool bpp8, bool mirror>
void GbaPpu::PushBgPixels()
{
	GbaLayerRendererData& data = _layerData[i];

	uint16_t tileData = _vram16[data.FetchAddr >> 1];
	if constexpr(mirror) {
		if constexpr(bpp8) {
			tileData = ((tileData & 0xFF00) >> 8) | ((tileData & 0x00FF) << 8);
		} else {
			tileData = (
				((tileData & 0xF000) >> 12) | 
				((tileData & 0x0F00) >> 4) |
				((tileData & 0x00F0) << 4) |
				((tileData & 0x000F) << 12)
			);
		}
	}

	int16_t renderX = data.RenderX;
	constexpr int len = bpp8 ? 2 : 4;
	for(int x = 0; x < len; x++) {
		if(renderX >= 0 && renderX < 240) {
			if constexpr(mosaic) {
				if(renderX % (_state.BgMosaicSizeX + 1) == 0) {
					data.MosaicColor = tileData & (bpp8 ? 0xFF : 0x0F);
				}
			} else {
				data.MosaicColor = tileData & (bpp8 ? 0xFF : 0x0F);
			}

			if(data.MosaicColor != 0) {
				SetPixelData(_layerOutput[i][renderX], (data.PaletteIndex * 32) + data.MosaicColor * 2, _state.BgLayers[i].Priority, i);
			}
		}
		tileData >>= bpp8 ? 8 : 4;
		renderX++;
	}

	data.FetchAddr += (mirror ? -2 : 2);
	data.RenderX = renderX;
};

template<int i>
void GbaPpu::RenderTilemap()
{
	if(!_state.BgLayers[i].Enabled || _emu->GetSettings()->GetGbaConfig().HideBgLayers[i]) {
		return;
	}

	if(_state.BgLayers[i].Mosaic) {
		if(_state.BgLayers[i].Bpp8Mode) {
			RenderTilemap<i, true, true>();
		} else {
			RenderTilemap<i, true, false>();
		}
	} else {
		if(_state.BgLayers[i].Bpp8Mode) {
			RenderTilemap<i, false, true>();
		} else {
			RenderTilemap<i, false, false>();
		}
	}
}

template<int i, bool mosaic, bool bpp8>
void GbaPpu::RenderTilemap()
{
	GbaBgConfig& layer = _state.BgLayers[i];

	uint16_t baseAddr = layer.TilemapAddr >> 1;
	uint16_t scanline = _state.Scanline;
	if constexpr(mosaic) {
		scanline = scanline - scanline % (_state.BgMosaicSizeY + 1);
	}
	uint16_t yPos = layer.ScrollY + scanline;

	if(layer.DoubleHeight && (yPos & 0x100)) {
		baseAddr += layer.DoubleWidth ? 0x800 : 0x400;
	}

	yPos &= 0xFF;

	if(_lastRenderCycle == -1) {
		_layerData[i].RenderX = -(layer.ScrollXLatch & 0x07);
	}

	//MessageManager::Log(std::to_string(_state.Scanline) + " render " + std::to_string(_lastRenderCycle+1) + " to " + std::to_string(_state.Cycle));
	int gap = (31 + i - (layer.ScrollXLatch & 0x07) * 4);
	int cycle = std::max(0, _lastRenderCycle + 1 - gap);
	int end = std::min<int>(_state.Cycle, 1005) - gap;

	for(; cycle <= end; cycle++) {
		//MessageManager::Log(std::to_string(_state.Scanline) + " fetch cycle " + std::to_string(fetchCycle));
		switch(cycle & 0x1F) {
			case 0: {
				//Fetch tilemap data
				uint16_t xPos = layer.ScrollXLatch + _layerData[i].RenderX;
				uint16_t addr = baseAddr;
				if(layer.DoubleWidth && (xPos & 0x100)) {
					addr += 0x400;
				}
				xPos &= 0xFF;
				
				uint16_t vramAddr = addr + yPos / 8 * 32 + xPos / 8;
				uint16_t tilemapData = _vram16[vramAddr];
				_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;

				_layerData[i].TileIndex = tilemapData & 0x3FF;
				_layerData[i].HoriMirror = tilemapData & (1 << 10);
				_layerData[i].VertMirror = tilemapData & (1 << 11);
				_layerData[i].PaletteIndex = bpp8 ? 0 : (tilemapData >> 12) & 0x0F;

				_layerData[i].TileRow = _layerData[i].VertMirror ? (~yPos & 0x07) : (yPos & 0x07);
				_layerData[i].TileColumn = _layerData[i].HoriMirror ? (bpp8 ? 6 : 2) : 0;
				_layerData[i].FetchAddr = layer.TilesetAddr + _layerData[i].TileIndex * (bpp8 ? 64 : 32) + _layerData[i].TileRow * (bpp8 ? 8 : 4) + _layerData[i].TileColumn;

				//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tilemap");
				cycle += 3;
				break;
			}

			case 4: {
				//Fetch tile data (4bpp & 8bpp)
				PushBgPixels<i, mosaic, bpp8>();
				_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;

				//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tile data 0");
				cycle += bpp8 ? 7 : 15;
				break;
			}

			case 12: {
				//Fetch tile data (8bpp only)
				if constexpr(bpp8) {
					PushBgPixels<i, mosaic, bpp8>();
					_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
				}
				cycle += 7;
				break;
			}

			case 20: {
				//Fetch tile data (4bpp & 8bpp)
				PushBgPixels<i, mosaic, bpp8>();
				_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;

				//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tile data 2");
				cycle += bpp8 ? 7 : (11 - i);
				break;
			}
						
			case 28: {
				//Fetch tile data (8bpp only)
				if constexpr(bpp8) {
					PushBgPixels<i, mosaic, bpp8>();
					_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
				}
				cycle += 3 - i;
				break;
			}
		}
	}
}

template<int i>
void GbaPpu::RenderTransformTilemap()
{
	GbaBgConfig& layer = _state.BgLayers[i];
	GbaTransformConfig& cfg = _state.Transform[i - 2];
	if(!layer.Enabled || _emu->GetSettings()->GetGbaConfig().HideBgLayers[i]) {
		return;
	}

	uint16_t screenSize = 128 << layer.ScreenSize;

	if(_lastRenderCycle == -1) {
		_layerData[i].TransformX = (cfg.LatchOriginX << 4) >> 4; //sign extend
		_layerData[i].TransformY = (cfg.LatchOriginY << 4) >> 4;
		_layerData[i].RenderX = 0;
	}
	
	//MessageManager::Log(std::to_string(_state.Scanline) + " render " + std::to_string(_lastRenderCycle+1) + " to " + std::to_string(_state.Cycle));
	constexpr int gap = (37 - i * 2);
	int cycle = std::max(0, _lastRenderCycle + 1 - gap);
	int end = std::min<int>(_state.Cycle, 1005) - gap;

	for(; cycle <= end; cycle++) {
		switch(cycle & 0x03) {
			case 0: {
				//Fetch tilemap data
				uint32_t wrapMask = layer.WrapAround ? (screenSize - 1) : 0xFFFFF;
				uint16_t columnCount = screenSize >> 3;

				if(!layer.Mosaic || _layerData[i].RenderX % (_state.BgMosaicSizeX + 1) == 0) {
					//Ignore decimal point value (bottom 8 bits), apply wraparound behavior
					//This produces the x,y coordinate (on the tilemap) that needs to be drawn
					_layerData[i].XPos = (_layerData[i].TransformX >> 8) & wrapMask;
					_layerData[i].YPos = (_layerData[i].TransformY >> 8) & wrapMask;
				}

				uint16_t vramAddr = layer.TilemapAddr + (_layerData[i].YPos >> 3) * columnCount + (_layerData[i].XPos >> 3);
				_layerData[i].TileIndex = _vram[vramAddr];
				_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;

				_layerData[i].TileRow = _layerData[i].YPos & 0x07;
				_layerData[i].TileColumn = _layerData[i].XPos & 0x07;

				//Update x/y values for next dot
				_layerData[i].TransformX += cfg.Matrix[0];
				_layerData[i].TransformY += cfg.Matrix[2];
				//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch tilemap");
				break;
			}

			case 1: {
				//Fetch pixel
				_memoryAccess[cycle + gap] |= GbaPpuMemAccess::Vram;
				if(_layerData[i].RenderX < 240) {
					uint8_t color = _vram[(layer.TilesetAddr + _layerData[i].TileIndex * 64 + _layerData[i].TileRow * 8 + _layerData[i].TileColumn) & 0xFFFF];
					if(color != 0 && _layerData[i].XPos < screenSize && _layerData[i].YPos < screenSize) {
						SetPixelData(_layerOutput[i][_layerData[i].RenderX], color * 2, layer.Priority, i);
					}
				}
				_layerData[i].RenderX++;
				//MessageManager::Log(std::to_string(_state.Scanline) + "," + std::to_string(cycle) + " fetch pixel");
				cycle += 2;
				break;
			}
		}
	}

	if(_state.Cycle == 1006) {
		//Update x/y values for next scanline
		if(!layer.Mosaic) {
			cfg.LatchOriginX += cfg.Matrix[1];
			cfg.LatchOriginY += cfg.Matrix[3];
		} else if(_state.Scanline % (_state.BgMosaicSizeY + 1) == _state.BgMosaicSizeY) {
			cfg.LatchOriginX += cfg.Matrix[1] * (_state.BgMosaicSizeY + 1);
			cfg.LatchOriginY += cfg.Matrix[3] * (_state.BgMosaicSizeY + 1);
		}
	}
}

template<int mode>
void GbaPpu::RenderBitmapMode()
{
	GbaBgConfig& layer = _state.BgLayers[2];
	if(!layer.Enabled || _emu->GetSettings()->GetGbaConfig().HideBgLayers[2]) {
		return;
	}

	GbaTransformConfig& cfg = _state.Transform[0];

	if(_lastRenderCycle == -1) {
		_layerData[2].TransformX = (cfg.LatchOriginX << 4) >> 4; //sign extend
		_layerData[2].TransformY = (cfg.LatchOriginY << 4) >> 4;
		_layerData[2].RenderX = 0;
	}

	uint16_t screenWidth = mode == 5 ? 160 : 240;
	uint16_t screenHeight = mode == 5 ? 128 : 160;

	uint32_t base = (_state.DisplayFrameSelect && (mode == 4 || mode == 5)) ? 0xA000 : 0;

	constexpr int gap = 34;
	int cycle = std::max(0, _lastRenderCycle + 1 - gap);
	int end = std::min<int>(_state.Cycle, 1005) - gap;

	for(; cycle <= end; cycle++) {
		if(!(cycle & 0x03)) {
			if(!layer.Mosaic || _layerData[2].RenderX % (_state.BgMosaicSizeX + 1) == 0) {
				//Ignore decimal point value (bottom 8 bits), apply wraparound behavior
				//This produces the x,y coordinate (on the tilemap) that needs to be drawn
				_layerData[2].XPos = (_layerData[2].TransformX >> 8);
				_layerData[2].YPos = (_layerData[2].TransformY >> 8);
			}

			_memoryAccess[cycle+gap] |= GbaPpuMemAccess::Vram;

			if(_layerData[2].YPos < screenHeight && _layerData[2].XPos < screenWidth) {
				uint32_t addr = _layerData[2].YPos * screenWidth + _layerData[2].XPos;
				if constexpr(mode == 3 || mode == 5) {
					SetPixelData(_layerOutput[2][_layerData[2].RenderX], _vram16[(base >> 1) + addr] | GbaPpu::DirectColorFlag, layer.Priority, 2);
				} else if constexpr(mode == 4) {
					uint8_t color = _vram[base + addr];
					if(color != 0) {
						SetPixelData(_layerOutput[2][_layerData[2].RenderX], color * 2, layer.Priority, 2);
					}
				}
			}

			//Update x/y values for next dot
			_layerData[2].TransformX += cfg.Matrix[0];
			_layerData[2].TransformY += cfg.Matrix[2];
			_layerData[2].RenderX++;
			cycle += 2;
		}
	}

	if(_state.Cycle == 1006) {
		//Update x/y values for next scanline
		if(!layer.Mosaic) {
			cfg.LatchOriginX += cfg.Matrix[1];
			cfg.LatchOriginY += cfg.Matrix[3];
		} else if(_state.Scanline % (_state.BgMosaicSizeY + 1) == _state.BgMosaicSizeY) {
			cfg.LatchOriginX += cfg.Matrix[1] * (_state.BgMosaicSizeY + 1);
			cfg.LatchOriginY += cfg.Matrix[3] * (_state.BgMosaicSizeY + 1);
		}
	}
}

//TODOGBA behavior for shape == 3? current behaves like shape == 0
static constexpr uint8_t _sprSize[4][4][2] = {
	{ { 8, 8 }, { 16, 8 }, { 8, 16 }, { 8, 8 } },
	{ { 16, 16 }, { 32, 8 }, { 8, 32 }, { 16, 16 } },
	{ { 32, 32 }, { 32, 16 }, { 16, 32 }, { 32, 32 } },
	{ { 64, 64 }, { 64, 32 }, { 32, 64 }, { 64, 64 } }
};

void GbaPpu::ProcessSprites()
{
	if(!_emu->GetSettings()->GetGbaConfig().DisableSprites && _state.ObjLayerEnabled && (_state.Scanline <= 159 || _state.Scanline == 227)) {
		if(_state.BgMode >= 3) {
			RenderSprites<true>();
		} else {
			RenderSprites<false>();
		}
	}
}

void GbaPpu::InitSpriteEvaluation()
{
	_oamScanline = _state.Scanline == 227 ? 0 : (_state.Scanline + 1);
	if(_oamScanline == 0) {
		_oamMosaicY = 0;
	} else {
		if(_oamMosaicY == _state.ObjMosaicSizeY) {
			_oamMosaicY = 0;
		} else {
			_oamMosaicY = (_oamMosaicY + 1) & 0x0F;
		}
	}
	_loadOamAttr01 = true;
	_loadOamAttr2 = false;
	_isFirstOamTileLoad = false;
	_loadOamTileCounter = 0;
	_loadObjMatrix = 0;
	_evalOamIndex = -1;

	//Update window data
	if(_oamHasWindowModeSprite) {
		for(int x = 0; x < 240; x++) {
			if(_oamWindow[x] == GbaPpu::ObjWindow && _activeWindow[x] == GbaPpu::OutsideWindow) {
				_activeWindow[x] = _oamWindow[x];
			}
		}
	}
	
	_oamHasWindowModeSprite = false;

	std::swap(_oamWriteOutput, _oamReadOutput);
	std::fill(_oamWriteOutput, _oamWriteOutput + 240, GbaPixelData {});
	memset(_oamWindow, GbaPpu::BackdropLayerIndex, sizeof(_oamWindow));

	if(_state.ObjEnableTimer) {
		_state.ObjEnableTimer--;
	}
}

void GbaPpu::AddVisibleSprite(uint32_t sprData)
{
	GbaSpriteRendererData& spr = _objData[0];

	spr.Width = _sprSize[spr.Size][spr.Shape][0];
	spr.Mosaic = sprData & 0x1000;
	if(spr.Mosaic) {
		spr.YOffset = std::max(0, (int)spr.YOffset - (int)_oamMosaicY);
	}

	spr.Bpp8Mode = sprData & 0x2000;
	spr.SpriteX = (sprData >> 16) & 0x1FF;
	spr.HoriMirror = sprData & 0x10000000;
	spr.VertMirror = sprData & 0x20000000;
	spr.TransformParamSelect = (sprData >> 25) & 0x1F;
	spr.RenderX = 0;

	_loadOamAttr01 = false;
	_loadOamAttr2 = true;

	if(spr.SpriteX >= 240) {
		//Skip hidden pixels that wrap around to the right side of the coordinate space
		int gap = -((int)spr.SpriteX - 512);
		spr.RenderX += spr.TransformEnabled ? gap : (gap & ~0x01);
		if(spr.RenderX >= (spr.Width << (uint8_t)spr.DoubleSize)) {
			//Skip this sprite entirely (it's completely hidden)
			_loadOamAttr01 = true;
			_loadOamAttr2 = false;
		}
	}
}

void GbaPpu::LoadSpriteAttr2()
{
	GbaSpriteRendererData& spr = _objData[0];
	uint32_t sprData = _oam[spr.Addr + 1];
	spr.TileIndex = sprData & 0x3FF;
	spr.Priority = (sprData >> 10) & 0x03;
	spr.PaletteIndex = spr.Bpp8Mode ? 0 : ((sprData >> 12) & 0x0F);

	_loadOamAttr2 = false;
	if(spr.TransformEnabled) {
		_loadObjMatrix = 4;
	} else {
		_loadOamAttr01 = true;
		_isFirstOamTileLoad = true;
		_loadOamTileCounter = (spr.Width - spr.RenderX) / 2;
		_objData[1] = spr;
	}
}

void GbaPpu::LoadSpriteTransformMatrix()
{
	GbaSpriteRendererData& spr = _objData[0];
	_loadObjMatrix--;

	uint8_t i = 3 - _loadObjMatrix;
	uint16_t transformAddr = (spr.TransformParamSelect << 3) + 1 + (i * 2);
	spr.MatrixData[i] = _oam[transformAddr] >> 16;

	if(_loadObjMatrix == 0) {
		spr.CenterX = spr.Width / 2;
		spr.CenterY = spr.Height / 2;

		int32_t originX = -(spr.CenterX << (uint8_t)spr.DoubleSize);
		int32_t originY = -(spr.CenterY << (uint8_t)spr.DoubleSize);

		spr.XValue = originX * spr.MatrixData[0] + (originY + spr.YOffset) * spr.MatrixData[1];
		spr.YValue = originX * spr.MatrixData[2] + (originY + spr.YOffset) * spr.MatrixData[3];
		spr.XValue += spr.MatrixData[0] * spr.RenderX;
		spr.YValue += spr.MatrixData[2] * spr.RenderX;
		_loadOamTileCounter = (spr.Width << (uint8_t)spr.DoubleSize) - spr.RenderX;
		_loadOamAttr01 = true;
		_isFirstOamTileLoad = true;
		_objData[1] = spr;
	}
}

template<bool blockFirst16k>
void GbaPpu::RenderSprites()
{
	if(_oamScanline >= 160) {
		return;
	}

	uint16_t ppuCycle = _state.Cycle >= 308 * 4 ? (308 * 4) - 1 : _state.Cycle;
	if(_state.AllowHblankOamAccess && ppuCycle > 996) {
		//Evaluation stops just before hblank when this is enabled
		ppuCycle = 996;
	}
	int cycle = _oamLastCycle + 1;

	if(cycle < 40 && (_state.AllowHblankOamAccess || _evalOamIndex >= 128 || _state.ObjEnableTimer > 0)) {
		//Evaluation in hblank is disabled, jump to cycle 40 (eval start)
		cycle = 40;
	} else if(cycle >= 40 && _state.ObjEnableTimer > 0) {
		return;
	}

	if(cycle & 0x01) {
		cycle++;
	}
	GbaSpriteRendererData& spr = _objData[0];
	for(; cycle <= ppuCycle; cycle+=2) {
		if(cycle == 40) {
			//start oam evaluation/fetching
			InitSpriteEvaluation();
			if(_oamScanline == 160 || _state.ObjEnableTimer > 0) {
				//Scanline 159 stops processing sprites after cycle 39
				break;
			}
		}

		bool allowLoadAttr01 = _loadOamTileCounter <= 1 || _isFirstOamTileLoad;

		if(_loadOamTileCounter) {
			if(_isFirstOamTileLoad && _objData[1].TransformEnabled) {
				_isFirstOamTileLoad = false;
			} else {
				//load+draw pixels
				_isFirstOamTileLoad = false;
				_loadOamTileCounter--;
				_memoryAccess[cycle] |= GbaPpuMemAccess::VramObj;

				//Last cycle (39) doesn't actually draw, but cycle 38 does read from VRAM anyway
				if(cycle != 38) {
					if(_objData[1].TransformEnabled) {
						RenderSprite<true, blockFirst16k>(_objData[1]);
					} else {
						RenderSprite<false, blockFirst16k>(_objData[1]);
					}
				}

				if(_evalOamIndex >= 128 && _loadOamTileCounter == 0) {
					//Finished loading last sprite
					if(cycle < 40) {
						//Jump to the start of the next evaluation cycle (cycle 40)
						cycle = 38;
						continue;
					} else {
						break;
					}
				}
			}
		}

		if(_loadOamAttr01 && allowLoadAttr01) {
			//Load first 4 bytes of OAM attributes, evaluate if sprite should appear on this scanline
			_evalOamIndex++;
			if(_evalOamIndex >= 128) {
				if(_loadOamTileCounter == 0) {
					//Finished loading last sprite
					if(cycle < 40) {
						//Jump to the start of the next evaluation cycle (cycle 40)
						cycle = 38;
						continue;
					} else {
						break;
					}
				}
				_loadOamAttr01 = false;
				continue;
			}
			
			_memoryAccess[cycle] |= GbaPpuMemAccess::Oam;

			uint16_t addr = _evalOamIndex << 1;
			uint32_t sprData = _oam[addr];
			spr.TransformEnabled = sprData & 0x0100;
			spr.DoubleSize = sprData & 0x0200;
			spr.HideSprite = !spr.TransformEnabled && spr.DoubleSize;
			spr.Mode = (GbaPpuObjMode)((sprData >> 10) & 0x03);
			if(!spr.HideSprite && spr.Mode != GbaPpuObjMode::Invalid) {
				spr.SpriteY = sprData & 0xFF;
				spr.Shape = (sprData >> 14) & 0x03;
				spr.Size = (sprData >> 30) & 0x03;
				spr.Height = _sprSize[spr.Size][spr.Shape][1];
				spr.YOffset = _oamScanline - spr.SpriteY;

				uint8_t sprHeight = (spr.Height << (uint8_t)spr.DoubleSize);
				if(spr.YOffset < sprHeight && _oamScanline < (uint8_t)(spr.SpriteY + sprHeight)) {
					//sprite is visible on this scanline
					spr.Addr = addr;
					AddVisibleSprite(sprData);
				}
			}
		} else if(_loadOamAttr2 && _loadOamTileCounter == 0) {
			//Load last 2 bytes of OAM attributes
			_memoryAccess[cycle] |= GbaPpuMemAccess::Oam;
			LoadSpriteAttr2();
		} else if(_loadObjMatrix) {
			//Load all 4 16-bit transform parameters (when transform flag is enabled)
			_memoryAccess[cycle] |= GbaPpuMemAccess::Oam;
			LoadSpriteTransformMatrix();
		}
	}

	_oamLastCycle = _state.Cycle;
}

template<bool blockFirst16k>
uint8_t GbaPpu::ReadSpriteVram(uint32_t addr)
{
	if constexpr(blockFirst16k) {
		//When in BG modes 3-5, the first 16kb of sprite tile vram ($10000-$13FFF) can't be accessed
		//by sprites (because the BG layer uses it), reading it returns 0
		return addr < 0x4000 ? 0 : _vram[0x10000 | (addr & 0x7FFF)];
	} else {
		return _vram[0x10000 | (addr & 0x7FFF)];
	}
}

template<bool transformEnabled, bool blockFirst16k>
void GbaPpu::RenderSprite(GbaSpriteRendererData& spr)
{
	for(int i = 0; i < (transformEnabled ? 1 : 2); i++) {
		if constexpr(transformEnabled) {
			spr.XPos = (spr.XValue >> 8) + spr.CenterX;
			spr.YPos = (spr.YValue >> 8) + spr.CenterY;
			spr.XValue += spr.MatrixData[0];
			spr.YValue += spr.MatrixData[2];
		} else {
			spr.XPos = spr.HoriMirror ? (spr.Width - spr.RenderX - 1) : spr.RenderX;
			spr.YPos = spr.VertMirror ? (spr.Height - spr.YOffset - 1) : spr.YOffset;
		}

		uint16_t drawPos = (spr.SpriteX + spr.RenderX) & 0x1FF;
		spr.RenderX++;

		if(drawPos >= 240 || spr.XPos >= spr.Width || spr.YPos >= spr.Height) {
			continue;
		}

		bool isHigherPriority = _oamWriteOutput[drawPos].Priority > spr.Priority;
		if(spr.Mode == GbaPpuObjMode::Window || isHigherPriority) {
			uint8_t tileRow = spr.YPos / 8;
			uint8_t tileColumn = spr.XPos / 8;
			uint16_t index;
			if(_state.ObjVramMappingOneDimension) {
				uint8_t tilesPerRow = (spr.Width / 8) * (spr.Bpp8Mode ? 2 : 1);
				index = _objData[0].TileIndex + tileRow * tilesPerRow + (spr.Bpp8Mode ? tileColumn * 2 : tileColumn);
			} else {
				index = (_objData[0].TileIndex & ~0x1F) + tileRow * 0x20 + (((_objData[0].TileIndex & 0x1F) + (spr.Bpp8Mode ? tileColumn * 2 : tileColumn)) & 0x1F);
			}
			uint8_t color = 0;

			if(spr.Bpp8Mode) {
				color = ReadSpriteVram<blockFirst16k>(index * 32 + (spr.YPos & 0x07) * 8 + (spr.XPos & 0x07));
			} else {
				color = ReadSpriteVram<blockFirst16k>(index * 32 + (spr.YPos & 0x07) * 4 + ((spr.XPos & 0x07) >> 1));
				if(spr.XPos & 0x01) {
					color >>= 4;
				} else {
					color &= 0x0F;
				}
			}

			if(color != 0) {
				if(spr.Mode == GbaPpuObjMode::Window) {
					_oamHasWindowModeSprite = true;
					_oamWindow[drawPos] = GbaPpu::ObjWindow;
				} else {
					uint16_t colorIndex = 0x200 + (spr.PaletteIndex * 32) + color * 2;
					if(spr.Mode == GbaPpuObjMode::Blending) {
						colorIndex |= GbaPpu::SpriteBlendFlag;
					}
					SetPixelData(_oamWriteOutput[drawPos], colorIndex, spr.Priority, GbaPpu::SpriteLayerIndex);
				}
			} else if(isHigherPriority && _oamWriteOutput[drawPos].Priority != 0xFF) {
				//If a sprite pixel already exists and another sprite with higher priority with a
				//transparent pixel is displayed here, the priority is updated to match the transparent
				//pixel's priority (Golden Sun requires this)
				_oamWriteOutput[drawPos].Priority = spr.Priority;
			}

			if(spr.Mosaic) {
				_oamWriteOutput[drawPos].Color |= GbaPpu::SpriteMosaicFlag;
			}
		}
	}
}

void GbaPpu::SetWindowActiveLayers(int window, uint8_t cfg)
{
	for(int i = 0; i < 6; i++) {
		_state.WindowActiveLayers[window][i] = cfg & (1 << i);
	}
}

template<int bit>
void GbaPpu::SetTransformOrigin(uint8_t i, uint8_t value, bool setY)
{
	if(setY) {
		BitUtilities::SetBits<bit>(_state.Transform[i].OriginY, value);
		_state.Transform[i].PendingUpdateY = true;
	} else {
		BitUtilities::SetBits<bit>(_state.Transform[i].OriginX, value);
		_state.Transform[i].PendingUpdateX = true;
	}
}

void GbaPpu::SetLayerEnabled(int layer, bool enabled)
{
	if(_state.BgLayers[layer].Enabled || !enabled) {
		_state.BgLayers[layer].Enabled = enabled;
		_state.BgLayers[layer].EnableTimer = 0;
	} else if(_state.BgLayers[layer].EnableTimer == 0) {
		_state.BgLayers[layer].EnableTimer = 3;
	}
}

void GbaPpu::WriteRegister(uint32_t addr, uint8_t value)
{
	if(_lastRenderCycle != _state.Cycle && (_state.Scanline < 160 || _state.Scanline == 227)) {
		if(_state.Cycle < 1006 || addr <= 0x01 || addr == 0x4D) {
			//Only run renderer during active rendering (< 1006), or if the write could affect sprites
			RenderScanline(true);
		}
	}

	switch(addr) {
		case 0x00:
			_state.Control = value & ~0x08;
			_state.BgMode = value & 0x07;
			_state.DisplayFrameSelect = value & 0x10;
			_state.AllowHblankOamAccess = value & 0x20;
			_state.ObjVramMappingOneDimension = value & 0x40;
			_state.ForcedBlank = value & 0x80;
			break;

		case 0x01: {
			_state.Control2 = value;
			SetLayerEnabled(0, value & 0x01);
			SetLayerEnabled(1, value & 0x02);
			SetLayerEnabled(2, value & 0x04);
			SetLayerEnabled(3, value & 0x08);
			bool objEnabled = value & 0x10;
			if(_state.ObjLayerEnabled && !objEnabled) {
				//Clear sprite row buffers when OBJ layer is disabled
				//TODOGBA what does hardware do if sprites are re-enabled on the same or next scanline?
				std::fill(_oamOutputBuffers[0], _oamOutputBuffers[0] + 240, GbaPixelData {});
				std::fill(_oamOutputBuffers[1], _oamOutputBuffers[1] + 240, GbaPixelData {});
			} else if(!_state.ObjLayerEnabled && objEnabled) {
				_state.ObjEnableTimer = 3;
			}
			_state.ObjLayerEnabled = objEnabled;
			_state.Window0Enabled = value & 0x20;
			_state.Window1Enabled = value & 0x40;
			_state.ObjWindowEnabled = value & 0x80;
			break;
		}

		case 0x02: _state.GreenSwapEnabled = value & 0x01; break;
		case 0x03: break;

		case 0x04:
			_state.DispStat = value & 0x38;
			_state.VblankIrqEnabled = value & 0x08;
			_state.HblankIrqEnabled = value & 0x10;
			_state.ScanlineIrqEnabled = value & 0x20;
			break;
		
		case 0x05:
			if(_state.Lyc != value) {
				_state.Lyc = value;

				//If LYC is changed to the current scanline and LYC IRQs are enabled, trigger it
				//(writing when LYC is already set to the current scanline doesn't trigger an irq - doing this breaks audio in Minish Cap)
				if(_state.ScanlineIrqEnabled && _state.Scanline == _state.Lyc) {
					_console->GetMemoryManager()->SetIrqSource(GbaIrqSource::LcdScanlineMatch);
				}
			}
			break;

		case 0x08: case 0x0A: case 0x0C: case 0x0E: {
			GbaBgConfig& cfg = _state.BgLayers[(addr & 0x06) >> 1];
			BitUtilities::SetBits<0>(cfg.Control, value);
			cfg.Priority = value & 0x03;
			cfg.TilesetAddr = ((value >> 2) & 0x03) * 0x4000;
			//Bits 4-5 unused
			cfg.Mosaic = value & 0x40;
			cfg.Bpp8Mode = value & 0x80;
			break;
		}

		case 0x09: case 0x0B: case 0x0D: case 0x0F: {
			uint8_t layer = (addr & 0x06) >> 1;
			GbaBgConfig& cfg = _state.BgLayers[layer];
			if(layer < 2) {
				//unused bit
				value &= ~0x20;
			}

			BitUtilities::SetBits<8>(cfg.Control, value);
			cfg.TilemapAddr = (value & 0x1F) * 0x800;
			cfg.WrapAround = value & 0x20;
			cfg.DoubleWidth = value & 0x40;
			cfg.DoubleHeight = value & 0x80;
			cfg.ScreenSize = (value >> 6) & 0x03;
			break;
		}

		case 0x10: case 0x14: case 0x18: case 0x1C:
			BitUtilities::SetBits<0>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollX, value);
			break;

		case 0x11: case 0x15: case 0x19: case 0x1D:
			BitUtilities::SetBits<8>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollX, value);
			break;

		case 0x12: case 0x16: case 0x1A: case 0x1E:
			BitUtilities::SetBits<0>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollY, value);
			break;

		case 0x13: case 0x17: case 0x1B: case 0x1F:
			BitUtilities::SetBits<8>(_state.BgLayers[(addr & 0x0C) >> 2].ScrollY, value);
			break;

		case 0x20: case 0x22: case 0x24: case 0x26:
		case 0x30: case 0x32: case 0x34: case 0x36:
			BitUtilities::SetBits<0>(_state.Transform[(addr & 0x10) >> 4].Matrix[(addr & 0x06) >> 1], value);
			break;

		case 0x21: case 0x23: case 0x25: case 0x27:
		case 0x31: case 0x33: case 0x35: case 0x37:
			BitUtilities::SetBits<8>(_state.Transform[(addr & 0x10) >> 4].Matrix[(addr & 0x06) >> 1], value);
			break;
		
		case 0x28: case 0x38: SetTransformOrigin<0>((addr & 0x10) >> 4, value, false); break;
		case 0x29: case 0x39: SetTransformOrigin<8>((addr & 0x10) >> 4, value, false);  break;
		case 0x2A: case 0x3A: SetTransformOrigin<16>((addr & 0x10) >> 4, value, false);  break;
		case 0x2B: case 0x3B: SetTransformOrigin<24>((addr & 0x10) >> 4, value & 0x0F, false); break;

		case 0x2C: case 0x3C: SetTransformOrigin<0>((addr & 0x10) >> 4, value, true);  break;
		case 0x2D: case 0x3D: SetTransformOrigin<8>((addr & 0x10) >> 4, value, true);  break;
		case 0x2E: case 0x3E: SetTransformOrigin<16>((addr & 0x10) >> 4, value, true);  break;
		case 0x2F: case 0x3F: SetTransformOrigin<24>((addr & 0x10) >> 4, value & 0x0F, true);  break;

		case 0x40: SetWindowX(_state.Window[0].RightX, value); break;
		case 0x41: SetWindowX(_state.Window[0].LeftX, value); break;
		case 0x42: SetWindowX(_state.Window[1].RightX, value); break;
		case 0x43: SetWindowX(_state.Window[1].LeftX, value); break;
		
		case 0x44: _state.Window[0].BottomY = value; break;
		case 0x45: _state.Window[0].TopY = value; break;
		case 0x46: _state.Window[1].BottomY = value; break;
		case 0x47: _state.Window[1].TopY = value; break;

		case 0x48: _state.Window0Control = value & 0x3F; SetWindowActiveLayers(0, value & 0x3F); break;
		case 0x49: _state.Window1Control = value & 0x3F; SetWindowActiveLayers(1, value & 0x3F); break;
		case 0x4A: _state.OutWindowControl = value & 0x3F; SetWindowActiveLayers(3, value & 0x3F);  break;
		case 0x4B: _state.ObjWindowControl = value & 0x3F; SetWindowActiveLayers(2, value & 0x3F);  break;

		case 0x4C:
			_state.BgMosaicSizeX = value & 0x0F;
			_state.BgMosaicSizeY = (value >> 4) & 0x0F;
			break;

		case 0x4D:
			_state.ObjMosaicSizeX = value & 0x0F;
			_state.ObjMosaicSizeY = (value >> 4) & 0x0F;
			break;

		case 0x50:
			_state.BlendMainControl = value;
			_state.BlendMain[0] = value & 0x01; //BG0
			_state.BlendMain[1] = value & 0x02; //BG1
			_state.BlendMain[2] = value & 0x04; //BG2
			_state.BlendMain[3] = value & 0x08; //BG3
			_state.BlendMain[GbaPpu::SpriteLayerIndex] = value & 0x10;
			_state.BlendMain[GbaPpu::BackdropLayerIndex] = value & 0x20;
			_state.BlendEffect = (GbaPpuBlendEffect)((value >> 6) & 0x03);
			break;

		case 0x51:
			_state.BlendSubControl = value & 0x3F;
			_state.BlendSub[0] = value & 0x01;
			_state.BlendSub[1] = value & 0x02;
			_state.BlendSub[2] = value & 0x04;
			_state.BlendSub[3] = value & 0x08;
			_state.BlendSub[GbaPpu::SpriteLayerIndex] = value & 0x10;
			_state.BlendSub[GbaPpu::BackdropLayerIndex] = value & 0x20;
			break;

		case 0x52: _state.BlendMainCoefficient = value & 0x1F; break;
		case 0x53: _state.BlendSubCoefficient = value & 0x1F; break;
		case 0x54: _state.Brightness = value & 0x1F; break;
		
		default:
			LogDebug("Write unimplemented LCD register: " + HexUtilities::ToHex32(addr) + " = " + HexUtilities::ToHex(value));
			break;
	}
}

uint8_t GbaPpu::ReadRegister(uint32_t addr)
{
	switch(addr) {
		case 0x00: return _state.Control;
		case 0x01: return _state.Control2;

		case 0x02: return (uint8_t)_state.GreenSwapEnabled;
		case 0x03: return 0;

		case 0x04:
			return (
				(_state.Scanline >= 160 && _state.Scanline != 227 ? 0x01 : 0) |
				((_state.Cycle > 1007 || _state.Cycle == 0) ? 0x02 : 0) |
				(_state.Scanline == _state.Lyc ? 0x04 : 0) |
				_state.DispStat
			);

		case 0x05: return _state.Lyc;

		case 0x06: return _state.Scanline;
		case 0x07: return 0;

		case 0x08: case 0x0A: case 0x0C: case 0x0E:
			return (uint8_t)_state.BgLayers[(addr & 0x06) >> 1].Control;

		case 0x09: case 0x0B: case 0x0D: case 0x0F:
			return (uint8_t)(_state.BgLayers[(addr & 0x06) >> 1].Control >> 8);

		case 0x48: return _state.Window0Control;
		case 0x49: return _state.Window1Control;
		case 0x4A: return _state.OutWindowControl;
		case 0x4B: return _state.ObjWindowControl;

		case 0x50: return _state.BlendMainControl;
		case 0x51: return _state.BlendSubControl;
		case 0x52: return _state.BlendMainCoefficient;
		case 0x53: return _state.BlendSubCoefficient;

		default:
			LogDebug("Read unimplemented LCD register: " + HexUtilities::ToHex32(addr));
			break;
	}

	return _memoryManager->GetOpenBus(addr);
}

void GbaPpu::DebugProcessMemoryAccessView()
{
	//Store memory access buffer in ppu tools to display in tilemap viewer
	GbaPpuTools* ppuTools = ((GbaPpuTools*)_emu->InternalGetDebugger()->GetPpuTools(CpuType::Gba));
	ppuTools->SetMemoryAccessData(_state.Scanline, _memoryAccess);
}

void GbaPpu::Serialize(Serializer& s)
{
	SV(_state.FrameCount);
	SV(_state.Cycle);
	SV(_state.Scanline);

	SV(_state.Control);
	SV(_state.BgMode);
	SV(_state.DisplayFrameSelect);
	SV(_state.AllowHblankOamAccess);
	SV(_state.ObjVramMappingOneDimension);
	SV(_state.ForcedBlank);
	SV(_state.GreenSwapEnabled);

	SV(_state.Control2);
	SV(_state.ObjEnableTimer);
	SV(_state.ObjLayerEnabled);
	SV(_state.Window0Enabled);
	SV(_state.Window1Enabled);
	SV(_state.ObjWindowEnabled);

	SV(_state.DispStat);
	SV(_state.VblankIrqEnabled);
	SV(_state.HblankIrqEnabled);
	SV(_state.ScanlineIrqEnabled);
	SV(_state.Lyc);

	SV(_state.BlendMainControl);
	SVArray(_state.BlendMain, 6);
	SV(_state.BlendSubControl);
	SVArray(_state.BlendSub, 6);
	SV(_state.BlendEffect);
	SV(_state.BlendMainCoefficient);
	SV(_state.BlendSubCoefficient);
	SV(_state.Brightness);

	for(int i = 0; i < 4; i++) {
		SVI(_state.BgLayers[i].Control);
		SVI(_state.BgLayers[i].TilemapAddr);
		SVI(_state.BgLayers[i].TilesetAddr);
		SVI(_state.BgLayers[i].ScrollX);
		SVI(_state.BgLayers[i].ScrollXLatch);
		SVI(_state.BgLayers[i].ScrollY);
		SVI(_state.BgLayers[i].ScreenSize);
		SVI(_state.BgLayers[i].DoubleWidth);
		SVI(_state.BgLayers[i].DoubleHeight);
		SVI(_state.BgLayers[i].Priority);
		SVI(_state.BgLayers[i].Mosaic);
		SVI(_state.BgLayers[i].WrapAround);
		SVI(_state.BgLayers[i].Bpp8Mode);
		SVI(_state.BgLayers[i].Enabled);
		SVI(_state.BgLayers[i].EnableTimer);
	}
	
	for(int i = 0; i < 2; i++) {
		SVI(_state.Transform[i].OriginX);
		SVI(_state.Transform[i].OriginY);
		SVI(_state.Transform[i].LatchOriginX);
		SVI(_state.Transform[i].LatchOriginY);
		SVI(_state.Transform[i].PendingUpdateX);
		SVI(_state.Transform[i].PendingUpdateY);
		SVI(_state.Transform[i].Matrix[0]);
		SVI(_state.Transform[i].Matrix[1]);
		SVI(_state.Transform[i].Matrix[2]);
		SVI(_state.Transform[i].Matrix[3]);

		SVI(_state.Window[i].LeftX);
		SVI(_state.Window[i].RightX);
		SVI(_state.Window[i].TopY);
		SVI(_state.Window[i].BottomY);
	}

	SV(_state.BgMosaicSizeX);
	SV(_state.BgMosaicSizeY);
	SV(_state.ObjMosaicSizeX);
	SV(_state.ObjMosaicSizeY);

	SV(_state.Window0Control);
	SV(_state.Window1Control);
	SV(_state.ObjWindowControl);
	SV(_state.OutWindowControl);

	if(s.GetFormat() != SerializeFormat::Map) {
		for(int i = 0; i < 5; i++) {
			SVI(_state.WindowActiveLayers[i][0]);
			SVI(_state.WindowActiveLayers[i][1]);
			SVI(_state.WindowActiveLayers[i][2]);
			SVI(_state.WindowActiveLayers[i][3]);
			SVI(_state.WindowActiveLayers[i][4]);
			SVI(_state.WindowActiveLayers[i][5]);
		}

		//Convert data to plain arrays to improve serialization performance
		GbaPixelData* src[6] = { _oamOutputBuffers[0], _oamOutputBuffers[1], _layerOutput[0], _layerOutput[1], _layerOutput[2], _layerOutput[3] };
		string names[6] = { "oamOutputBuffers[0]", "oamOutputBuffers[1]", "layerOutput[0]", "layerOutput[1]", "layerOutput[2]", "layerOutput[3]" };
		for(int i = 0; i < 6; i++) {
			GbaPixelData* data = src[i];
			uint16_t color[GbaConstants::ScreenWidth];
			uint8_t layer[GbaConstants::ScreenWidth];
			uint8_t priority[GbaConstants::ScreenWidth];
			if(s.IsSaving()) {
				for(int j = 0; j < GbaConstants::ScreenWidth; j++) {
					color[j] = data[j].Color;
					layer[j] = data[j].Layer;
					priority[j] = data[j].Priority;
				}
			}
			s.StreamArray(color, GbaConstants::ScreenWidth, (names[i] + "_color").c_str());
			s.StreamArray(layer, GbaConstants::ScreenWidth, (names[i] + "_layer").c_str());
			s.StreamArray(priority, GbaConstants::ScreenWidth, (names[i] + "_priority").c_str());
			if(!s.IsSaving()) {
				for(int j = 0; j < GbaConstants::ScreenWidth; j++) {
					data[j].Color = color[j];
					data[j].Layer = layer[j];
					data[j].Priority = priority[j];
				}
			}
		}

		SVArray(_oamWindow, GbaConstants::ScreenWidth);
		SVArray(_activeWindow, GbaConstants::ScreenWidth);
		SVArray(_memoryAccess, 308 * 4);

		SV(_triggerSpecialDma);

		SV(_lastWindowCycle);
		SV(_window0ActiveY);
		SV(_window1ActiveY);
		SV(_window0ActiveX);
		SV(_window1ActiveX);

		SV(_lastRenderCycle);
		SV(_evalOamIndex);
		SV(_loadOamTileCounter);
		SV(_oamHasWindowModeSprite);
		SV(_loadOamAttr01);
		SV(_loadOamAttr2);
		SV(_isFirstOamTileLoad);
		SV(_loadObjMatrix);
		SV(_oamScanline);
		SV(_oamMosaicY);

		for(int i = 0; i < 4; i++) {
			SVI(_layerData[i].TilemapData);
			SVI(_layerData[i].TileData);
			SVI(_layerData[i].FetchAddr);
			SVI(_layerData[i].TileIndex);
			SVI(_layerData[i].RenderX);
			SVI(_layerData[i].TransformX);
			SVI(_layerData[i].TransformY);
			SVI(_layerData[i].XPos);
			SVI(_layerData[i].YPos);
			SVI(_layerData[i].PaletteIndex);
			SVI(_layerData[i].HoriMirror);
			SVI(_layerData[i].VertMirror);
			SVI(_layerData[i].TileRow);
			SVI(_layerData[i].TileColumn);
			SVI(_layerData[i].MosaicColor);
		}

		for(int i = 0; i < 2; i++) {
			SVI(_objData[i].MatrixData[0]);
			SVI(_objData[i].MatrixData[1]);
			SVI(_objData[i].MatrixData[2]);
			SVI(_objData[i].MatrixData[3]);
			SVI(_objData[i].XValue);
			SVI(_objData[i].YValue);
			SVI(_objData[i].XPos);
			SVI(_objData[i].YPos);
			SVI(_objData[i].CenterX);
			SVI(_objData[i].CenterY);

			SVI(_objData[i].SpriteX);
			SVI(_objData[i].TileIndex);
			SVI(_objData[i].Addr);
			SVI(_objData[i].RenderX);
			SVI(_objData[i].SpriteY);
			SVI(_objData[i].YOffset);
			SVI(_objData[i].TransformEnabled);
			SVI(_objData[i].DoubleSize);
			SVI(_objData[i].HideSprite);

			SVI(_objData[i].Mode);
			SVI(_objData[i].Mosaic);
			SVI(_objData[i].Bpp8Mode);
			SVI(_objData[i].Shape);
			SVI(_objData[i].HoriMirror);
			SVI(_objData[i].VertMirror);
			SVI(_objData[i].TransformParamSelect);
			SVI(_objData[i].Size);

			SVI(_objData[i].Priority);
			SVI(_objData[i].PaletteIndex);

			SVI(_objData[i].Width);
			SVI(_objData[i].Height);
		}
	}

	if(!s.IsSaving()) {
		UpdateWindowChangePoints();
	}
}
