/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com.                */

#include "graphics_threaded.h"

#include <base/dbg.h>
#include <base/detect.h>
#include <base/io.h>
#include <base/log.h>
#include <base/mem.h>
#include <base/str.h>
#include <base/time.h>

#include <engine/engine.h>
#include <engine/gfx/image_loader.h>
#include <engine/gfx/image_manipulation.h>
#include <engine/graphics.h>
#include <engine/shared/config.h>
#include <engine/shared/jobs.h>
#include <engine/storage.h>

#include <generated/data_types.h>

#include <game/localization.h>

#if defined(CONF_VIDEORECORDER)
#include <engine/shared/video.h>
#endif

#include <algorithm>

class CSemaphore;

static CVideoMode g_aFakeModes[] = {
	{8192, 4320, 8192, 4320, 0}, {7680, 4320, 7680, 4320, 0}, {5120, 2880, 5120, 2880, 0},
	{4096, 2160, 4096, 2160, 0}, {3840, 2160, 3840, 2160, 0}, {2560, 1440, 2560, 1440, 0},
	{2048, 1536, 2048, 1536, 0}, {1920, 2400, 1920, 2400, 0}, {1920, 1440, 1920, 1440, 0},
	{1920, 1200, 1920, 1200, 0}, {1920, 1080, 1920, 1080, 0}, {1856, 1392, 1856, 1392, 0},
	{1800, 1440, 1800, 1440, 0}, {1792, 1344, 1792, 1344, 0}, {1680, 1050, 1680, 1050, 0},
	{1600, 1200, 1600, 1200, 0}, {1600, 1000, 1600, 1000, 0}, {1440, 1050, 1440, 1050, 0},
	{1440, 900, 1440, 900, 0}, {1400, 1050, 1400, 1050, 0}, {1368, 768, 1368, 768, 0},
	{1280, 1024, 1280, 1024, 0}, {1280, 960, 1280, 960, 0}, {1280, 800, 1280, 800, 0},
	{1280, 768, 1280, 768, 0}, {1152, 864, 1152, 864, 0}, {1024, 768, 1024, 768, 0},
	{1024, 600, 1024, 600, 0}, {800, 600, 800, 600, 0}, {768, 576, 768, 576, 0},
	{720, 400, 720, 400, 0}, {640, 480, 640, 480, 0}, {400, 300, 400, 300, 0},
	{320, 240, 320, 240, 0}};

void CGraphics_Threaded::FlushVertices(bool KeepVertices)
{
	CCommandBuffer::SCommand_Render Cmd;
	EPrimitiveType PrimType;
	size_t PrimCount, NumVerts;
	FlushVerticesImpl(KeepVertices, PrimType, PrimCount, NumVerts, Cmd, sizeof(CCommandBuffer::SVertex));

	if(Cmd.m_pVertices != nullptr)
	{
		mem_copy(Cmd.m_pVertices, m_aVertices, sizeof(CCommandBuffer::SVertex) * NumVerts);
	}
}

void CGraphics_Threaded::FlushVerticesTex3D()
{
	CCommandBuffer::SCommand_RenderTex3D Cmd;
	EPrimitiveType PrimType;
	size_t PrimCount, NumVerts;
	FlushVerticesImpl(false, PrimType, PrimCount, NumVerts, Cmd, sizeof(CCommandBuffer::SVertexTex3DStream));

	if(Cmd.m_pVertices != nullptr)
	{
		mem_copy(Cmd.m_pVertices, m_aVerticesTex3D, sizeof(CCommandBuffer::SVertexTex3DStream) * NumVerts);
	}
}

void CGraphics_Threaded::AddVertices(int Count)
{
	m_NumVertices += Count;
	if((m_NumVertices + Count) >= CCommandBuffer::MAX_VERTICES)
		FlushVertices();
}

void CGraphics_Threaded::AddVertices(int Count, CCommandBuffer::SVertex *pVertices)
{
	AddVertices(Count);
}

void CGraphics_Threaded::AddVertices(int Count, CCommandBuffer::SVertexTex3DStream *pVertices)
{
	m_NumVertices += Count;
	if((m_NumVertices + Count) >= CCommandBuffer::MAX_VERTICES)
		FlushVerticesTex3D();
}

CGraphics_Threaded::CGraphics_Threaded()
{
	m_State.m_ScreenTL.x = 0;
	m_State.m_ScreenTL.y = 0;
	m_State.m_ScreenBR.x = 0;
	m_State.m_ScreenBR.y = 0;
	m_State.m_ClipEnable = false;
	m_State.m_ClipX = 0;
	m_State.m_ClipY = 0;
	m_State.m_ClipW = 0;
	m_State.m_ClipH = 0;
	m_State.m_Texture = -1;
	m_State.m_BlendMode = EBlendMode::ALPHA;
	m_State.m_WrapMode = EWrapMode::REPEAT;

	m_CurrentCommandBuffer = 0;
	m_pCommandBuffer = nullptr;
	m_apCommandBuffers[0] = nullptr;
	m_apCommandBuffers[1] = nullptr;

	m_NumVertices = 0;

	m_ScreenWidth = -1;
	m_ScreenHeight = -1;
	m_ScreenRefreshRate = -1;

	m_Rotation = 0;
	m_Drawing = EDrawing::NONE;

	m_TextureMemoryUsage = 0;

	m_RenderEnable = true;
	m_DoScreenshot = false;
}

void CGraphics_Threaded::ClipEnable(int x, int y, int w, int h)
{
	if(x < 0)
		w += x;
	if(y < 0)
		h += y;

	x = std::clamp(x, 0, ScreenWidth());
	y = std::clamp(y, 0, ScreenHeight());
	w = std::clamp(w, 0, ScreenWidth() - x);
	h = std::clamp(h, 0, ScreenHeight() - y);

	m_State.m_ClipEnable = true;
	m_State.m_ClipX = x;
	m_State.m_ClipY = ScreenHeight() - (y + h);
	m_State.m_ClipW = w;
	m_State.m_ClipH = h;
}

void CGraphics_Threaded::ClipDisable()
{
	m_State.m_ClipEnable = false;
}

void CGraphics_Threaded::BlendNone()
{
	m_State.m_BlendMode = EBlendMode::NONE;
}

void CGraphics_Threaded::BlendNormal()
{
	m_State.m_BlendMode = EBlendMode::ALPHA;
}

void CGraphics_Threaded::BlendAdditive()
{
	m_State.m_BlendMode = EBlendMode::ADDITIVE;
}

void CGraphics_Threaded::WrapNormal()
{
	m_State.m_WrapMode = EWrapMode::REPEAT;
}

void CGraphics_Threaded::WrapClamp()
{
	m_State.m_WrapMode = EWrapMode::CLAMP;
}

uint64_t CGraphics_Threaded::TextureMemoryUsage() const
{
	return m_pBackend->TextureMemoryUsage();
}

uint64_t CGraphics_Threaded::BufferMemoryUsage() const
{
	return m_pBackend->BufferMemoryUsage();
}

uint64_t CGraphics_Threaded::StreamedMemoryUsage() const
{
	return m_pBackend->StreamedMemoryUsage();
}

uint64_t CGraphics_Threaded::StagingMemoryUsage() const
{
	return m_pBackend->StagingMemoryUsage();
}

const TTwGraphicsGpuList &CGraphics_Threaded::GetGpus() const
{
	return m_pBackend->GetGpus();
}

void CGraphics_Threaded::MapScreen(const CScreenRect &ScreenRect)
{
	m_State.m_ScreenTL = ScreenRect.m_TopLeft;
	m_State.m_ScreenBR = ScreenRect.m_BottomRight;
}

CScreenRect CGraphics_Threaded::GetScreen() const
{
	return CScreenRect(m_State.m_ScreenTL, m_State.m_ScreenBR);
}

void CGraphics_Threaded::LinesBegin()
{
	dbg_assert(m_Drawing == EDrawing::NONE, "called Graphics()->LinesBegin twice");
	m_Drawing = EDrawing::LINES;
	SetColor(1, 1, 1, 1);
}

void CGraphics_Threaded::LinesEnd()
{
	dbg_assert(m_Drawing == EDrawing::LINES, "called Graphics()->LinesEnd without begin");
	FlushVertices();
	m_Drawing = EDrawing::NONE;
}

void CGraphics_Threaded::LinesDraw(const CLineItem *pArray, size_t Num)
{
	dbg_assert(m_Drawing == EDrawing::LINES, "called Graphics()->LinesDraw without begin");

	size_t VertexIndex = m_NumVertices;
	for(size_t i = 0; i < Num; ++i)
	{
		m_aVertices[VertexIndex].m_Pos.x = pArray[i].m_X0;
		m_aVertices[VertexIndex].m_Pos.y = pArray[i].m_Y0;
		m_aVertices[VertexIndex].m_Tex = m_aTexture[0];
		SetColor(&m_aVertices[VertexIndex], 0);
		++VertexIndex;

		m_aVertices[VertexIndex].m_Pos.x = pArray[i].m_X1;
		m_aVertices[VertexIndex].m_Pos.y = pArray[i].m_Y1;
		m_aVertices[VertexIndex].m_Tex = m_aTexture[1];
		SetColor(&m_aVertices[VertexIndex], 1);
		++VertexIndex;
	}

	AddVertices(2 * Num);
}

void CGraphics_Threaded::LinesBatchBegin(CLineItemBatch *pBatch)
{
	pBatch->m_NumItems = 0;
	LinesBegin();
}

void CGraphics_Threaded::LinesBatchEnd(CLineItemBatch *pBatch)
{
	if(pBatch->m_NumItems > 0)
	{
		LinesDraw(pBatch->m_aItems, pBatch->m_NumItems);
		pBatch->m_NumItems = 0;
	}
	LinesEnd();
}

void CGraphics_Threaded::LinesBatchDraw(CLineItemBatch *pBatch, const CLineItem *pArray, size_t Num)
{
	if(pBatch->m_NumItems + Num >= std::size(pBatch->m_aItems))
	{
		LinesDraw(pBatch->m_aItems, pBatch->m_NumItems);
		pBatch->m_NumItems = 0;
	}
	if(Num >= std::size(pBatch->m_aItems))
	{
		LinesDraw(pArray, Num);
		return;
	}
	std::copy(pArray, pArray + Num, pBatch->m_aItems + pBatch->m_NumItems);
	pBatch->m_NumItems += Num;
}

IGraphics::CTextureHandle CGraphics_Threaded::FindFreeTextureIndex()
{
	const size_t CurSize = m_vTextureIndices.size();
	if(m_FirstFreeTexture == CurSize)
	{
		m_vTextureIndices.resize(CurSize * 2);
		for(size_t i = 0; i < CurSize; ++i)
			m_vTextureIndices[CurSize + i] = CurSize + i + 1;
	}
	const size_t Tex = m_FirstFreeTexture;
	m_FirstFreeTexture = m_vTextureIndices[Tex];
	m_vTextureIndices[Tex] = -1;
	return CreateTextureHandle(Tex);
}

void CGraphics_Threaded::FreeTextureIndex(CTextureHandle *pIndex)
{
	dbg_assert(pIndex->IsValid(), "Cannot free invalid texture index");
	dbg_assert(m_vTextureIndices[pIndex->Id()] == -1, "Cannot free already freed texture index");

	m_vTextureIndices[pIndex->Id()] = m_FirstFreeTexture;
	m_FirstFreeTexture = pIndex->Id();
	pIndex->Invalidate();
}

void CGraphics_Threaded::UnloadTexture(CTextureHandle *pIndex)
{
	if(pIndex->IsNullTexture() || !pIndex->IsValid())
		return;

	CCommandBuffer::SCommand_Texture_Destroy Cmd;
	Cmd.m_Slot = pIndex->Id();
	AddCmd(Cmd);

	FreeTextureIndex(pIndex);
}

IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTexture(const CImageInfo &FromImageInfo, const std::optional<CImageInfo> &FallbackImageInfo, const CDataSprite *pSprite)
{
	int ImageGridX = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx;
	int ImageGridY = FromImageInfo.m_Height / pSprite->m_pSet->m_Gridy;
	int x = pSprite->m_X * ImageGridX;
	int y = pSprite->m_Y * ImageGridY;
	int w = pSprite->m_W * ImageGridX;
	int h = pSprite->m_H * ImageGridY;

	// check for invisible texture, maybe due to outdated game assets
	if(FallbackImageInfo.has_value() && IsImageSubFullyTransparent(FromImageInfo, x, y, w, h))
	{
		log_warn("graphics", "Asset '%s' appears to be invisible, falling back to default", pSprite->m_pName);
		return LoadSpriteTexture(FallbackImageInfo.value(), std::nullopt, pSprite);
	}

	CImageInfo SpriteInfo;
	SpriteInfo.m_Width = w;
	SpriteInfo.m_Height = h;
	SpriteInfo.m_Format = FromImageInfo.m_Format;
	SpriteInfo.Allocate();
	SpriteInfo.CopyRectFrom(FromImageInfo, x, y, w, h, 0, 0);
	return LoadTextureRawMove(SpriteInfo, 0, pSprite->m_pName);
}

bool CGraphics_Threaded::IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h)
{
	if(FromImageInfo.m_Format == CImageInfo::FORMAT_R || FromImageInfo.m_Format == CImageInfo::FORMAT_RA || FromImageInfo.m_Format == CImageInfo::FORMAT_RGBA)
	{
		const uint8_t *pImgData = FromImageInfo.m_pData;
		const size_t PixelSize = FromImageInfo.PixelSize();
		for(int iy = 0; iy < h; ++iy)
		{
			for(int ix = 0; ix < w; ++ix)
			{
				const size_t RealOffset = (x + ix) * PixelSize + (y + iy) * PixelSize * FromImageInfo.m_Width;
				if(pImgData[RealOffset + (PixelSize - 1)] > 0)
					return false;
			}
		}

		return true;
	}
	return false;
}

bool CGraphics_Threaded::IsSpriteTextureFullyTransparent(const CImageInfo &FromImageInfo, const CDataSprite *pSprite)
{
	int ImageGridX = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx;
	int ImageGridY = FromImageInfo.m_Height / pSprite->m_pSet->m_Gridy;
	int x = pSprite->m_X * ImageGridX;
	int y = pSprite->m_Y * ImageGridY;
	int w = pSprite->m_W * ImageGridX;
	int h = pSprite->m_H * ImageGridY;
	return IsImageSubFullyTransparent(FromImageInfo, x, y, w, h);
}

void CGraphics_Threaded::LoadTextureAddWarning(size_t Width, size_t Height, int Flags, const char *pTexName)
{
	if((Flags & IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE) != 0 || (Flags & IGraphics::TEXLOAD_TO_3D_TEXTURE) != 0)
	{
		if(Width == 0 || (Width % 16) != 0 || Height == 0 || (Height % 16) != 0)
		{
			SWarning NewWarning;
			char aText[128];
			str_format(aText, sizeof(aText), "\"%s\"", pTexName ? pTexName : "(no name)");
			str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), aText, 16, 16);
			AddWarning(NewWarning);
		}
	}
}

static CCommandBuffer::SCommand_Texture_Create LoadTextureCreateCommand(int TextureId, size_t Width, size_t Height, int Flags)
{
	CCommandBuffer::SCommand_Texture_Create Cmd;
	Cmd.m_Slot = TextureId;
	Cmd.m_Width = Width;
	Cmd.m_Height = Height;

	Cmd.m_Flags = 0;
	if((Flags & IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE) != 0)
		Cmd.m_Flags |= TextureFlag::TO_2D_ARRAY_TEXTURE;
	if((Flags & IGraphics::TEXLOAD_TO_3D_TEXTURE) != 0)
		Cmd.m_Flags |= TextureFlag::TO_3D_TEXTURE;
	if((Flags & IGraphics::TEXLOAD_NO_2D_TEXTURE) != 0)
		Cmd.m_Flags |= TextureFlag::NO_2D_TEXTURE;

	return Cmd;
}

IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName)
{
	LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName);

	if(Image.m_Width == 0 || Image.m_Height == 0)
		return IGraphics::CTextureHandle();

	IGraphics::CTextureHandle TextureHandle = FindFreeTextureIndex();
	CCommandBuffer::SCommand_Texture_Create Cmd = LoadTextureCreateCommand(TextureHandle.Id(), Image.m_Width, Image.m_Height, Flags);

	// Copy texture data and convert if necessary
	uint8_t *pTmpData;
	if(!ConvertToRgbaAlloc(pTmpData, Image))
	{
		log_warn("graphics", "Converted image '%s' to RGBA, consider making its file format RGBA.", pTexName ? pTexName : "(no name)");
	}
	Cmd.m_pData = pTmpData;

	AddCmd(Cmd);

	return TextureHandle;
}

IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRawMove(CImageInfo &Image, int Flags, const char *pTexName)
{
	if(Image.m_Format != CImageInfo::FORMAT_RGBA)
	{
		// Moving not possible, texture needs to be converted
		IGraphics::CTextureHandle TextureHandle = LoadTextureRaw(Image, Flags, pTexName);
		Image.Free();
		return TextureHandle;
	}

	LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName);

	if(Image.m_Width == 0 || Image.m_Height == 0)
		return IGraphics::CTextureHandle();

	IGraphics::CTextureHandle TextureHandle = FindFreeTextureIndex();
	CCommandBuffer::SCommand_Texture_Create Cmd = LoadTextureCreateCommand(TextureHandle.Id(), Image.m_Width, Image.m_Height, Flags);
	Cmd.m_pData = Image.m_pData;
	Image.m_pData = nullptr;
	Image.Free();
	AddCmd(Cmd);

	return TextureHandle;
}

IGraphics::CTextureHandle CGraphics_Threaded::LoadTexture(const char *pFilename, int StorageType, int Flags)
{
	dbg_assert(pFilename[0] != '\0', "Cannot load texture from file with empty filename"); // would cause Valgrind to crash otherwise

	CImageInfo Image;
	if(LoadPng(Image, pFilename, StorageType))
	{
		CTextureHandle Id = LoadTextureRawMove(Image, Flags, pFilename);
		if(Id.IsValid())
		{
			if(g_Config.m_Debug)
				log_trace("graphics/texture", "Loaded texture '%s'", pFilename);
			return Id;
		}
	}

	return m_NullTexture;
}

bool CGraphics_Threaded::LoadTextTextures(size_t Width, size_t Height, CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture, uint8_t *pTextData, uint8_t *pTextOutlineData)
{
	if(Width == 0 || Height == 0)
		return false;

	TextTexture = FindFreeTextureIndex();
	TextOutlineTexture = FindFreeTextureIndex();

	CCommandBuffer::SCommand_TextTextures_Create Cmd;
	Cmd.m_Slot = TextTexture.Id();
	Cmd.m_SlotOutline = TextOutlineTexture.Id();
	Cmd.m_Width = Width;
	Cmd.m_Height = Height;
	Cmd.m_pTextData = pTextData;
	Cmd.m_pTextOutlineData = pTextOutlineData;
	AddCmd(Cmd);

	return true;
}

bool CGraphics_Threaded::UnloadTextTextures(CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture)
{
	CCommandBuffer::SCommand_TextTextures_Destroy Cmd;
	Cmd.m_Slot = TextTexture.Id();
	Cmd.m_SlotOutline = TextOutlineTexture.Id();
	AddCmd(Cmd);

	if(TextTexture.IsValid())
		FreeTextureIndex(&TextTexture);
	if(TextOutlineTexture.IsValid())
		FreeTextureIndex(&TextOutlineTexture);
	return true;
}

bool CGraphics_Threaded::UpdateTextTexture(CTextureHandle TextureId, int x, int y, size_t Width, size_t Height, uint8_t *pData, bool IsMovedPointer)
{
	CCommandBuffer::SCommand_TextTexture_Update Cmd;
	Cmd.m_Slot = TextureId.Id();
	Cmd.m_X = x;
	Cmd.m_Y = y;
	Cmd.m_Width = Width;
	Cmd.m_Height = Height;

	if(IsMovedPointer)
	{
		Cmd.m_pData = pData;
	}
	else
	{
		const size_t MemSize = Width * Height;
		uint8_t *pTmpData = static_cast<uint8_t *>(malloc(MemSize));
		mem_copy(pTmpData, pData, MemSize);
		Cmd.m_pData = pTmpData;
	}
	AddCmd(Cmd);

	return true;
}

static SWarning FormatPngliteIncompatibilityWarning(int PngliteIncompatible, const char *pContextName)
{
	SWarning Warning;
	str_format(Warning.m_aWarningMsg, sizeof(Warning.m_aWarningMsg), Localize("\"%s\" is not compatible with pnglite and cannot be loaded by old DDNet versions:"), pContextName);
	str_append(Warning.m_aWarningMsg, " ");
	static const int FLAGS[] = {CImageLoader::PNGLITE_COLOR_TYPE, CImageLoader::PNGLITE_BIT_DEPTH, CImageLoader::PNGLITE_INTERLACE_TYPE, CImageLoader::PNGLITE_COMPRESSION_TYPE, CImageLoader::PNGLITE_FILTER_TYPE};
	static const char *const EXPLANATION[] = {"color type", "bit depth", "interlace type", "compression type", "filter type"};

	bool First = true;
	for(size_t i = 0; i < std::size(FLAGS); ++i)
	{
		if((PngliteIncompatible & FLAGS[i]) != 0)
		{
			if(!First)
			{
				str_append(Warning.m_aWarningMsg, ", ");
			}
			str_append(Warning.m_aWarningMsg, EXPLANATION[i]);
			First = false;
		}
	}
	str_append(Warning.m_aWarningMsg, " unsupported");
	return Warning;
}

bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const char *pFilename, int StorageType)
{
	IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType);

	int PngliteIncompatible;
	if(!CImageLoader::LoadPng(File, pFilename, Image, PngliteIncompatible))
		return false;

	if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0)
	{
		AddWarning(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pFilename));
	}

	return true;
}

bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const uint8_t *pData, size_t DataSize, const char *pContextName)
{
	CByteBufferReader Reader(pData, DataSize);
	int PngliteIncompatible;
	if(!CImageLoader::LoadPng(Reader, pContextName, Image, PngliteIncompatible))
		return false;

	if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0)
	{
		AddWarning(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pContextName));
	}

	return true;
}

bool CGraphics_Threaded::CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize)
{
	dbg_assert(DivX != 0 && DivY != 0, "Passing 0 to this function is not allowed.");
	bool ImageIsValid = true;
	bool WidthBroken = Image.m_Width == 0 || (Image.m_Width % DivX) != 0;
	bool HeightBroken = Image.m_Height == 0 || (Image.m_Height % DivY) != 0;
	if(WidthBroken || HeightBroken)
	{
		SWarning NewWarning;
		char aContextNameQuoted[128];
		str_format(aContextNameQuoted, sizeof(aContextNameQuoted), "\"%s\"", pContextName);
		str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg),
			Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), aContextNameQuoted, DivX, DivY);
		AddWarning(NewWarning);
		ImageIsValid = false;
	}

	if(AllowResize && !ImageIsValid && Image.m_Width > 0 && Image.m_Height > 0)
	{
		int NewWidth = DivX;
		int NewHeight = DivY;
		if(WidthBroken)
		{
			NewWidth = std::max(HighestBit(Image.m_Width), DivX);
			NewHeight = (NewWidth / DivX) * DivY;
		}
		else
		{
			NewHeight = std::max(HighestBit(Image.m_Height), DivY);
			NewWidth = (NewHeight / DivY) * DivX;
		}
		ResizeImage(Image, NewWidth, NewHeight);
		ImageIsValid = true;
	}

	return ImageIsValid;
}

bool CGraphics_Threaded::IsImageFormatRgba(const char *pContextName, const CImageInfo &Image)
{
	if(Image.m_Format != CImageInfo::FORMAT_RGBA)
	{
		SWarning NewWarning;
		char aContextNameQuoted[128];
		str_format(aContextNameQuoted, sizeof(aContextNameQuoted), "\"%s\"", pContextName);
		str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg),
			Localize("The format of texture %s is not RGBA which will cause visual bugs."), aContextNameQuoted);
		AddWarning(NewWarning);
		return false;
	}
	return true;
}

void CGraphics_Threaded::KickCommandBuffer()
{
	m_pBackend->RunBuffer(m_pCommandBuffer);

	std::vector<std::string> WarningStrings;
	if(m_pBackend->GetWarning(WarningStrings))
	{
		SWarning NewWarning;
		std::string WarningStr;
		for(const auto &WarnStr : WarningStrings)
			WarningStr.append((WarnStr + "\n"));
		str_copy(NewWarning.m_aWarningMsg, WarningStr.c_str());
		AddWarning(NewWarning);
	}

	// swap buffer
	m_CurrentCommandBuffer ^= 1;
	m_pCommandBuffer = m_apCommandBuffers[m_CurrentCommandBuffer];
	m_pCommandBuffer->Reset();
}

class CScreenshotSaveJob : public IJob
{
	IStorage *m_pStorage;
	char m_aName[IO_MAX_PATH_LENGTH];
	CImageInfo m_Image;

	void Run() override
	{
		static constexpr LOG_COLOR SCREENSHOT_LOG_COLOR = LOG_COLOR{255, 153, 76};
		char aWholePath[IO_MAX_PATH_LENGTH];
		if(CImageLoader::SavePng(m_pStorage->OpenFile(m_aName, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)), m_aName, m_Image))
		{
			log_info_color(SCREENSHOT_LOG_COLOR, "client", "Saved screenshot to '%s'", aWholePath);
		}
		else
		{
			log_error_color(SCREENSHOT_LOG_COLOR, "client", "Failed to save screenshot to '%s'", aWholePath);
		}
	}

public:
	CScreenshotSaveJob(IStorage *pStorage, const char *pName, CImageInfo &&Image) :
		m_pStorage(pStorage),
		m_Image(std::move(Image))
	{
		str_copy(m_aName, pName);
	}

	~CScreenshotSaveJob() override
	{
		m_Image.Free();
	}
};

void CGraphics_Threaded::ScreenshotDirect(bool *pSwapped)
{
	if(!m_DoScreenshot)
		return;
	m_DoScreenshot = false;
	if(!WindowActive())
		return;

	CImageInfo Image;

	CCommandBuffer::SCommand_TrySwapAndScreenshot Cmd;
	Cmd.m_pImage = &Image;
	Cmd.m_pSwapped = pSwapped;
	AddCmd(Cmd);

	KickCommandBuffer();
	WaitForIdle();

	if(Image.m_pData)
	{
		m_pEngine->AddJob(std::make_shared<CScreenshotSaveJob>(m_pStorage, m_aScreenshotName, std::move(Image)));
	}
	else
	{
		log_error("graphics", "Failed to create screenshot");
	}
}

void CGraphics_Threaded::TextureSet(CTextureHandle TextureId)
{
	dbg_assert(m_Drawing == EDrawing::NONE, "called Graphics()->TextureSet within begin");
	dbg_assert(!TextureId.IsValid() || m_vTextureIndices[TextureId.Id()] == -1, "Texture handle was not invalid, but also did not correlate to an existing texture.");
	m_State.m_Texture = TextureId.Id();
}

void CGraphics_Threaded::Clear(float r, float g, float b, bool ForceClearNow)
{
	CCommandBuffer::SCommand_Clear Cmd;
	Cmd.m_Color.r = r;
	Cmd.m_Color.g = g;
	Cmd.m_Color.b = b;
	Cmd.m_Color.a = 0;
	Cmd.m_ForceClear = ForceClearNow;
	AddCmd(Cmd);
}

void CGraphics_Threaded::QuadsBegin()
{
	dbg_assert(m_Drawing == EDrawing::NONE, "called Graphics()->QuadsBegin twice");
	m_Drawing = EDrawing::QUADS;

	QuadsSetSubset(0, 0, 1, 1);
	QuadsSetRotation(0);
	SetColor(1, 1, 1, 1);
}

void CGraphics_Threaded::QuadsEnd()
{
	dbg_assert(m_Drawing == EDrawing::QUADS, "called Graphics()->QuadsEnd without begin");
	FlushVertices();
	m_Drawing = EDrawing::NONE;
}

void CGraphics_Threaded::QuadsTex3DBegin()
{
	QuadsBegin();
}

void CGraphics_Threaded::QuadsTex3DEnd()
{
	dbg_assert(m_Drawing == EDrawing::QUADS, "called Graphics()->QuadsEnd without begin");
	FlushVerticesTex3D();
	m_Drawing = EDrawing::NONE;
}

void CGraphics_Threaded::TrianglesBegin()
{
	dbg_assert(m_Drawing == EDrawing::NONE, "called Graphics()->TrianglesBegin twice");
	m_Drawing = EDrawing::TRIANGLES;

	QuadsSetSubset(0, 0, 1, 1);
	QuadsSetRotation(0);
	SetColor(1, 1, 1, 1);
}

void CGraphics_Threaded::TrianglesEnd()
{
	dbg_assert(m_Drawing == EDrawing::TRIANGLES, "called Graphics()->TrianglesEnd without begin");
	FlushVertices();
	m_Drawing = EDrawing::NONE;
}

void CGraphics_Threaded::QuadsEndKeepVertices()
{
	dbg_assert(m_Drawing == EDrawing::QUADS, "called Graphics()->QuadsEndKeepVertices without begin");
	FlushVertices(true);
	m_Drawing = EDrawing::NONE;
}

void CGraphics_Threaded::QuadsDrawCurrentVertices(bool KeepVertices)
{
	m_Drawing = EDrawing::QUADS;
	FlushVertices(KeepVertices);
	m_Drawing = EDrawing::NONE;
}

void CGraphics_Threaded::QuadsSetRotation(float Angle)
{
	m_Rotation = Angle;
}

static unsigned char NormalizeColorComponent(float ColorComponent)
{
	return (unsigned char)(std::clamp(ColorComponent, 0.0f, 1.0f) * 255.0f + 0.5f); // +0.5f to round to nearest
}

void CGraphics_Threaded::SetColorVertex(const CColorVertex *pArray, size_t Num)
{
	dbg_assert(m_Drawing != EDrawing::NONE, "called Graphics()->SetColorVertex without begin");

	for(size_t i = 0; i < Num; ++i)
	{
		const CColorVertex &Vertex = pArray[i];
		CCommandBuffer::SColor &Color = m_aColor[Vertex.m_Index];
		Color.r = NormalizeColorComponent(Vertex.m_R);
		Color.g = NormalizeColorComponent(Vertex.m_G);
		Color.b = NormalizeColorComponent(Vertex.m_B);
		Color.a = NormalizeColorComponent(Vertex.m_A);
	}
}

void CGraphics_Threaded::SetColor(float r, float g, float b, float a)
{
	CCommandBuffer::SColor NewColor;
	NewColor.r = NormalizeColorComponent(r);
	NewColor.g = NormalizeColorComponent(g);
	NewColor.b = NormalizeColorComponent(b);
	NewColor.a = NormalizeColorComponent(a);
	std::fill(std::begin(m_aColor), std::end(m_aColor), NewColor);
}

void CGraphics_Threaded::SetColor(ColorRGBA Color)
{
	SetColor(Color.r, Color.g, Color.b, Color.a);
}

void CGraphics_Threaded::SetColor4(ColorRGBA TopLeft, ColorRGBA TopRight, ColorRGBA BottomLeft, ColorRGBA BottomRight)
{
	CColorVertex aArray[] = {
		CColorVertex(0, TopLeft),
		CColorVertex(1, TopRight),
		CColorVertex(2, BottomRight),
		CColorVertex(3, BottomLeft)};
	SetColorVertex(aArray, std::size(aArray));
}

void CGraphics_Threaded::ChangeColorOfCurrentQuadVertices(float r, float g, float b, float a)
{
	m_aColor[0].r = NormalizeColorComponent(r);
	m_aColor[0].g = NormalizeColorComponent(g);
	m_aColor[0].b = NormalizeColorComponent(b);
	m_aColor[0].a = NormalizeColorComponent(a);

	for(int i = 0; i < m_NumVertices; ++i)
	{
		SetColor(&m_aVertices[i], 0);
	}
}

void CGraphics_Threaded::ChangeColorOfQuadVertices(size_t QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a)
{
	const CCommandBuffer::SColor Color(r, g, b, a);
	const size_t VertNum = g_Config.m_GfxQuadAsTriangle && !m_GLUseTrianglesAsQuad ? 6 : 4;
	for(size_t i = 0; i < VertNum; ++i)
	{
		m_aVertices[QuadOffset * VertNum + i].m_Color = Color;
	}
}

void CGraphics_Threaded::QuadsSetSubset(float TlU, float TlV, float BrU, float BrV)
{
	m_aTexture[0].u = TlU;
	m_aTexture[1].u = BrU;
	m_aTexture[0].v = TlV;
	m_aTexture[1].v = TlV;

	m_aTexture[3].u = TlU;
	m_aTexture[2].u = BrU;
	m_aTexture[3].v = BrV;
	m_aTexture[2].v = BrV;
}

void CGraphics_Threaded::QuadsSetSubsetFree(
	float x0, float y0, float x1, float y1,
	float x2, float y2, float x3, float y3, int Index)
{
	m_aTexture[0].u = x0;
	m_aTexture[0].v = y0;
	m_aTexture[1].u = x1;
	m_aTexture[1].v = y1;
	m_aTexture[2].u = x2;
	m_aTexture[2].v = y2;
	m_aTexture[3].u = x3;
	m_aTexture[3].v = y3;
	m_CurIndex = Index;
}

void CGraphics_Threaded::QuadsDraw(CQuadItem *pArray, int Num)
{
	for(int i = 0; i < Num; ++i)
	{
		pArray[i].m_X -= pArray[i].m_Width / 2;
		pArray[i].m_Y -= pArray[i].m_Height / 2;
	}

	QuadsDrawTL(pArray, Num);
}

void CGraphics_Threaded::QuadsDrawTL(const CQuadItem *pArray, int Num)
{
	QuadsDrawTLImpl(m_aVertices, pArray, Num);
}

void CGraphics_Threaded::QuadsTex3DDrawTL(const CQuadItem *pArray, int Num)
{
	const int VertNum = g_Config.m_GfxQuadAsTriangle && !m_GLUseTrianglesAsQuad ? 6 : 4;
	const float CurIndex = Uses2DTextureArrays() ? m_CurIndex : (m_CurIndex + 0.5f) / 256.0f;

	for(int i = 0; i < Num; ++i)
	{
		for(int n = 0; n < VertNum; ++n)
		{
			m_aVerticesTex3D[m_NumVertices + VertNum * i + n].m_Tex.w = CurIndex;
		}
	}

	QuadsDrawTLImpl(m_aVerticesTex3D, pArray, Num);
}

void CGraphics_Threaded::QuadsDrawFreeform(const CFreeformItem *pArray, int Num)
{
	dbg_assert(m_Drawing == EDrawing::QUADS || m_Drawing == EDrawing::TRIANGLES, "called Graphics()->QuadsDrawFreeform without begin");

	if((g_Config.m_GfxQuadAsTriangle && !m_GLUseTrianglesAsQuad) || m_Drawing == EDrawing::TRIANGLES)
	{
		for(int i = 0; i < Num; ++i)
		{
			m_aVertices[m_NumVertices + 6 * i].m_Pos.x = pArray[i].m_X0;
			m_aVertices[m_NumVertices + 6 * i].m_Pos.y = pArray[i].m_Y0;
			m_aVertices[m_NumVertices + 6 * i].m_Tex = m_aTexture[0];
			SetColor(&m_aVertices[m_NumVertices + 6 * i], 0);

			m_aVertices[m_NumVertices + 6 * i + 1].m_Pos.x = pArray[i].m_X1;
			m_aVertices[m_NumVertices + 6 * i + 1].m_Pos.y = pArray[i].m_Y1;
			m_aVertices[m_NumVertices + 6 * i + 1].m_Tex = m_aTexture[1];
			SetColor(&m_aVertices[m_NumVertices + 6 * i + 1], 1);

			m_aVertices[m_NumVertices + 6 * i + 2].m_Pos.x = pArray[i].m_X3;
			m_aVertices[m_NumVertices + 6 * i + 2].m_Pos.y = pArray[i].m_Y3;
			m_aVertices[m_NumVertices + 6 * i + 2].m_Tex = m_aTexture[3];
			SetColor(&m_aVertices[m_NumVertices + 6 * i + 2], 3);

			m_aVertices[m_NumVertices + 6 * i + 3].m_Pos.x = pArray[i].m_X0;
			m_aVertices[m_NumVertices + 6 * i + 3].m_Pos.y = pArray[i].m_Y0;
			m_aVertices[m_NumVertices + 6 * i + 3].m_Tex = m_aTexture[0];
			SetColor(&m_aVertices[m_NumVertices + 6 * i + 3], 0);

			m_aVertices[m_NumVertices + 6 * i + 4].m_Pos.x = pArray[i].m_X3;
			m_aVertices[m_NumVertices + 6 * i + 4].m_Pos.y = pArray[i].m_Y3;
			m_aVertices[m_NumVertices + 6 * i + 4].m_Tex = m_aTexture[3];
			SetColor(&m_aVertices[m_NumVertices + 6 * i + 4], 3);

			m_aVertices[m_NumVertices + 6 * i + 5].m_Pos.x = pArray[i].m_X2;
			m_aVertices[m_NumVertices + 6 * i + 5].m_Pos.y = pArray[i].m_Y2;
			m_aVertices[m_NumVertices + 6 * i + 5].m_Tex = m_aTexture[2];
			SetColor(&m_aVertices[m_NumVertices + 6 * i + 5], 2);
		}

		AddVertices(3 * 2 * Num);
	}
	else
	{
		for(int i = 0; i < Num; ++i)
		{
			m_aVertices[m_NumVertices + 4 * i].m_Pos.x = pArray[i].m_X0;
			m_aVertices[m_NumVertices + 4 * i].m_Pos.y = pArray[i].m_Y0;
			m_aVertices[m_NumVertices + 4 * i].m_Tex = m_aTexture[0];
			SetColor(&m_aVertices[m_NumVertices + 4 * i], 0);

			m_aVertices[m_NumVertices + 4 * i + 1].m_Pos.x = pArray[i].m_X1;
			m_aVertices[m_NumVertices + 4 * i + 1].m_Pos.y = pArray[i].m_Y1;
			m_aVertices[m_NumVertices + 4 * i + 1].m_Tex = m_aTexture[1];
			SetColor(&m_aVertices[m_NumVertices + 4 * i + 1], 1);

			m_aVertices[m_NumVertices + 4 * i + 2].m_Pos.x = pArray[i].m_X3;
			m_aVertices[m_NumVertices + 4 * i + 2].m_Pos.y = pArray[i].m_Y3;
			m_aVertices[m_NumVertices + 4 * i + 2].m_Tex = m_aTexture[3];
			SetColor(&m_aVertices[m_NumVertices + 4 * i + 2], 3);

			m_aVertices[m_NumVertices + 4 * i + 3].m_Pos.x = pArray[i].m_X2;
			m_aVertices[m_NumVertices + 4 * i + 3].m_Pos.y = pArray[i].m_Y2;
			m_aVertices[m_NumVertices + 4 * i + 3].m_Tex = m_aTexture[2];
			SetColor(&m_aVertices[m_NumVertices + 4 * i + 3], 2);
		}

		AddVertices(4 * Num);
	}
}

void CGraphics_Threaded::QuadsText(float x, float y, float Size, const char *pText)
{
	float StartX = x;

	while(*pText)
	{
		char c = *pText;
		pText++;

		if(c == '\n')
		{
			x = StartX;
			y += Size;
		}
		else
		{
			QuadsSetSubset(
				(c % 16) / 16.0f,
				(c / 16) / 16.0f,
				(c % 16) / 16.0f + 1.0f / 16.0f,
				(c / 16) / 16.0f + 1.0f / 16.0f);

			CQuadItem QuadItem(x, y, Size, Size);
			QuadsDrawTL(&QuadItem, 1);
			x += Size / 2;
		}
	}
}

void CGraphics_Threaded::DrawRectExt(float x, float y, float w, float h, float r, int Corners)
{
	const int NumSegments = 8;
	const float SegmentsAngle = pi / 2 / NumSegments;
	IGraphics::CFreeformItem aFreeform[NumSegments * 4];
	size_t NumItems = 0;

	for(int i = 0; i < NumSegments; i += 2)
	{
		float a1 = i * SegmentsAngle;
		float a2 = (i + 1) * SegmentsAngle;
		float a3 = (i + 2) * SegmentsAngle;
		float Ca1 = std::cos(a1);
		float Ca2 = std::cos(a2);
		float Ca3 = std::cos(a3);
		float Sa1 = std::sin(a1);
		float Sa2 = std::sin(a2);
		float Sa3 = std::sin(a3);

		if(Corners & CORNER_TL)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + r, y + r,
				x + (1 - Ca1) * r, y + (1 - Sa1) * r,
				x + (1 - Ca3) * r, y + (1 - Sa3) * r,
				x + (1 - Ca2) * r, y + (1 - Sa2) * r);

		if(Corners & CORNER_TR)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + w - r, y + r,
				x + w - r + Ca1 * r, y + (1 - Sa1) * r,
				x + w - r + Ca3 * r, y + (1 - Sa3) * r,
				x + w - r + Ca2 * r, y + (1 - Sa2) * r);

		if(Corners & CORNER_BL)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + r, y + h - r,
				x + (1 - Ca1) * r, y + h - r + Sa1 * r,
				x + (1 - Ca3) * r, y + h - r + Sa3 * r,
				x + (1 - Ca2) * r, y + h - r + Sa2 * r);

		if(Corners & CORNER_BR)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + w - r, y + h - r,
				x + w - r + Ca1 * r, y + h - r + Sa1 * r,
				x + w - r + Ca3 * r, y + h - r + Sa3 * r,
				x + w - r + Ca2 * r, y + h - r + Sa2 * r);
	}
	QuadsDrawFreeform(aFreeform, NumItems);

	CQuadItem aQuads[9];
	NumItems = 0;
	aQuads[NumItems++] = CQuadItem(x + r, y + r, w - r * 2, h - r * 2); // center
	aQuads[NumItems++] = CQuadItem(x + r, y, w - r * 2, r); // top
	aQuads[NumItems++] = CQuadItem(x + r, y + h - r, w - r * 2, r); // bottom
	aQuads[NumItems++] = CQuadItem(x, y + r, r, h - r * 2); // left
	aQuads[NumItems++] = CQuadItem(x + w - r, y + r, r, h - r * 2); // right

	if(!(Corners & CORNER_TL))
		aQuads[NumItems++] = CQuadItem(x, y, r, r);
	if(!(Corners & CORNER_TR))
		aQuads[NumItems++] = CQuadItem(x + w, y, -r, r);
	if(!(Corners & CORNER_BL))
		aQuads[NumItems++] = CQuadItem(x, y + h, r, -r);
	if(!(Corners & CORNER_BR))
		aQuads[NumItems++] = CQuadItem(x + w, y + h, -r, -r);

	QuadsDrawTL(aQuads, NumItems);
}

void CGraphics_Threaded::DrawRectExt4(float x, float y, float w, float h, ColorRGBA ColorTopLeft, ColorRGBA ColorTopRight, ColorRGBA ColorBottomLeft, ColorRGBA ColorBottomRight, float r, int Corners)
{
	if(Corners == 0 || r == 0.0f)
	{
		SetColor4(ColorTopLeft, ColorTopRight, ColorBottomLeft, ColorBottomRight);
		CQuadItem ItemQ = CQuadItem(x, y, w, h);
		QuadsDrawTL(&ItemQ, 1);
		return;
	}

	const int NumSegments = 8;
	const float SegmentsAngle = pi / 2 / NumSegments;
	for(int i = 0; i < NumSegments; i += 2)
	{
		float a1 = i * SegmentsAngle;
		float a2 = (i + 1) * SegmentsAngle;
		float a3 = (i + 2) * SegmentsAngle;
		float Ca1 = std::cos(a1);
		float Ca2 = std::cos(a2);
		float Ca3 = std::cos(a3);
		float Sa1 = std::sin(a1);
		float Sa2 = std::sin(a2);
		float Sa3 = std::sin(a3);

		if(Corners & CORNER_TL)
		{
			SetColor(ColorTopLeft);
			IGraphics::CFreeformItem ItemF = IGraphics::CFreeformItem(
				x + r, y + r,
				x + (1 - Ca1) * r, y + (1 - Sa1) * r,
				x + (1 - Ca3) * r, y + (1 - Sa3) * r,
				x + (1 - Ca2) * r, y + (1 - Sa2) * r);
			QuadsDrawFreeform(&ItemF, 1);
		}

		if(Corners & CORNER_TR)
		{
			SetColor(ColorTopRight);
			IGraphics::CFreeformItem ItemF = IGraphics::CFreeformItem(
				x + w - r, y + r,
				x + w - r + Ca1 * r, y + (1 - Sa1) * r,
				x + w - r + Ca3 * r, y + (1 - Sa3) * r,
				x + w - r + Ca2 * r, y + (1 - Sa2) * r);
			QuadsDrawFreeform(&ItemF, 1);
		}

		if(Corners & CORNER_BL)
		{
			SetColor(ColorBottomLeft);
			IGraphics::CFreeformItem ItemF = IGraphics::CFreeformItem(
				x + r, y + h - r,
				x + (1 - Ca1) * r, y + h - r + Sa1 * r,
				x + (1 - Ca3) * r, y + h - r + Sa3 * r,
				x + (1 - Ca2) * r, y + h - r + Sa2 * r);
			QuadsDrawFreeform(&ItemF, 1);
		}

		if(Corners & CORNER_BR)
		{
			SetColor(ColorBottomRight);
			IGraphics::CFreeformItem ItemF = IGraphics::CFreeformItem(
				x + w - r, y + h - r,
				x + w - r + Ca1 * r, y + h - r + Sa1 * r,
				x + w - r + Ca3 * r, y + h - r + Sa3 * r,
				x + w - r + Ca2 * r, y + h - r + Sa2 * r);
			QuadsDrawFreeform(&ItemF, 1);
		}
	}

	SetColor4(ColorTopLeft, ColorTopRight, ColorBottomLeft, ColorBottomRight);
	CQuadItem ItemQ = CQuadItem(x + r, y + r, w - r * 2, h - r * 2); // center
	QuadsDrawTL(&ItemQ, 1);

	SetColor4(ColorTopLeft, ColorTopRight, ColorTopLeft, ColorTopRight);
	ItemQ = CQuadItem(x + r, y, w - r * 2, r); // top
	QuadsDrawTL(&ItemQ, 1);

	SetColor4(ColorBottomLeft, ColorBottomRight, ColorBottomLeft, ColorBottomRight);
	ItemQ = CQuadItem(x + r, y + h - r, w - r * 2, r); // bottom
	QuadsDrawTL(&ItemQ, 1);

	SetColor4(ColorTopLeft, ColorTopLeft, ColorBottomLeft, ColorBottomLeft);
	ItemQ = CQuadItem(x, y + r, r, h - r * 2); // left
	QuadsDrawTL(&ItemQ, 1);

	SetColor4(ColorTopRight, ColorTopRight, ColorBottomRight, ColorBottomRight);
	ItemQ = CQuadItem(x + w - r, y + r, r, h - r * 2); // right
	QuadsDrawTL(&ItemQ, 1);

	if(!(Corners & CORNER_TL))
	{
		SetColor(ColorTopLeft);
		ItemQ = CQuadItem(x, y, r, r);
		QuadsDrawTL(&ItemQ, 1);
	}

	if(!(Corners & CORNER_TR))
	{
		SetColor(ColorTopRight);
		ItemQ = CQuadItem(x + w, y, -r, r);
		QuadsDrawTL(&ItemQ, 1);
	}

	if(!(Corners & CORNER_BL))
	{
		SetColor(ColorBottomLeft);
		ItemQ = CQuadItem(x, y + h, r, -r);
		QuadsDrawTL(&ItemQ, 1);
	}

	if(!(Corners & CORNER_BR))
	{
		SetColor(ColorBottomRight);
		ItemQ = CQuadItem(x + w, y + h, -r, -r);
		QuadsDrawTL(&ItemQ, 1);
	}
}

int CGraphics_Threaded::CreateRectQuadContainer(float x, float y, float w, float h, float r, int Corners)
{
	int ContainerIndex = CreateQuadContainer(false);

	if(Corners == 0 || r == 0.0f)
	{
		CQuadItem ItemQ = CQuadItem(x, y, w, h);
		QuadContainerAddQuads(ContainerIndex, &ItemQ, 1);
		QuadContainerUpload(ContainerIndex);
		QuadContainerChangeAutomaticUpload(ContainerIndex, true);
		return ContainerIndex;
	}

	const int NumSegments = 8;
	const float SegmentsAngle = pi / 2 / NumSegments;
	IGraphics::CFreeformItem aFreeform[NumSegments * 4];
	size_t NumItems = 0;

	for(int i = 0; i < NumSegments; i += 2)
	{
		float a1 = i * SegmentsAngle;
		float a2 = (i + 1) * SegmentsAngle;
		float a3 = (i + 2) * SegmentsAngle;
		float Ca1 = std::cos(a1);
		float Ca2 = std::cos(a2);
		float Ca3 = std::cos(a3);
		float Sa1 = std::sin(a1);
		float Sa2 = std::sin(a2);
		float Sa3 = std::sin(a3);

		if(Corners & CORNER_TL)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + r, y + r,
				x + (1 - Ca1) * r, y + (1 - Sa1) * r,
				x + (1 - Ca3) * r, y + (1 - Sa3) * r,
				x + (1 - Ca2) * r, y + (1 - Sa2) * r);

		if(Corners & CORNER_TR)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + w - r, y + r,
				x + w - r + Ca1 * r, y + (1 - Sa1) * r,
				x + w - r + Ca3 * r, y + (1 - Sa3) * r,
				x + w - r + Ca2 * r, y + (1 - Sa2) * r);

		if(Corners & CORNER_BL)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + r, y + h - r,
				x + (1 - Ca1) * r, y + h - r + Sa1 * r,
				x + (1 - Ca3) * r, y + h - r + Sa3 * r,
				x + (1 - Ca2) * r, y + h - r + Sa2 * r);

		if(Corners & CORNER_BR)
			aFreeform[NumItems++] = IGraphics::CFreeformItem(
				x + w - r, y + h - r,
				x + w - r + Ca1 * r, y + h - r + Sa1 * r,
				x + w - r + Ca3 * r, y + h - r + Sa3 * r,
				x + w - r + Ca2 * r, y + h - r + Sa2 * r);
	}

	if(NumItems > 0)
		QuadContainerAddQuads(ContainerIndex, aFreeform, NumItems);

	CQuadItem aQuads[9];
	NumItems = 0;
	aQuads[NumItems++] = CQuadItem(x + r, y + r, w - r * 2, h - r * 2); // center
	aQuads[NumItems++] = CQuadItem(x + r, y, w - r * 2, r); // top
	aQuads[NumItems++] = CQuadItem(x + r, y + h - r, w - r * 2, r); // bottom
	aQuads[NumItems++] = CQuadItem(x, y + r, r, h - r * 2); // left
	aQuads[NumItems++] = CQuadItem(x + w - r, y + r, r, h - r * 2); // right

	if(!(Corners & CORNER_TL))
		aQuads[NumItems++] = CQuadItem(x, y, r, r);
	if(!(Corners & CORNER_TR))
		aQuads[NumItems++] = CQuadItem(x + w, y, -r, r);
	if(!(Corners & CORNER_BL))
		aQuads[NumItems++] = CQuadItem(x, y + h, r, -r);
	if(!(Corners & CORNER_BR))
		aQuads[NumItems++] = CQuadItem(x + w, y + h, -r, -r);

	if(NumItems > 0)
		QuadContainerAddQuads(ContainerIndex, aQuads, NumItems);

	QuadContainerUpload(ContainerIndex);
	QuadContainerChangeAutomaticUpload(ContainerIndex, true);

	return ContainerIndex;
}

void CGraphics_Threaded::DrawRect(float x, float y, float w, float h, ColorRGBA Color, int Corners, float Rounding)
{
	TextureClear();
	QuadsBegin();
	SetColor(Color);
	DrawRectExt(x, y, w, h, Rounding, Corners);
	QuadsEnd();
}

void CGraphics_Threaded::DrawRect4(float x, float y, float w, float h, ColorRGBA ColorTopLeft, ColorRGBA ColorTopRight, ColorRGBA ColorBottomLeft, ColorRGBA ColorBottomRight, int Corners, float Rounding)
{
	TextureClear();
	QuadsBegin();
	DrawRectExt4(x, y, w, h, ColorTopLeft, ColorTopRight, ColorBottomLeft, ColorBottomRight, Rounding, Corners);
	QuadsEnd();
}

void CGraphics_Threaded::DrawCircle(float CenterX, float CenterY, float Radius, int Segments)
{
	IGraphics::CFreeformItem aItems[32];
	size_t NumItems = 0;
	const float SegmentsAngle = 2 * pi / Segments;
	for(int i = 0; i < Segments; i += 2)
	{
		const float a1 = i * SegmentsAngle;
		const float a2 = (i + 1) * SegmentsAngle;
		const float a3 = (i + 2) * SegmentsAngle;
		aItems[NumItems++] = IGraphics::CFreeformItem(
			CenterX, CenterY,
			CenterX + std::cos(a1) * Radius, CenterY + std::sin(a1) * Radius,
			CenterX + std::cos(a3) * Radius, CenterY + std::sin(a3) * Radius,
			CenterX + std::cos(a2) * Radius, CenterY + std::sin(a2) * Radius);
		if(NumItems == std::size(aItems))
		{
			QuadsDrawFreeform(aItems, std::size(aItems));
			NumItems = 0;
		}
	}
	if(NumItems)
		QuadsDrawFreeform(aItems, NumItems);
}

void CGraphics_Threaded::RenderTileLayer(int BufferContainerIndex, const ColorRGBA &Color, char **pOffsets, unsigned int *pIndicedVertexDrawNum, size_t NumIndicesOffset)
{
	if(NumIndicesOffset == 0)
		return;

	// add the VertexArrays and draw
	CCommandBuffer::SCommand_RenderTileLayer Cmd;
	Cmd.m_State = m_State;
	Cmd.m_IndicesDrawNum = NumIndicesOffset;
	Cmd.m_BufferContainerIndex = BufferContainerIndex;
	Cmd.m_Color = Color;

	void *pData = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffset);
	if(pData == nullptr)
	{
		// kick command buffer and try again
		KickCommandBuffer();

		pData = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffset);
		if(pData == nullptr)
		{
			log_error("graphics", "Failed to allocate data for tile layer vertices. NumIndicesOffset=%" PRIzu, NumIndicesOffset);
			return;
		}
	}
	Cmd.m_pIndicesOffsets = (char **)pData;
	Cmd.m_pDrawCount = (unsigned int *)(((char *)pData) + (sizeof(char *) * NumIndicesOffset));

	AddCmd(Cmd, [&] {
		pData = m_pCommandBuffer->AllocData((sizeof(char *) + sizeof(unsigned int)) * NumIndicesOffset);
		if(pData == nullptr)
			return false;
		Cmd.m_pIndicesOffsets = (char **)pData;
		Cmd.m_pDrawCount = (unsigned int *)(((char *)pData) + (sizeof(char *) * NumIndicesOffset));
		return true;
	});

	mem_copy(Cmd.m_pIndicesOffsets, pOffsets, sizeof(char *) * NumIndicesOffset);
	mem_copy(Cmd.m_pDrawCount, pIndicedVertexDrawNum, sizeof(unsigned int) * NumIndicesOffset);

	m_pCommandBuffer->AddRenderCalls(NumIndicesOffset);
	// todo max indices group check!!
}

void CGraphics_Threaded::RenderBorderTiles(int BufferContainerIndex, const ColorRGBA &Color, char *pIndexBufferOffset, const vec2 &Offset, const vec2 &Scale, uint32_t DrawNum)
{
	if(DrawNum == 0)
		return;
	// Draw a border tile a lot of times
	CCommandBuffer::SCommand_RenderBorderTile Cmd;
	Cmd.m_State = m_State;
	Cmd.m_DrawNum = DrawNum;
	Cmd.m_BufferContainerIndex = BufferContainerIndex;
	Cmd.m_Color = Color;

	Cmd.m_pIndicesOffset = pIndexBufferOffset;

	Cmd.m_Offset = Offset;
	Cmd.m_Scale = Scale;

	AddCmd(Cmd);

	m_pCommandBuffer->AddRenderCalls(1);
}

void CGraphics_Threaded::RenderQuadLayer(int BufferContainerIndex, SQuadRenderInfo *pQuadInfo, size_t QuadNum, int QuadOffset, bool Grouped)
{
	if(QuadNum == 0)
		return;

	// add the VertexArrays and draw
	CCommandBuffer::SCommand_RenderQuadLayer Cmd(Grouped);
	Cmd.m_State = m_State;
	Cmd.m_QuadNum = QuadNum;
	Cmd.m_QuadOffset = QuadOffset;
	Cmd.m_BufferContainerIndex = BufferContainerIndex;
	if(!Grouped)
	{
		Cmd.m_pQuadInfo = (SQuadRenderInfo *)AllocCommandBufferData(Cmd.m_QuadNum * sizeof(SQuadRenderInfo));
		AddCmd(Cmd, [&] {
			Cmd.m_pQuadInfo = (SQuadRenderInfo *)m_pCommandBuffer->AllocData(QuadNum * sizeof(SQuadRenderInfo));
			return Cmd.m_pQuadInfo != nullptr;
		});

		mem_copy(Cmd.m_pQuadInfo, pQuadInfo, sizeof(SQuadRenderInfo) * QuadNum);
		m_pCommandBuffer->AddRenderCalls(((QuadNum - 1) / GRAPHICS_MAX_QUADS_RENDER_COUNT) + 1);
	}
	else
	{
		Cmd.m_pQuadInfo = (SQuadRenderInfo *)AllocCommandBufferData(sizeof(SQuadRenderInfo));
		AddCmd(Cmd, [&] {
			Cmd.m_pQuadInfo = (SQuadRenderInfo *)m_pCommandBuffer->AllocData(sizeof(SQuadRenderInfo));
			return Cmd.m_pQuadInfo != nullptr;
		});

		*Cmd.m_pQuadInfo = *pQuadInfo;
		m_pCommandBuffer->AddRenderCalls(1);
	}
}

void CGraphics_Threaded::RenderText(int BufferContainerIndex, int TextQuadNum, int TextureSize, int TextureTextIndex, int TextureTextOutlineIndex, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor)
{
	if(BufferContainerIndex == -1)
		return;

	CCommandBuffer::SCommand_RenderText Cmd;
	Cmd.m_State = m_State;
	Cmd.m_BufferContainerIndex = BufferContainerIndex;
	Cmd.m_DrawNum = TextQuadNum * 6;
	Cmd.m_TextureSize = TextureSize;
	Cmd.m_TextTextureIndex = TextureTextIndex;
	Cmd.m_TextOutlineTextureIndex = TextureTextOutlineIndex;
	Cmd.m_TextColor = TextColor;
	Cmd.m_TextOutlineColor = TextOutlineColor;

	AddCmd(Cmd);

	m_pCommandBuffer->AddRenderCalls(1);
}

int CGraphics_Threaded::CreateQuadContainer(bool AutomaticUpload)
{
	int Index = -1;
	if(m_FirstFreeQuadContainer == -1)
	{
		Index = m_vQuadContainers.size();
		m_vQuadContainers.emplace_back(AutomaticUpload);
	}
	else
	{
		Index = m_FirstFreeQuadContainer;
		m_FirstFreeQuadContainer = m_vQuadContainers[Index].m_FreeIndex;
		m_vQuadContainers[Index].m_FreeIndex = Index;
	}

	return Index;
}

void CGraphics_Threaded::QuadContainerChangeAutomaticUpload(int ContainerIndex, bool AutomaticUpload)
{
	SQuadContainer &Container = m_vQuadContainers[ContainerIndex];
	Container.m_AutomaticUpload = AutomaticUpload;
}

void CGraphics_Threaded::QuadContainerUpload(int ContainerIndex)
{
	if(IsQuadContainerBufferingEnabled())
	{
		SQuadContainer &Container = m_vQuadContainers[ContainerIndex];
		if(!Container.m_vQuads.empty())
		{
			if(Container.m_QuadBufferObjectIndex == -1)
			{
				size_t UploadDataSize = Container.m_vQuads.size() * sizeof(SQuadContainer::SQuad);
				Container.m_QuadBufferObjectIndex = CreateBufferObject(UploadDataSize, Container.m_vQuads.data(), 0);
			}
			else
			{
				size_t UploadDataSize = Container.m_vQuads.size() * sizeof(SQuadContainer::SQuad);
				RecreateBufferObject(Container.m_QuadBufferObjectIndex, UploadDataSize, Container.m_vQuads.data(), 0);
			}

			if(Container.m_QuadBufferContainerIndex == -1)
			{
				SBufferContainerInfo Info;
				Info.m_Stride = sizeof(CCommandBuffer::SVertex);
				Info.m_VertBufferBindingIndex = Container.m_QuadBufferObjectIndex;

				Info.m_vAttributes.emplace_back();
				SBufferContainerInfo::SAttribute *pAttr = &Info.m_vAttributes.back();
				pAttr->m_DataTypeCount = 2;
				pAttr->m_FuncType = 0;
				pAttr->m_Normalized = false;
				pAttr->m_pOffset = nullptr;
				pAttr->m_Type = GRAPHICS_TYPE_FLOAT;
				Info.m_vAttributes.emplace_back();
				pAttr = &Info.m_vAttributes.back();
				pAttr->m_DataTypeCount = 2;
				pAttr->m_FuncType = 0;
				pAttr->m_Normalized = false;
				pAttr->m_pOffset = (void *)(sizeof(float) * 2);
				pAttr->m_Type = GRAPHICS_TYPE_FLOAT;
				Info.m_vAttributes.emplace_back();
				pAttr = &Info.m_vAttributes.back();
				pAttr->m_DataTypeCount = 4;
				pAttr->m_FuncType = 0;
				pAttr->m_Normalized = true;
				pAttr->m_pOffset = (void *)(sizeof(float) * 2 + sizeof(float) * 2);
				pAttr->m_Type = GRAPHICS_TYPE_UNSIGNED_BYTE;

				Container.m_QuadBufferContainerIndex = CreateBufferContainer(&Info);
			}
		}
	}
}

int CGraphics_Threaded::QuadContainerAddQuads(int ContainerIndex, CQuadItem *pArray, int Num)
{
	SQuadContainer &Container = m_vQuadContainers[ContainerIndex];

	if((int)Container.m_vQuads.size() > Num + CCommandBuffer::CCommandBuffer::MAX_VERTICES)
		return -1;

	int RetOff = (int)Container.m_vQuads.size();

	for(int i = 0; i < Num; ++i)
	{
		Container.m_vQuads.emplace_back();
		SQuadContainer::SQuad &Quad = Container.m_vQuads.back();

		Quad.m_aVertices[0].m_Pos.x = pArray[i].m_X;
		Quad.m_aVertices[0].m_Pos.y = pArray[i].m_Y;
		Quad.m_aVertices[0].m_Tex = m_aTexture[0];
		SetColor(&Quad.m_aVertices[0], 0);

		Quad.m_aVertices[1].m_Pos.x = pArray[i].m_X + pArray[i].m_Width;
		Quad.m_aVertices[1].m_Pos.y = pArray[i].m_Y;
		Quad.m_aVertices[1].m_Tex = m_aTexture[1];
		SetColor(&Quad.m_aVertices[1], 1);

		Quad.m_aVertices[2].m_Pos.x = pArray[i].m_X + pArray[i].m_Width;
		Quad.m_aVertices[2].m_Pos.y = pArray[i].m_Y + pArray[i].m_Height;
		Quad.m_aVertices[2].m_Tex = m_aTexture[2];
		SetColor(&Quad.m_aVertices[2], 2);

		Quad.m_aVertices[3].m_Pos.x = pArray[i].m_X;
		Quad.m_aVertices[3].m_Pos.y = pArray[i].m_Y + pArray[i].m_Height;
		Quad.m_aVertices[3].m_Tex = m_aTexture[3];
		SetColor(&Quad.m_aVertices[3], 3);

		if(m_Rotation != 0)
		{
			CCommandBuffer::SPoint Center;
			Center.x = pArray[i].m_X + pArray[i].m_Width / 2;
			Center.y = pArray[i].m_Y + pArray[i].m_Height / 2;

			Rotate(Center, Quad.m_aVertices, 4);
		}
	}

	if(Container.m_AutomaticUpload)
		QuadContainerUpload(ContainerIndex);

	return RetOff;
}

int CGraphics_Threaded::QuadContainerAddQuads(int ContainerIndex, CFreeformItem *pArray, int Num)
{
	SQuadContainer &Container = m_vQuadContainers[ContainerIndex];

	if((int)Container.m_vQuads.size() > Num + CCommandBuffer::CCommandBuffer::MAX_VERTICES)
		return -1;

	int RetOff = (int)Container.m_vQuads.size();

	for(int i = 0; i < Num; ++i)
	{
		Container.m_vQuads.emplace_back();
		SQuadContainer::SQuad &Quad = Container.m_vQuads.back();

		Quad.m_aVertices[0].m_Pos.x = pArray[i].m_X0;
		Quad.m_aVertices[0].m_Pos.y = pArray[i].m_Y0;
		Quad.m_aVertices[0].m_Tex = m_aTexture[0];
		SetColor(&Quad.m_aVertices[0], 0);

		Quad.m_aVertices[1].m_Pos.x = pArray[i].m_X1;
		Quad.m_aVertices[1].m_Pos.y = pArray[i].m_Y1;
		Quad.m_aVertices[1].m_Tex = m_aTexture[1];
		SetColor(&Quad.m_aVertices[1], 1);

		Quad.m_aVertices[2].m_Pos.x = pArray[i].m_X3;
		Quad.m_aVertices[2].m_Pos.y = pArray[i].m_Y3;
		Quad.m_aVertices[2].m_Tex = m_aTexture[3];
		SetColor(&Quad.m_aVertices[2], 3);

		Quad.m_aVertices[3].m_Pos.x = pArray[i].m_X2;
		Quad.m_aVertices[3].m_Pos.y = pArray[i].m_Y2;
		Quad.m_aVertices[3].m_Tex = m_aTexture[2];
		SetColor(&Quad.m_aVertices[3], 2);
	}

	if(Container.m_AutomaticUpload)
		QuadContainerUpload(ContainerIndex);

	return RetOff;
}

void CGraphics_Threaded::QuadContainerReset(int ContainerIndex)
{
	if(ContainerIndex == -1)
		return;

	SQuadContainer &Container = m_vQuadContainers[ContainerIndex];
	if(IsQuadContainerBufferingEnabled())
		DeleteBufferContainer(Container.m_QuadBufferContainerIndex, true);
	Container.m_vQuads.clear();
	Container.m_QuadBufferObjectIndex = -1;
}

void CGraphics_Threaded::DeleteQuadContainer(int &ContainerIndex)
{
	if(ContainerIndex == -1)
		return;

	QuadContainerReset(ContainerIndex);

	// also clear the container index
	m_vQuadContainers[ContainerIndex].m_FreeIndex = m_FirstFreeQuadContainer;
	m_FirstFreeQuadContainer = ContainerIndex;
	ContainerIndex = -1;
}

void CGraphics_Threaded::RenderQuadContainer(int ContainerIndex, int QuadDrawNum)
{
	RenderQuadContainer(ContainerIndex, 0, QuadDrawNum);
}

void CGraphics_Threaded::RenderQuadContainer(int ContainerIndex, int QuadOffset, int QuadDrawNum, bool ChangeWrapMode)
{
	SQuadContainer &Container = m_vQuadContainers[ContainerIndex];

	if(QuadDrawNum == -1)
		QuadDrawNum = (int)Container.m_vQuads.size() - QuadOffset;

	if((int)Container.m_vQuads.size() < QuadOffset + QuadDrawNum || QuadDrawNum == 0)
		return;

	if(IsQuadContainerBufferingEnabled())
	{
		if(Container.m_QuadBufferContainerIndex == -1)
			return;

		if(ChangeWrapMode)
			WrapClamp();

		CCommandBuffer::SCommand_RenderQuadContainer Cmd;
		Cmd.m_State = m_State;
		Cmd.m_DrawNum = (unsigned int)QuadDrawNum * 6;
		Cmd.m_pOffset = (void *)(QuadOffset * 6 * sizeof(unsigned int));
		Cmd.m_BufferContainerIndex = Container.m_QuadBufferContainerIndex;

		AddCmd(Cmd);
		m_pCommandBuffer->AddRenderCalls(1);
	}
	else
	{
		if(g_Config.m_GfxQuadAsTriangle)
		{
			for(int i = 0; i < QuadDrawNum; ++i)
			{
				SQuadContainer::SQuad &Quad = Container.m_vQuads[QuadOffset + i];
				m_aVertices[i * 6] = Quad.m_aVertices[0];
				m_aVertices[i * 6 + 1] = Quad.m_aVertices[1];
				m_aVertices[i * 6 + 2] = Quad.m_aVertices[2];
				m_aVertices[i * 6 + 3] = Quad.m_aVertices[0];
				m_aVertices[i * 6 + 4] = Quad.m_aVertices[2];
				m_aVertices[i * 6 + 5] = Quad.m_aVertices[3];
				m_NumVertices += 6;
			}
		}
		else
		{
			mem_copy(m_aVertices, &Container.m_vQuads[QuadOffset], sizeof(CCommandBuffer::SVertex) * 4 * QuadDrawNum);
			m_NumVertices += 4 * QuadDrawNum;
		}
		m_Drawing = EDrawing::QUADS;
		if(ChangeWrapMode)
			WrapClamp();
		FlushVertices(false);
		m_Drawing = EDrawing::NONE;
	}
	WrapNormal();
}

void CGraphics_Threaded::RenderQuadContainerEx(int ContainerIndex, int QuadOffset, int QuadDrawNum, float X, float Y, float ScaleX, float ScaleY)
{
	SQuadContainer &Container = m_vQuadContainers[ContainerIndex];

	if((int)Container.m_vQuads.size() < QuadOffset + 1)
		return;

	if(QuadDrawNum == -1)
		QuadDrawNum = (int)Container.m_vQuads.size() - QuadOffset;

	if(IsQuadContainerBufferingEnabled())
	{
		if(Container.m_QuadBufferContainerIndex == -1)
			return;

		SQuadContainer::SQuad &Quad = Container.m_vQuads[QuadOffset];
		CCommandBuffer::SCommand_RenderQuadContainerEx Cmd;

		WrapClamp();

		CScreenRect ScreenRect = GetScreen();
		CScreenRect CommandScreenRect = ScreenRect.Move({-X, -Y});
		CommandScreenRect.m_TopLeft /= vec2(ScaleX, ScaleY);
		CommandScreenRect.m_BottomRight /= vec2(ScaleX, ScaleY);
		Cmd.m_State = m_State;
		Cmd.m_State.m_ScreenTL = CommandScreenRect.m_TopLeft;
		Cmd.m_State.m_ScreenBR = CommandScreenRect.m_BottomRight;

		Cmd.m_DrawNum = QuadDrawNum * 6;
		Cmd.m_pOffset = (void *)(QuadOffset * 6 * sizeof(unsigned int));
		Cmd.m_BufferContainerIndex = Container.m_QuadBufferContainerIndex;

		Cmd.m_VertexColor.r = (float)m_aColor[0].r / 255.f;
		Cmd.m_VertexColor.g = (float)m_aColor[0].g / 255.f;
		Cmd.m_VertexColor.b = (float)m_aColor[0].b / 255.f;
		Cmd.m_VertexColor.a = (float)m_aColor[0].a / 255.f;

		Cmd.m_Rotation = m_Rotation;

		// rotate before positioning
		Cmd.m_Center.x = Quad.m_aVertices[0].m_Pos.x + (Quad.m_aVertices[1].m_Pos.x - Quad.m_aVertices[0].m_Pos.x) / 2.f;
		Cmd.m_Center.y = Quad.m_aVertices[0].m_Pos.y + (Quad.m_aVertices[2].m_Pos.y - Quad.m_aVertices[0].m_Pos.y) / 2.f;

		AddCmd(Cmd);
		m_pCommandBuffer->AddRenderCalls(1);
	}
	else
	{
		if(g_Config.m_GfxQuadAsTriangle)
		{
			for(int i = 0; i < QuadDrawNum; ++i)
			{
				SQuadContainer::SQuad &Quad = Container.m_vQuads[QuadOffset + i];
				m_aVertices[i * 6 + 0] = Quad.m_aVertices[0];
				m_aVertices[i * 6 + 1] = Quad.m_aVertices[1];
				m_aVertices[i * 6 + 2] = Quad.m_aVertices[2];
				m_aVertices[i * 6 + 3] = Quad.m_aVertices[0];
				m_aVertices[i * 6 + 4] = Quad.m_aVertices[2];
				m_aVertices[i * 6 + 5] = Quad.m_aVertices[3];

				for(int n = 0; n < 6; ++n)
				{
					m_aVertices[i * 6 + n].m_Pos.x *= ScaleX;
					m_aVertices[i * 6 + n].m_Pos.y *= ScaleY;

					SetColor(&m_aVertices[i * 6 + n], 0);
				}

				if(m_Rotation != 0)
				{
					CCommandBuffer::SPoint Center;
					Center.x = m_aVertices[i * 6 + 0].m_Pos.x + (m_aVertices[i * 6 + 1].m_Pos.x - m_aVertices[i * 6 + 0].m_Pos.x) / 2.f;
					Center.y = m_aVertices[i * 6 + 0].m_Pos.y + (m_aVertices[i * 6 + 2].m_Pos.y - m_aVertices[i * 6 + 0].m_Pos.y) / 2.f;

					Rotate(Center, &m_aVertices[i * 6 + 0], 6);
				}

				for(int n = 0; n < 6; ++n)
				{
					m_aVertices[i * 6 + n].m_Pos.x += X;
					m_aVertices[i * 6 + n].m_Pos.y += Y;
				}
				m_NumVertices += 6;
			}
		}
		else
		{
			mem_copy(m_aVertices, &Container.m_vQuads[QuadOffset], sizeof(CCommandBuffer::SVertex) * 4 * QuadDrawNum);
			for(int i = 0; i < QuadDrawNum; ++i)
			{
				for(int n = 0; n < 4; ++n)
				{
					m_aVertices[i * 4 + n].m_Pos.x *= ScaleX;
					m_aVertices[i * 4 + n].m_Pos.y *= ScaleY;
					SetColor(&m_aVertices[i * 4 + n], 0);
				}

				if(m_Rotation != 0)
				{
					CCommandBuffer::SPoint Center;
					Center.x = m_aVertices[i * 4 + 0].m_Pos.x + (m_aVertices[i * 4 + 1].m_Pos.x - m_aVertices[i * 4 + 0].m_Pos.x) / 2.f;
					Center.y = m_aVertices[i * 4 + 0].m_Pos.y + (m_aVertices[i * 4 + 2].m_Pos.y - m_aVertices[i * 4 + 0].m_Pos.y) / 2.f;

					Rotate(Center, &m_aVertices[i * 4 + 0], 4);
				}

				for(int n = 0; n < 4; ++n)
				{
					m_aVertices[i * 4 + n].m_Pos.x += X;
					m_aVertices[i * 4 + n].m_Pos.y += Y;
				}
				m_NumVertices += 4;
			}
		}
		m_Drawing = EDrawing::QUADS;
		WrapClamp();
		FlushVertices(false);
		m_Drawing = EDrawing::NONE;
	}
	WrapNormal();
}

void CGraphics_Threaded::RenderQuadContainerAsSprite(int ContainerIndex, int QuadOffset, float X, float Y, float ScaleX, float ScaleY)
{
	RenderQuadContainerEx(ContainerIndex, QuadOffset, 1, X, Y, ScaleX, ScaleY);
}

void CGraphics_Threaded::RenderQuadContainerAsSpriteMultiple(int ContainerIndex, int QuadOffset, int DrawCount, SRenderSpriteInfo *pRenderInfo)
{
	SQuadContainer &Container = m_vQuadContainers[ContainerIndex];

	if(DrawCount == 0)
		return;

	if(IsQuadContainerBufferingEnabled())
	{
		if(Container.m_QuadBufferContainerIndex == -1)
			return;

		WrapClamp();
		SQuadContainer::SQuad &Quad = Container.m_vQuads[0];
		CCommandBuffer::SCommand_RenderQuadContainerAsSpriteMultiple Cmd;

		Cmd.m_State = m_State;

		Cmd.m_DrawNum = 1 * 6;
		Cmd.m_DrawCount = DrawCount;
		Cmd.m_pOffset = (void *)(QuadOffset * 6 * sizeof(unsigned int));
		Cmd.m_BufferContainerIndex = Container.m_QuadBufferContainerIndex;

		Cmd.m_VertexColor.r = (float)m_aColor[0].r / 255.f;
		Cmd.m_VertexColor.g = (float)m_aColor[0].g / 255.f;
		Cmd.m_VertexColor.b = (float)m_aColor[0].b / 255.f;
		Cmd.m_VertexColor.a = (float)m_aColor[0].a / 255.f;

		// rotate before positioning
		Cmd.m_Center.x = Quad.m_aVertices[0].m_Pos.x + (Quad.m_aVertices[1].m_Pos.x - Quad.m_aVertices[0].m_Pos.x) / 2.f;
		Cmd.m_Center.y = Quad.m_aVertices[0].m_Pos.y + (Quad.m_aVertices[2].m_Pos.y - Quad.m_aVertices[0].m_Pos.y) / 2.f;

		Cmd.m_pRenderInfo = (IGraphics::SRenderSpriteInfo *)m_pCommandBuffer->AllocData(sizeof(IGraphics::SRenderSpriteInfo) * DrawCount);
		if(Cmd.m_pRenderInfo == nullptr)
		{
			// kick command buffer and try again
			KickCommandBuffer();

			Cmd.m_pRenderInfo = (IGraphics::SRenderSpriteInfo *)m_pCommandBuffer->AllocData(sizeof(IGraphics::SRenderSpriteInfo) * DrawCount);
			if(Cmd.m_pRenderInfo == nullptr)
			{
				log_error("graphics", "Failed to allocate data for quad container render info. DrawCount=%d", DrawCount);
				return;
			}
		}

		AddCmd(Cmd, [&] {
			Cmd.m_pRenderInfo = (IGraphics::SRenderSpriteInfo *)m_pCommandBuffer->AllocData(sizeof(IGraphics::SRenderSpriteInfo) * DrawCount);
			return Cmd.m_pRenderInfo != nullptr;
		});

		mem_copy(Cmd.m_pRenderInfo, pRenderInfo, sizeof(IGraphics::SRenderSpriteInfo) * DrawCount);

		m_pCommandBuffer->AddRenderCalls(((DrawCount - 1) / GRAPHICS_MAX_QUADS_RENDER_COUNT) + 1);

		WrapNormal();
	}
	else
	{
		for(int i = 0; i < DrawCount; ++i)
		{
			QuadsSetRotation(pRenderInfo[i].m_Rotation);
			RenderQuadContainerAsSprite(ContainerIndex, QuadOffset, pRenderInfo[i].m_Pos.x, pRenderInfo[i].m_Pos.y, pRenderInfo[i].m_Scale, pRenderInfo[i].m_Scale);
		}
	}
}

void *CGraphics_Threaded::AllocCommandBufferData(size_t AllocSize)
{
	void *pData = m_pCommandBuffer->AllocData(AllocSize);
	if(pData == nullptr)
	{
		// kick command buffer and try again
		KickCommandBuffer();

		pData = m_pCommandBuffer->AllocData(AllocSize);
		dbg_assert(pData, "graphics: failed to allocate data (size %" PRIzu ") for command buffer", AllocSize);
	}
	return pData;
}

int CGraphics_Threaded::CreateBufferObject(size_t UploadDataSize, void *pUploadData, int CreateFlags, bool IsMovedPointer)
{
	int Index = -1;
	if(m_FirstFreeBufferObjectIndex == -1)
	{
		Index = m_vBufferObjectIndices.size();
		m_vBufferObjectIndices.push_back(Index);
	}
	else
	{
		Index = m_FirstFreeBufferObjectIndex;
		m_FirstFreeBufferObjectIndex = m_vBufferObjectIndices[Index];
		m_vBufferObjectIndices[Index] = Index;
	}

	dbg_assert((CreateFlags & EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT) == 0 || (UploadDataSize <= CCommandBuffer::MAX_VERTICES * std::max(sizeof(CCommandBuffer::SVertexTex3DStream), sizeof(CCommandBuffer::SVertexTex3D))),
		"If BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT is used, then the buffer size must not exceed max(sizeof(CCommandBuffer::SVertexTex3DStream), sizeof(CCommandBuffer::SVertexTex3D)) * CCommandBuffer::MAX_VERTICES");

	CCommandBuffer::SCommand_CreateBufferObject Cmd;
	Cmd.m_BufferIndex = Index;
	Cmd.m_DataSize = UploadDataSize;
	Cmd.m_DeletePointer = IsMovedPointer;
	Cmd.m_Flags = CreateFlags;

	if(IsMovedPointer)
	{
		Cmd.m_pUploadData = pUploadData;
		AddCmd(Cmd);
	}
	else
	{
		if(UploadDataSize <= CMD_BUFFER_DATA_BUFFER_SIZE)
		{
			Cmd.m_pUploadData = AllocCommandBufferData(UploadDataSize);

			AddCmd(Cmd, [&] {
				Cmd.m_pUploadData = m_pCommandBuffer->AllocData(UploadDataSize);
				return Cmd.m_pUploadData != nullptr;
			});
			mem_copy(Cmd.m_pUploadData, pUploadData, UploadDataSize);
		}
		else
		{
			Cmd.m_pUploadData = nullptr;
			AddCmd(Cmd);

			// update the buffer instead
			size_t UploadDataOffset = 0;
			while(UploadDataSize > 0)
			{
				size_t UpdateSize = (UploadDataSize > CMD_BUFFER_DATA_BUFFER_SIZE ? CMD_BUFFER_DATA_BUFFER_SIZE : UploadDataSize);

				UpdateBufferObjectInternal(Index, UpdateSize, (((char *)pUploadData) + UploadDataOffset), (void *)UploadDataOffset);

				UploadDataOffset += UpdateSize;
				UploadDataSize -= UpdateSize;
			}
		}
	}

	return Index;
}

void CGraphics_Threaded::RecreateBufferObject(int BufferIndex, size_t UploadDataSize, void *pUploadData, int CreateFlags, bool IsMovedPointer)
{
	CCommandBuffer::SCommand_RecreateBufferObject Cmd;
	Cmd.m_BufferIndex = BufferIndex;
	Cmd.m_DataSize = UploadDataSize;
	Cmd.m_DeletePointer = IsMovedPointer;
	Cmd.m_Flags = CreateFlags;

	dbg_assert((CreateFlags & EBufferObjectCreateFlags::BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT) == 0 || (UploadDataSize <= CCommandBuffer::MAX_VERTICES * std::max(sizeof(CCommandBuffer::SVertexTex3DStream), sizeof(CCommandBuffer::SVertexTex3D))),
		"If BUFFER_OBJECT_CREATE_FLAGS_ONE_TIME_USE_BIT is used, then the buffer size must not exceed max(sizeof(CCommandBuffer::SVertexTex3DStream), sizeof(CCommandBuffer::SVertexTex3D)) * CCommandBuffer::MAX_VERTICES");

	if(IsMovedPointer)
	{
		Cmd.m_pUploadData = pUploadData;
		AddCmd(Cmd);
	}
	else
	{
		if(UploadDataSize <= CMD_BUFFER_DATA_BUFFER_SIZE)
		{
			Cmd.m_pUploadData = AllocCommandBufferData(UploadDataSize);

			AddCmd(Cmd, [&] {
				Cmd.m_pUploadData = m_pCommandBuffer->AllocData(UploadDataSize);
				return Cmd.m_pUploadData != nullptr;
			});

			mem_copy(Cmd.m_pUploadData, pUploadData, UploadDataSize);
		}
		else
		{
			Cmd.m_pUploadData = nullptr;
			AddCmd(Cmd);

			// update the buffer instead
			size_t UploadDataOffset = 0;
			while(UploadDataSize > 0)
			{
				size_t UpdateSize = (UploadDataSize > CMD_BUFFER_DATA_BUFFER_SIZE ? CMD_BUFFER_DATA_BUFFER_SIZE : UploadDataSize);

				UpdateBufferObjectInternal(BufferIndex, UpdateSize, (((char *)pUploadData) + UploadDataOffset), (void *)UploadDataOffset);

				UploadDataOffset += UpdateSize;
				UploadDataSize -= UpdateSize;
			}
		}
	}
}

void CGraphics_Threaded::UpdateBufferObjectInternal(int BufferIndex, size_t UploadDataSize, void *pUploadData, void *pOffset, bool IsMovedPointer)
{
	CCommandBuffer::SCommand_UpdateBufferObject Cmd;
	Cmd.m_BufferIndex = BufferIndex;
	Cmd.m_DataSize = UploadDataSize;
	Cmd.m_pOffset = pOffset;
	Cmd.m_DeletePointer = IsMovedPointer;

	if(IsMovedPointer)
	{
		Cmd.m_pUploadData = pUploadData;
		AddCmd(Cmd);
	}
	else
	{
		Cmd.m_pUploadData = AllocCommandBufferData(UploadDataSize);

		AddCmd(Cmd, [&] {
			Cmd.m_pUploadData = m_pCommandBuffer->AllocData(UploadDataSize);
			return Cmd.m_pUploadData != nullptr;
		});

		mem_copy(Cmd.m_pUploadData, pUploadData, UploadDataSize);
	}
}

void CGraphics_Threaded::CopyBufferObjectInternal(int WriteBufferIndex, int ReadBufferIndex, size_t WriteOffset, size_t ReadOffset, size_t CopyDataSize)
{
	CCommandBuffer::SCommand_CopyBufferObject Cmd;
	Cmd.m_WriteBufferIndex = WriteBufferIndex;
	Cmd.m_ReadBufferIndex = ReadBufferIndex;
	Cmd.m_WriteOffset = WriteOffset;
	Cmd.m_ReadOffset = ReadOffset;
	Cmd.m_CopySize = CopyDataSize;
	AddCmd(Cmd);
}

void CGraphics_Threaded::DeleteBufferObject(int BufferIndex)
{
	if(BufferIndex == -1)
		return;

	CCommandBuffer::SCommand_DeleteBufferObject Cmd;
	Cmd.m_BufferIndex = BufferIndex;
	AddCmd(Cmd);

	// also clear the buffer object index
	m_vBufferObjectIndices[BufferIndex] = m_FirstFreeBufferObjectIndex;
	m_FirstFreeBufferObjectIndex = BufferIndex;
}

int CGraphics_Threaded::CreateBufferContainer(SBufferContainerInfo *pContainerInfo)
{
	int Index = -1;
	if(m_FirstFreeVertexArrayInfo == -1)
	{
		Index = m_vVertexArrayInfo.size();
		m_vVertexArrayInfo.emplace_back();
	}
	else
	{
		Index = m_FirstFreeVertexArrayInfo;
		m_FirstFreeVertexArrayInfo = m_vVertexArrayInfo[Index].m_FreeIndex;
		m_vVertexArrayInfo[Index].m_FreeIndex = Index;
	}

	CCommandBuffer::SCommand_CreateBufferContainer Cmd;
	Cmd.m_BufferContainerIndex = Index;
	Cmd.m_AttrCount = pContainerInfo->m_vAttributes.size();
	Cmd.m_Stride = pContainerInfo->m_Stride;
	Cmd.m_VertBufferBindingIndex = pContainerInfo->m_VertBufferBindingIndex;
	Cmd.m_pAttributes = (SBufferContainerInfo::SAttribute *)AllocCommandBufferData(Cmd.m_AttrCount * sizeof(SBufferContainerInfo::SAttribute));

	AddCmd(Cmd, [&] {
		Cmd.m_pAttributes = (SBufferContainerInfo::SAttribute *)m_pCommandBuffer->AllocData(Cmd.m_AttrCount * sizeof(SBufferContainerInfo::SAttribute));
		return Cmd.m_pAttributes != nullptr;
	});

	mem_copy(Cmd.m_pAttributes, pContainerInfo->m_vAttributes.data(), Cmd.m_AttrCount * sizeof(SBufferContainerInfo::SAttribute));

	m_vVertexArrayInfo[Index].m_AssociatedBufferObjectIndex = pContainerInfo->m_VertBufferBindingIndex;

	return Index;
}

void CGraphics_Threaded::DeleteBufferContainer(int &ContainerIndex, bool DestroyAllBO)
{
	if(ContainerIndex == -1)
		return;

	CCommandBuffer::SCommand_DeleteBufferContainer Cmd;
	Cmd.m_BufferContainerIndex = ContainerIndex;
	Cmd.m_DestroyAllBO = DestroyAllBO;
	AddCmd(Cmd);

	if(DestroyAllBO)
	{
		// delete all associated references
		int BufferObjectIndex = m_vVertexArrayInfo[ContainerIndex].m_AssociatedBufferObjectIndex;
		if(BufferObjectIndex != -1)
		{
			// clear the buffer object index
			m_vBufferObjectIndices[BufferObjectIndex] = m_FirstFreeBufferObjectIndex;
			m_FirstFreeBufferObjectIndex = BufferObjectIndex;
		}
	}
	m_vVertexArrayInfo[ContainerIndex].m_AssociatedBufferObjectIndex = -1;

	// also clear the buffer object index
	m_vVertexArrayInfo[ContainerIndex].m_FreeIndex = m_FirstFreeVertexArrayInfo;
	m_FirstFreeVertexArrayInfo = ContainerIndex;
	ContainerIndex = -1;
}

void CGraphics_Threaded::UpdateBufferContainerInternal(int ContainerIndex, SBufferContainerInfo *pContainerInfo)
{
	CCommandBuffer::SCommand_UpdateBufferContainer Cmd;
	Cmd.m_BufferContainerIndex = ContainerIndex;
	Cmd.m_AttrCount = pContainerInfo->m_vAttributes.size();
	Cmd.m_Stride = pContainerInfo->m_Stride;
	Cmd.m_VertBufferBindingIndex = pContainerInfo->m_VertBufferBindingIndex;
	Cmd.m_pAttributes = (SBufferContainerInfo::SAttribute *)AllocCommandBufferData(Cmd.m_AttrCount * sizeof(SBufferContainerInfo::SAttribute));

	AddCmd(Cmd, [&] {
		Cmd.m_pAttributes = (SBufferContainerInfo::SAttribute *)m_pCommandBuffer->AllocData(Cmd.m_AttrCount * sizeof(SBufferContainerInfo::SAttribute));
		return Cmd.m_pAttributes != nullptr;
	});

	mem_copy(Cmd.m_pAttributes, pContainerInfo->m_vAttributes.data(), Cmd.m_AttrCount * sizeof(SBufferContainerInfo::SAttribute));

	m_vVertexArrayInfo[ContainerIndex].m_AssociatedBufferObjectIndex = pContainerInfo->m_VertBufferBindingIndex;
}

void CGraphics_Threaded::IndicesNumRequiredNotify(unsigned int RequiredIndicesCount)
{
	CCommandBuffer::SCommand_IndicesRequiredNumNotify Cmd;
	Cmd.m_RequiredIndicesNum = RequiredIndicesCount;
	AddCmd(Cmd);
}

int CGraphics_Threaded::IssueInit()
{
	// The flags have to be kept consistent with flags set in the CGraphicsBackend_SDL_GL::SetWindowParams function!

	bool IsPurelyWindowed = g_Config.m_GfxFullscreen == 0;
	bool IsExclusiveFullscreen = g_Config.m_GfxFullscreen == 1;
	bool IsDesktopFullscreen = g_Config.m_GfxFullscreen == 2;
#ifndef CONF_FAMILY_WINDOWS
	//  Windowed fullscreen is only available on Windows, use desktop fullscreen on other platforms
	IsDesktopFullscreen |= g_Config.m_GfxFullscreen == 3;
#endif

	int Flags = 0;
	if(IsExclusiveFullscreen)
	{
		Flags |= IGraphicsBackend::INITFLAG_FULLSCREEN;
	}
	else if(IsDesktopFullscreen)
	{
		Flags |= IGraphicsBackend::INITFLAG_DESKTOP_FULLSCREEN;
	}
	else if(IsPurelyWindowed)
	{
		Flags |= IGraphicsBackend::INITFLAG_RESIZABLE;
		if(g_Config.m_GfxBorderless)
		{
			Flags |= IGraphicsBackend::INITFLAG_BORDERLESS;
		}
	}
	if(g_Config.m_GfxVsync)
	{
		Flags |= IGraphicsBackend::INITFLAG_VSYNC;
	}

	const int Result = m_pBackend->Init("DDNet Client", &g_Config.m_GfxScreen, &g_Config.m_GfxScreenWidth, &g_Config.m_GfxScreenHeight, &g_Config.m_GfxScreenRefreshRate, &g_Config.m_GfxFsaaSamples, Flags, &m_DesktopSize.x, &m_DesktopSize.y, &m_ScreenWidth, &m_ScreenHeight, m_pStorage);
	AddBackEndWarningIfExists();
	if(Result == 0)
	{
		m_GLUseTrianglesAsQuad = m_pBackend->UseTrianglesAsQuad();
		m_GLTileBufferingEnabled = m_pBackend->HasTileBuffering();
		m_GLQuadBufferingEnabled = m_pBackend->HasQuadBuffering();
		m_GLQuadContainerBufferingEnabled = m_pBackend->HasQuadContainerBuffering();
		m_GLTextBufferingEnabled = (m_GLQuadContainerBufferingEnabled && m_pBackend->HasTextBuffering());
		m_GLUses2DTextureArrays = m_pBackend->Uses2DTextureArrays();
		m_GLHasTextureArraysSupport = m_pBackend->HasTextureArraysSupport();
		m_ScreenHiDPIScale = m_ScreenWidth / (float)g_Config.m_GfxScreenWidth;
		m_ScreenRefreshRate = g_Config.m_GfxScreenRefreshRate;
	}
	return Result;
}

void CGraphics_Threaded::AdjustViewport(bool SendViewportChangeToBackend)
{
	// adjust the viewport to only allow certain aspect ratios
	// keep this in sync with backend_vulkan GetSwapImageSize's check
	if(m_ScreenHeight > 4 * m_ScreenWidth / 5)
	{
		m_IsForcedViewport = true;
		m_ScreenHeight = 4 * m_ScreenWidth / 5;

		if(SendViewportChangeToBackend)
		{
			UpdateViewport(0, 0, m_ScreenWidth, m_ScreenHeight, true);
		}
	}
	else
	{
		m_IsForcedViewport = false;
	}
}

void CGraphics_Threaded::UpdateViewport(int X, int Y, int W, int H, bool ByResize)
{
	CCommandBuffer::SCommand_Update_Viewport Cmd;
	Cmd.m_X = X;
	Cmd.m_Y = Y;
	Cmd.m_Width = W;
	Cmd.m_Height = H;
	Cmd.m_ByResize = ByResize;
	AddCmd(Cmd);
}

void CGraphics_Threaded::AddBackEndWarningIfExists()
{
	const char *pErrStr = m_pBackend->GetErrorString();
	if(pErrStr != nullptr)
	{
		SWarning NewWarning;
		str_copy(NewWarning.m_aWarningMsg, Localize(pErrStr));
		AddWarning(NewWarning);
	}
}

int CGraphics_Threaded::InitWindow()
{
	int ErrorCode = IssueInit();
	if(ErrorCode == 0)
		return 0;

	// try disabling fsaa
	while(g_Config.m_GfxFsaaSamples)
	{
		// 4 is the minimum required by OpenGL ES spec (GL_MAX_SAMPLES - https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glGet.xhtml),
		// so can probably also be assumed for OpenGL
		if(g_Config.m_GfxFsaaSamples > 4)
			g_Config.m_GfxFsaaSamples = 4;
		else
			g_Config.m_GfxFsaaSamples = 0;

		if(g_Config.m_GfxFsaaSamples)
			log_warn("gfx", "Failed to initialize graphics. Lowering FSAA to %d and trying again.", g_Config.m_GfxFsaaSamples);
		else
			log_warn("gfx", "Failed to initialize graphics. Disabling FSAA and trying again.");

		ErrorCode = IssueInit();
		if(ErrorCode == 0)
			return 0;
	}

	size_t GLInitTryCount = 0;
	while(ErrorCode == EGraphicsBackendErrorCodes::GRAPHICS_BACKEND_ERROR_CODE_GL_CONTEXT_FAILED ||
		ErrorCode == EGraphicsBackendErrorCodes::GRAPHICS_BACKEND_ERROR_CODE_GL_VERSION_FAILED)
	{
		if(ErrorCode == EGraphicsBackendErrorCodes::GRAPHICS_BACKEND_ERROR_CODE_GL_CONTEXT_FAILED)
		{
			// try next smaller major/minor or patch version
			if(g_Config.m_GfxGLMajor >= 4)
			{
				g_Config.m_GfxGLMajor = 3;
				g_Config.m_GfxGLMinor = 3;
				g_Config.m_GfxGLPatch = 0;
			}
			else if(g_Config.m_GfxGLMajor == 3 && g_Config.m_GfxGLMinor >= 1)
			{
				g_Config.m_GfxGLMajor = 3;
				g_Config.m_GfxGLMinor = 0;
				g_Config.m_GfxGLPatch = 0;
			}
			else if(g_Config.m_GfxGLMajor == 3 && g_Config.m_GfxGLMinor == 0)
			{
				g_Config.m_GfxGLMajor = 2;
				g_Config.m_GfxGLMinor = 1;
				g_Config.m_GfxGLPatch = 0;
			}
			else if(g_Config.m_GfxGLMajor == 2 && g_Config.m_GfxGLMinor >= 1)
			{
				g_Config.m_GfxGLMajor = 2;
				g_Config.m_GfxGLMinor = 0;
				g_Config.m_GfxGLPatch = 0;
			}
			else if(g_Config.m_GfxGLMajor == 2 && g_Config.m_GfxGLMinor == 0)
			{
				g_Config.m_GfxGLMajor = 1;
				g_Config.m_GfxGLMinor = 5;
				g_Config.m_GfxGLPatch = 0;
			}
			else if(g_Config.m_GfxGLMajor == 1 && g_Config.m_GfxGLMinor == 5)
			{
				g_Config.m_GfxGLMajor = 1;
				g_Config.m_GfxGLMinor = 4;
				g_Config.m_GfxGLPatch = 0;
			}
			else if(g_Config.m_GfxGLMajor == 1 && g_Config.m_GfxGLMinor == 4)
			{
				g_Config.m_GfxGLMajor = 1;
				g_Config.m_GfxGLMinor = 3;
				g_Config.m_GfxGLPatch = 0;
			}
			else if(g_Config.m_GfxGLMajor == 1 && g_Config.m_GfxGLMinor == 3)
			{
				g_Config.m_GfxGLMajor = 1;
				g_Config.m_GfxGLMinor = 2;
				g_Config.m_GfxGLPatch = 1;
			}
			else if(g_Config.m_GfxGLMajor == 1 && g_Config.m_GfxGLMinor == 2)
			{
				g_Config.m_GfxGLMajor = 1;
				g_Config.m_GfxGLMinor = 1;
				g_Config.m_GfxGLPatch = 0;
			}
		}
		log_warn("gfx", "Failed to initialize graphics. Setting GL version %d.%d.%d and trying again.", g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch);

		// new gl version was set by backend, try again
		ErrorCode = IssueInit();
		if(ErrorCode == 0)
		{
			return 0;
		}

		if(++GLInitTryCount >= 9)
		{
			// try something else
			break;
		}
	}

	// try lowering the resolution
	if(g_Config.m_GfxScreenWidth != 640 || g_Config.m_GfxScreenHeight != 480)
	{
		g_Config.m_GfxScreenWidth = 640;
		g_Config.m_GfxScreenHeight = 480;
		log_warn("gfx", "Failed to initialize graphics. Setting resolution to %dx%d and trying again.", g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight);

		if(IssueInit() == 0)
			return 0;
	}

	// at the very end, just try to set to gl 1.4
	{
		g_Config.m_GfxGLMajor = 1;
		g_Config.m_GfxGLMinor = 4;
		g_Config.m_GfxGLPatch = 0;
		log_warn("gfx", "Failed to initialize graphics. Setting GL version %d.%d.%d and trying again.", g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch);

		if(IssueInit() == 0)
			return 0;
	}

	log_error("gfx", "Failed to initialize graphics. Out of ideas.");
	return -1;
}

int CGraphics_Threaded::Init()
{
	// fetch pointers
	m_pStorage = Kernel()->RequestInterface<IStorage>();
	m_pEngine = Kernel()->RequestInterface<IEngine>();

	// init textures
	m_FirstFreeTexture = 0;
	m_vTextureIndices.resize(CCommandBuffer::MAX_TEXTURES);
	for(size_t i = 0; i < m_vTextureIndices.size(); ++i)
		m_vTextureIndices[i] = i + 1;

	m_FirstFreeVertexArrayInfo = -1;
	m_FirstFreeBufferObjectIndex = -1;
	m_FirstFreeQuadContainer = -1;

	m_pBackend = CreateGraphicsBackend(Localize);
	if(InitWindow() != 0)
		return -1;

	for(auto &FakeMode : g_aFakeModes)
	{
		FakeMode.m_WindowWidth = FakeMode.m_CanvasWidth / m_ScreenHiDPIScale;
		FakeMode.m_WindowHeight = FakeMode.m_CanvasHeight / m_ScreenHiDPIScale;
		FakeMode.m_RefreshRate = g_Config.m_GfxScreenRefreshRate;
	}

	// create command buffers
	for(auto &pCommandBuffer : m_apCommandBuffers)
		pCommandBuffer = new CCommandBuffer(CMD_BUFFER_CMD_BUFFER_SIZE, CMD_BUFFER_DATA_BUFFER_SIZE);
	m_pCommandBuffer = m_apCommandBuffers[0];

	// create null texture, will get id=0
	{
		const size_t PixelSize = 4;
		const unsigned char aRed[] = {0xff, 0x00, 0x00, 0xff};
		const unsigned char aGreen[] = {0x00, 0xff, 0x00, 0xff};
		const unsigned char aBlue[] = {0x00, 0x00, 0xff, 0xff};
		const unsigned char aYellow[] = {0xff, 0xff, 0x00, 0xff};
		constexpr size_t NullTextureDimension = 16;
		unsigned char aNullTextureData[NullTextureDimension * NullTextureDimension * PixelSize];
		for(size_t y = 0; y < NullTextureDimension; ++y)
		{
			for(size_t x = 0; x < NullTextureDimension; ++x)
			{
				const unsigned char *pColor;
				if(x < NullTextureDimension / 2 && y < NullTextureDimension / 2)
					pColor = aRed;
				else if(x >= NullTextureDimension / 2 && y < NullTextureDimension / 2)
					pColor = aGreen;
				else if(x < NullTextureDimension / 2 && y >= NullTextureDimension / 2)
					pColor = aBlue;
				else
					pColor = aYellow;
				mem_copy(&aNullTextureData[(y * NullTextureDimension + x) * PixelSize], pColor, PixelSize);
			}
		}
		CImageInfo NullTextureInfo;
		NullTextureInfo.m_Width = NullTextureDimension;
		NullTextureInfo.m_Height = NullTextureDimension;
		NullTextureInfo.m_Format = CImageInfo::FORMAT_RGBA;
		NullTextureInfo.m_pData = aNullTextureData;
		m_NullTexture.Invalidate();
		m_NullTexture = LoadTextureRaw(NullTextureInfo, TextureLoadFlags(), "null-texture");
		dbg_assert(m_NullTexture.IsNullTexture(), "Null texture invalid");
	}

	static constexpr LOG_COLOR GPU_INFO_LOG_COLOR = LOG_COLOR{153, 127, 255};
	log_info_color(GPU_INFO_LOG_COLOR, "gfx", "GPU vendor: %s", GetVendorString());
	log_info_color(GPU_INFO_LOG_COLOR, "gfx", "GPU renderer: %s", GetRendererString());
	log_info_color(GPU_INFO_LOG_COLOR, "gfx", "GPU version: %s", GetVersionString());

	AdjustViewport(true);

	return 0;
}

void CGraphics_Threaded::Shutdown()
{
	// shutdown the backend
	m_pBackend->Shutdown();
	delete m_pBackend;
	m_pBackend = nullptr;

	// delete the command buffers
	for(auto &pCommandBuffer : m_apCommandBuffers)
		delete pCommandBuffer;
}

int CGraphics_Threaded::GetNumScreens() const
{
	return m_pBackend->GetNumScreens();
}

const char *CGraphics_Threaded::GetScreenName(int Screen) const
{
	return m_pBackend->GetScreenName(Screen);
}

void CGraphics_Threaded::Minimize()
{
	m_pBackend->Minimize();

	for(auto &PropChangedListener : m_vPropChangeListeners)
		PropChangedListener();
}

void CGraphics_Threaded::WarnPngliteIncompatibleImages(bool Warn)
{
	m_WarnPngliteIncompatibleImages = Warn;
}

void CGraphics_Threaded::SetWindowParams(int FullscreenMode, bool IsBorderless)
{
	g_Config.m_GfxFullscreen = std::clamp(FullscreenMode, 0, 3);
	g_Config.m_GfxBorderless = (int)IsBorderless;

	m_pBackend->SetWindowParams(g_Config.m_GfxFullscreen, g_Config.m_GfxBorderless);
	CVideoMode CurMode;
	m_pBackend->GetCurrentVideoMode(CurMode, m_ScreenHiDPIScale, m_DesktopSize.x, m_DesktopSize.y, g_Config.m_GfxScreen);
	GotResized(CurMode.m_WindowWidth, CurMode.m_WindowHeight, CurMode.m_RefreshRate);

	for(auto &PropChangedListener : m_vPropChangeListeners)
		PropChangedListener();
}

bool CGraphics_Threaded::SetWindowScreen(int Index, bool MoveToCenter)
{
	if(!m_pBackend->SetWindowScreen(Index, MoveToCenter, &m_DesktopSize))
	{
		return false;
	}

	// send a got resized event so that the current canvas size is requested
	GotResized(g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight, g_Config.m_GfxScreenRefreshRate);

	for(auto &PropChangedListener : m_vPropChangeListeners)
		PropChangedListener();

	return true;
}

bool CGraphics_Threaded::SwitchWindowScreen(int Index, bool MoveToCenter)
{
	const int IsFullscreen = g_Config.m_GfxFullscreen;
	const int IsBorderless = g_Config.m_GfxBorderless;
	const bool IsPurelyWindowed = IsFullscreen == 0 && !IsBorderless;

	if(!SetWindowScreen(Index, !IsPurelyWindowed || MoveToCenter))
	{
		return false;
	}

	if(IsFullscreen != 3 && !IsPurelyWindowed)
	{
		// Prevent window from being stretched over multiple monitors by temporarily switching to
		// windowed fullscreen mode on Windows, which is desktop fullscreen mode on other systems.
		SetWindowParams(3, false);
	}

	// In purely windowed mode we preserve the window's size instead of resizing to the screen.
	if(!IsPurelyWindowed)
	{
		CVideoMode CurMode;
		GetCurrentVideoMode(CurMode, Index);

		g_Config.m_GfxScreenWidth = CurMode.m_WindowWidth;
		g_Config.m_GfxScreenHeight = CurMode.m_WindowHeight;
		g_Config.m_GfxScreenRefreshRate = CurMode.m_RefreshRate;

		ResizeToScreen();
	}

	SetWindowParams(IsFullscreen, IsBorderless);
	return true;
}

void CGraphics_Threaded::Move(int x, int y)
{
#if defined(CONF_VIDEORECORDER)
	if(IVideo::Current() && IVideo::Current()->IsRecording())
		return;
#endif

	// Only handling CurScreen != m_GfxScreen doesn't work reliably
	const int CurScreen = m_pBackend->GetWindowScreen();
	if(!m_pBackend->UpdateDisplayMode(CurScreen, &m_DesktopSize))
		return;

	// send a got resized event so that the current canvas size is requested
	GotResized(g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight, g_Config.m_GfxScreenRefreshRate);

	for(auto &PropChangedListener : m_vPropChangeListeners)
		PropChangedListener();
}

bool CGraphics_Threaded::Resize(int w, int h, int RefreshRate)
{
#if defined(CONF_VIDEORECORDER)
	if(IVideo::Current() && IVideo::Current()->IsRecording())
		return false;
#endif

	if(WindowWidth() == w && WindowHeight() == h && RefreshRate == m_ScreenRefreshRate)
		return false;

	// if the size is changed manually, only set the window resize, a window size changed event is triggered anyway
	if(m_pBackend->ResizeWindow(w, h, RefreshRate))
	{
		CVideoMode CurMode;
		m_pBackend->GetCurrentVideoMode(CurMode, m_ScreenHiDPIScale, m_DesktopSize.x, m_DesktopSize.y, g_Config.m_GfxScreen);
		GotResized(w, h, RefreshRate);
		return true;
	}
	return false;
}

void CGraphics_Threaded::ResizeToScreen()
{
	if(Resize(g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight, g_Config.m_GfxScreenRefreshRate))
		return;

	// Revert config variables if the change was not accepted
	g_Config.m_GfxScreenWidth = ScreenWidth();
	g_Config.m_GfxScreenHeight = ScreenHeight();
	g_Config.m_GfxScreenRefreshRate = m_ScreenRefreshRate;
}

void CGraphics_Threaded::GotResized(int w, int h, int RefreshRate)
{
#if defined(CONF_VIDEORECORDER)
	if(IVideo::Current() && IVideo::Current()->IsRecording())
		return;
#endif

	// if RefreshRate is -1 use the current config refresh rate
	if(RefreshRate == -1)
		RefreshRate = g_Config.m_GfxScreenRefreshRate;

	// if the size change event is triggered, set all parameters and change the viewport
	auto PrevCanvasWidth = m_ScreenWidth;
	auto PrevCanvasHeight = m_ScreenHeight;
	m_pBackend->GetViewportSize(m_ScreenWidth, m_ScreenHeight);

	AdjustViewport(false);

	m_ScreenRefreshRate = RefreshRate;

	g_Config.m_GfxScreenWidth = w;
	g_Config.m_GfxScreenHeight = h;
	g_Config.m_GfxScreenRefreshRate = m_ScreenRefreshRate;

	auto OldDpi = m_ScreenHiDPIScale;
	m_ScreenHiDPIScale = m_ScreenWidth / (float)g_Config.m_GfxScreenWidth;

	// A DPI change must notify the listeners, since e.g. video modes
	// currently depend on it.
	if(OldDpi != m_ScreenHiDPIScale)
	{
		for(auto &PropChangedListener : m_vPropChangeListeners)
			PropChangedListener();
	}

	UpdateViewport(0, 0, m_ScreenWidth, m_ScreenHeight, true);

	// kick the command buffer and wait
	KickCommandBuffer();
	WaitForIdle();

	if(PrevCanvasWidth != m_ScreenWidth || PrevCanvasHeight != m_ScreenHeight)
	{
		for(auto &ResizeListener : m_vResizeListeners)
			ResizeListener();
	}
}

bool CGraphics_Threaded::IsScreenKeyboardShown()
{
	return m_pBackend->IsScreenKeyboardShown();
}

void CGraphics_Threaded::AddWindowResizeListener(WINDOW_RESIZE_FUNC pFunc)
{
	m_vResizeListeners.emplace_back(pFunc);
}

void CGraphics_Threaded::AddWindowPropChangeListener(WINDOW_PROPS_CHANGED_FUNC pFunc)
{
	m_vPropChangeListeners.emplace_back(pFunc);
}

int CGraphics_Threaded::GetWindowScreen()
{
	return m_pBackend->GetWindowScreen();
}

void CGraphics_Threaded::WindowDestroyNtf(uint32_t WindowId)
{
	m_pBackend->WindowDestroyNtf(WindowId);

	CCommandBuffer::SCommand_WindowDestroyNtf Cmd;
	Cmd.m_WindowId = WindowId;
	AddCmd(Cmd);

	// wait
	KickCommandBuffer();
	WaitForIdle();
}

void CGraphics_Threaded::WindowCreateNtf(uint32_t WindowId)
{
	m_pBackend->WindowCreateNtf(WindowId);

	CCommandBuffer::SCommand_WindowCreateNtf Cmd;
	Cmd.m_WindowId = WindowId;
	AddCmd(Cmd);

	// wait
	KickCommandBuffer();
	WaitForIdle();
}

int CGraphics_Threaded::WindowActive()
{
	return m_pBackend->WindowActive();
}

int CGraphics_Threaded::WindowOpen()
{
	return m_pBackend->WindowOpen();
}

void CGraphics_Threaded::SetWindowGrab(bool Grab)
{
	m_pBackend->SetWindowGrab(Grab);
}

void CGraphics_Threaded::NotifyWindow()
{
	m_pBackend->NotifyWindow();
}

void CGraphics_Threaded::ReadPixel(ivec2 Position, ColorRGBA *pColor)
{
	dbg_assert(Position.x >= 0 && Position.x < ScreenWidth(), "ReadPixel position x out of range");
	dbg_assert(Position.y >= 0 && Position.y < ScreenHeight(), "ReadPixel position y out of range");

	m_ReadPixelPosition = Position;
	m_pReadPixelColor = pColor;
}

void CGraphics_Threaded::ReadPixelDirect(bool *pSwapped)
{
	if(m_pReadPixelColor == nullptr)
		return;

	CCommandBuffer::SCommand_TrySwapAndReadPixel Cmd;
	Cmd.m_Position = m_ReadPixelPosition;
	Cmd.m_pColor = m_pReadPixelColor;
	Cmd.m_pSwapped = pSwapped;
	AddCmd(Cmd);

	KickCommandBuffer();
	WaitForIdle();

	m_pReadPixelColor = nullptr;
}

void CGraphics_Threaded::TakeScreenshot(const char *pFilename)
{
	// TODO: screenshot support
	char aDate[20];
	str_timestamp(aDate, sizeof(aDate));
	str_format(m_aScreenshotName, sizeof(m_aScreenshotName), "screenshots/%s_%s.png", pFilename ? pFilename : "screenshot", aDate);
	m_DoScreenshot = true;
}

void CGraphics_Threaded::TakeCustomScreenshot(const char *pFilename)
{
	str_copy(m_aScreenshotName, pFilename);
	m_DoScreenshot = true;
}

void CGraphics_Threaded::Swap()
{
	bool Swapped = false;
	ScreenshotDirect(&Swapped);
	ReadPixelDirect(&Swapped);

	if(!Swapped)
	{
		CCommandBuffer::SCommand_Swap Cmd;
		AddCmd(Cmd);
	}

	KickCommandBuffer();
	// TODO: Remove when https://github.com/libsdl-org/SDL/issues/5203 is fixed
#ifdef CONF_PLATFORM_MACOS
	if(str_find(GetVersionString(), "Metal"))
		WaitForIdle();
#endif
}

bool CGraphics_Threaded::SetVSync(bool State)
{
	if(!m_pCommandBuffer)
		return true;

	// add vsync command
	bool RetOk = false;
	CCommandBuffer::SCommand_VSync Cmd;
	Cmd.m_VSync = State ? 1 : 0;
	Cmd.m_pRetOk = &RetOk;
	AddCmd(Cmd);

	// kick the command buffer
	KickCommandBuffer();
	WaitForIdle();

	if(RetOk)
	{
		g_Config.m_GfxVsync = State;
	}
	return RetOk;
}

bool CGraphics_Threaded::SetMultiSampling(uint32_t ReqMultiSamplingCount, uint32_t &MultiSamplingCountBackend)
{
	if(!m_pCommandBuffer)
		return true;

	// add multisampling command
	bool RetOk = false;
	CCommandBuffer::SCommand_MultiSampling Cmd;
	Cmd.m_RequestedMultiSamplingCount = ReqMultiSamplingCount;
	Cmd.m_pRetMultiSamplingCount = &MultiSamplingCountBackend;
	Cmd.m_pRetOk = &RetOk;
	AddCmd(Cmd);

	// kick the command buffer
	KickCommandBuffer();
	WaitForIdle();
	return RetOk;
}

// synchronization
void CGraphics_Threaded::InsertSignal(CSemaphore *pSemaphore)
{
	CCommandBuffer::SCommand_Signal Cmd;
	Cmd.m_pSemaphore = pSemaphore;
	AddCmd(Cmd);
}

bool CGraphics_Threaded::IsIdle() const
{
	return m_pBackend->IsIdle();
}

void CGraphics_Threaded::WaitForIdle()
{
	m_pBackend->WaitForIdle();
}

void CGraphics_Threaded::AddWarning(const SWarning &Warning)
{
	const std::unique_lock<std::mutex> Lock(m_WarningsMutex);
	m_vWarnings.emplace_back(Warning);
}

std::optional<SWarning> CGraphics_Threaded::CurrentWarning()
{
	const std::unique_lock<std::mutex> Lock(m_WarningsMutex);
	if(m_vWarnings.empty())
	{
		return std::nullopt;
	}
	else
	{
		std::optional<SWarning> Result = std::make_optional(m_vWarnings[0]);
		m_vWarnings.erase(m_vWarnings.begin());
		return Result;
	}
}

std::optional<int> CGraphics_Threaded::ShowMessageBox(const CMessageBox &MessageBox)
{
	if(m_pBackend == nullptr)
	{
		return std::nullopt;
	}
	m_pBackend->WaitForIdle();
	return m_pBackend->ShowMessageBox(MessageBox);
}

bool CGraphics_Threaded::IsBackendInitialized()
{
	return m_pBackend != nullptr;
}

const char *CGraphics_Threaded::GetVendorString()
{
	return m_pBackend->GetVendorString();
}

const char *CGraphics_Threaded::GetVersionString()
{
	return m_pBackend->GetVersionString();
}

const char *CGraphics_Threaded::GetRendererString()
{
	return m_pBackend->GetRendererString();
}

const char *CGraphics_Threaded::GetFatalError() const
{
	return m_pBackend == nullptr ? "" : m_pBackend->GetFatalError();
}

TGLBackendReadPresentedImageData &CGraphics_Threaded::GetReadPresentedImageDataFuncUnsafe()
{
	return m_pBackend->GetReadPresentedImageDataFuncUnsafe();
}

int CGraphics_Threaded::GetVideoModes(CVideoMode *pModes, int MaxModes, int Screen)
{
	if(g_Config.m_GfxDisplayAllVideoModes)
	{
		const int Count = std::min(std::size(g_aFakeModes), (size_t)MaxModes);
		mem_copy(pModes, g_aFakeModes, Count * sizeof(CVideoMode));
		return Count;
	}

	int NumModes = 0;
	m_pBackend->GetVideoModes(pModes, MaxModes, &NumModes, m_ScreenHiDPIScale, m_DesktopSize.x, m_DesktopSize.y, Screen);
	return NumModes;
}

void CGraphics_Threaded::GetCurrentVideoMode(CVideoMode &CurMode, int Screen)
{
	m_pBackend->GetCurrentVideoMode(CurMode, m_ScreenHiDPIScale, m_DesktopSize.x, m_DesktopSize.y, Screen);
}

extern IEngineGraphics *CreateEngineGraphicsThreaded()
{
	return new CGraphics_Threaded();
}
