Download
2355 lines 67.6 KiBTextModified 2026-07-29 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355
/**
 * Copyright (c) 2006-2026 LOVE Development Team
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors be held liable for any damages
 * arising from the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software
 *    in a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 3. This notice may not be removed or altered from any source distribution.
 **/

#include "Graphics.h"
#include "StreamBuffer.h"
#include "Buffer.h"
#include "Texture.h"
#include "GraphicsReadback.h"
#include "Shader.h"
#include "ShaderStage.h"
#include "window/Window.h"
#include "image/Image.h"
#include "common/memory.h"

#include <tuple>

#import <QuartzCore/CAMetalLayer.h>

#ifdef LOVE_MACOS
// Needed for the GPU dynamic switching hack below.
#define GL_SILENCE_DEPRECATION 1
#import <Cocoa/Cocoa.h>
#endif

namespace love
{
namespace graphics
{
namespace metal
{

static MTLSamplerMinMagFilter getMTLSamplerFilter(SamplerState::FilterMode mode)
{
	switch (mode)
	{
		case SamplerState::FILTER_LINEAR: return MTLSamplerMinMagFilterLinear;
		case SamplerState::FILTER_NEAREST: return MTLSamplerMinMagFilterNearest;
		case SamplerState::FILTER_MAX_ENUM: return MTLSamplerMinMagFilterLinear;
	}
	return MTLSamplerMinMagFilterLinear;
}

static MTLSamplerMipFilter getMTLSamplerMipFilter(SamplerState::MipmapFilterMode mode)
{
	switch (mode)
	{
		case SamplerState::MIPMAP_FILTER_NONE: return MTLSamplerMipFilterNotMipmapped;
		case SamplerState::MIPMAP_FILTER_LINEAR: return MTLSamplerMipFilterLinear;
		case SamplerState::MIPMAP_FILTER_NEAREST: return MTLSamplerMipFilterNearest;
		case SamplerState::MIPMAP_FILTER_MAX_ENUM: return MTLSamplerMipFilterNotMipmapped;
	}
	return MTLSamplerMipFilterNotMipmapped;
}

static MTLSamplerAddressMode getMTLSamplerAddressMode(SamplerState::WrapMode mode)
{
	switch (mode)
	{
		case SamplerState::WRAP_CLAMP: return MTLSamplerAddressModeClampToEdge;
		case SamplerState::WRAP_CLAMP_ZERO: return MTLSamplerAddressModeClampToZero;
		case SamplerState::WRAP_CLAMP_ONE:
			if (@available(macOS 10.12, iOS 14.0, *))
				return MTLSamplerAddressModeClampToBorderColor;
			else
				return MTLSamplerAddressModeClampToZero;
		case SamplerState::WRAP_REPEAT: return MTLSamplerAddressModeRepeat;
		case SamplerState::WRAP_MIRRORED_REPEAT: return MTLSamplerAddressModeMirrorRepeat;
		case SamplerState::WRAP_MAX_ENUM: return MTLSamplerAddressModeClampToEdge;
	}
	return MTLSamplerAddressModeClampToEdge;
}

static MTLCompareFunction getMTLCompareFunction(CompareMode mode)
{
	switch (mode)
	{
		case COMPARE_LESS: return MTLCompareFunctionLess;
		case COMPARE_LEQUAL: return MTLCompareFunctionLessEqual;
		case COMPARE_EQUAL: return MTLCompareFunctionEqual;
		case COMPARE_GEQUAL: return MTLCompareFunctionGreaterEqual;
		case COMPARE_GREATER: return MTLCompareFunctionGreater;
		case COMPARE_NOTEQUAL: return MTLCompareFunctionNotEqual;
		case COMPARE_ALWAYS: return MTLCompareFunctionAlways;
		case COMPARE_NEVER: return MTLCompareFunctionNever;
		case COMPARE_MAX_ENUM: return MTLCompareFunctionNever;
	}
	return MTLCompareFunctionNever;
}

static MTLStencilOperation getMTLStencilOperation(StencilAction action)
{
	switch (action)
	{
		case STENCIL_KEEP: return MTLStencilOperationKeep;
		case STENCIL_ZERO: return MTLStencilOperationZero;
		case STENCIL_REPLACE: return MTLStencilOperationReplace;
		case STENCIL_INCREMENT: return MTLStencilOperationIncrementClamp;
		case STENCIL_DECREMENT: return MTLStencilOperationDecrementClamp;
		case STENCIL_INCREMENT_WRAP: return MTLStencilOperationIncrementWrap;
		case STENCIL_DECREMENT_WRAP: return MTLStencilOperationDecrementWrap;
		case STENCIL_INVERT: return MTLStencilOperationInvert;
		case STENCIL_MAX_ENUM: return MTLStencilOperationKeep;
	}
	return MTLStencilOperationKeep;
}

static MTLPrimitiveType getMTLPrimitiveType(PrimitiveType prim)
{
	switch (prim)
	{
		case PRIMITIVE_TRIANGLES: return MTLPrimitiveTypeTriangle;
		case PRIMITIVE_TRIANGLE_STRIP: return MTLPrimitiveTypeTriangleStrip;
		case PRIMITIVE_TRIANGLE_FAN: return MTLPrimitiveTypeTriangle; // This is emulated with an index buffer.
		case PRIMITIVE_POINTS: return MTLPrimitiveTypePoint;
		case PRIMITIVE_MAX_ENUM: return MTLPrimitiveTypeTriangle;
	}
	return MTLPrimitiveTypeTriangle;
}

static inline id<MTLTexture> getMTLTexture(love::graphics::Texture *tex)
{
	return tex ? (__bridge id<MTLTexture>)(void *) tex->getHandle() : nil;
}

static inline id<MTLTexture> getMTLRenderTarget(love::graphics::Texture *tex)
{
	return tex ? (__bridge id<MTLTexture>)(void *) tex->getRenderTargetHandle() : nil;
}

static inline id<MTLBuffer> getMTLBuffer(love::graphics::Resource *res)
{
	return res ? (__bridge id<MTLBuffer>)(void *) res->getHandle() : nil;
}

static inline void setBuffer(id<MTLRenderCommandEncoder> encoder, Graphics::RenderEncoderBindings &bindings, ShaderStageType stage, int index, id<MTLBuffer> buffer, size_t offset)
{
	void *b = (__bridge void *)buffer;
	auto &binding = bindings.buffers[index][stage];
	if (binding.buffer != b)
	{
		binding.buffer = b;
		binding.offset = offset;
		if (stage == SHADERSTAGE_VERTEX)
			[encoder setVertexBuffer:buffer offset:offset atIndex:index];
		else if (stage == SHADERSTAGE_PIXEL)
			[encoder setFragmentBuffer:buffer offset:offset atIndex:index];
	}
	else if (binding.offset != offset)
	{
		binding.offset = offset;
		if (stage == SHADERSTAGE_VERTEX)
			[encoder setVertexBufferOffset:offset atIndex:index];
		else if (stage == SHADERSTAGE_PIXEL)
			[encoder setFragmentBufferOffset:offset atIndex:index];
	}
}

static inline void setBuffer(id<MTLComputeCommandEncoder> encoder, Graphics::RenderEncoderBindings &bindings, int index, id<MTLBuffer> buffer, size_t offset)
{
	void *b = (__bridge void *)buffer;
	auto &binding = bindings.buffers[index][SHADERSTAGE_COMPUTE];
	if (binding.buffer != b)
	{
		binding.buffer = b;
		binding.offset = offset;
		[encoder setBuffer:buffer offset:offset atIndex:index];
	}
	else if (binding.offset != offset)
	{
		binding.offset = offset;
		[encoder setBufferOffset:offset atIndex:index];
	}
}

static inline void setTexture(id<MTLRenderCommandEncoder> encoder, Graphics::RenderEncoderBindings &bindings, ShaderStageType stage, int index, id<MTLTexture> texture)
{
	void *t = (__bridge void *)texture;
	auto &binding = bindings.textures[index][stage];
	if (binding != t)
	{
		binding = t;
		if (stage == SHADERSTAGE_VERTEX)
			[encoder setVertexTexture:texture atIndex:index];
		else if (stage == SHADERSTAGE_PIXEL)
			[encoder setFragmentTexture:texture atIndex:index];
	}
}

static inline void setTexture(id<MTLComputeCommandEncoder> encoder, Graphics::RenderEncoderBindings &bindings, int index, id<MTLTexture> texture)
{
	void *t = (__bridge void *)texture;
	auto &binding = bindings.textures[index][SHADERSTAGE_COMPUTE];
	if (binding != t)
	{
		binding = t;
		[encoder setTexture:texture atIndex:index];
	}
}

static inline void setSampler(id<MTLRenderCommandEncoder> encoder, Graphics::RenderEncoderBindings &bindings, ShaderStageType stage, int index, love::graphics::Texture *samplertex)
{
	void *s = samplertex != nullptr ? (void *)samplertex->getSamplerHandle() : nullptr;
	auto &binding = bindings.samplers[index][stage];
	if (binding != s)
	{
		binding = s;
		id<MTLSamplerState> sampler = (__bridge id<MTLSamplerState>) s;
		if (stage == SHADERSTAGE_VERTEX)
			[encoder setVertexSamplerState:sampler atIndex:index];
		else if (stage == SHADERSTAGE_PIXEL)
			[encoder setFragmentSamplerState:sampler atIndex:index];
	}
}

static inline void setSampler(id<MTLComputeCommandEncoder> encoder, Graphics::RenderEncoderBindings &bindings, int index, love::graphics::Texture *samplertex)
{
	void *s = samplertex != nullptr ? (void *)samplertex->getSamplerHandle() : nullptr;
	auto &binding = bindings.samplers[index][SHADERSTAGE_COMPUTE];
	if (binding != s)
	{
		binding = s;
		id<MTLSamplerState> sampler = (__bridge id<MTLSamplerState>) s;
		[encoder setSamplerState:sampler atIndex:index];
	}
}

std::tuple<love::graphics::Graphics *, std::string> createInstance()
{
	love::graphics::Graphics *instance = nullptr;
	std::string err;

	try
	{
		instance = new Graphics();
	}
	catch (love::Exception &e)
	{
		err = "Cannot create Metal renderer: " + std::string(e.what()) + "\n";
	}

	return { instance, err };
}

struct DefaultVertexAttributes
{
	float floats[4];
	int ints[4];
	float color[4];
};

Graphics *Graphics::graphicsInstance = nullptr;

Graphics::Graphics()
	: love::graphics::Graphics("love.graphics.metal")
	, device(nil)
	, commandQueue(nil)
	, commandBuffer(nil)
	, renderEncoder(nil)
	, blitEncoder(nil)
	, metalLayer(nil)
	, activeDrawable(nil)
	, passDesc(nil)
	, dirtyRenderState(STATEBIT_ALL)
	, lastCullMode(CULL_MAX_ENUM)
	, lastRenderPipelineKey()
	, shaderSwitches(0)
	, attachmentStoreActions()
	, renderBindings()
	, uniformBufferOffset(0)
	, uniformBufferGPUStart(0)
	, defaultAttributesBuffer(nullptr)
	, families()
	, isVMDevice(false)
{ @autoreleasepool {
	graphicsInstance = this;
#ifdef LOVE_MACOS
	if (isLowPowerPreferred())
	{
		for (id<MTLDevice> dev in MTLCopyAllDevices())
		{
			if (dev.isLowPower)
			{
				device = dev;
				break;
			}
		}
	}
#endif
	if (device == nil)
		device = MTLCreateSystemDefaultDevice();
	if (device == nil)
		throw love::Exception("Metal is not supported on this system.");

	isVMDevice = [device.name containsString:@("Apple Paravirtual device")];

#ifdef LOVE_MACOS
	// On multi-GPU macOS systems with a low and high power GPU (e.g. a 2016
	// Macbook Pro), the OS doesn't activate the high power GPU for final
	// monitor blitting even when MTLCreateSystemDefaultDevice is called, on
	// newer macOS versions. https://developer.apple.com/forums/thread/675411
	// But it does when an OpenGL context is created, so this big hack forces it
	// when we want it to happen.
	if (!device.isLowPower && MTLCopyAllDevices().count > 1)
	{
		NSOpenGLPixelFormatAttribute attributes[] =
		{
			NSOpenGLPFADoubleBuffer,
			NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
			NSOpenGLPFAColorSize, 24,
			NSOpenGLPFAAlphaSize, 8,
			NSOpenGLPFADepthSize, 24,
			NSOpenGLPFAStencilSize, 8,
			NSOpenGLPFASampleBuffers, 0,
			0,
		};

		auto pixelformat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
		auto glcontext = [[NSOpenGLContext alloc] initWithFormat:pixelformat shareContext:nullptr];
		LOVE_UNUSED(glcontext);
	}
#endif

	commandQueue = [device newCommandQueue];
	passDesc = [MTLRenderPassDescriptor new];

	initCapabilities();

	uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_UNIFORM, 1024 * 512 * 1);

	{
		std::vector<Buffer::DataDeclaration> dataformat = {
			{"floats", DATAFORMAT_FLOAT_VEC4, 0},
			{"ints", DATAFORMAT_INT32_VEC4, 0},
			{"color", DATAFORMAT_FLOAT_VEC4, 0}
		};

		DefaultVertexAttributes defaults = {
			{0.0f, 0.0f, 0.0f, 1.0f},
			{0, 0, 0, 1},
			{1.0f, 1.0f, 1.0f, 1.0f}
		};

		Buffer::Settings attribsettings(BUFFERUSAGEFLAG_VERTEX, BUFFERDATAUSAGE_STATIC);
		attribsettings.debugName = "Default Vertex Attributes";

		defaultAttributesBuffer = newBuffer(attribsettings, dataformat, &defaults, sizeof(DefaultVertexAttributes), 0);
	}

	if (batchedDrawState.vb[0] == nullptr)
	{
		// Initial sizes that should be good enough for most cases. It will
		// resize to fit if needed, later.
		batchedDrawState.vb[0] = CreateStreamBuffer(device, BUFFERUSAGE_VERTEX, 1024 * 1024 * 1);
		batchedDrawState.vb[1] = CreateStreamBuffer(device, BUFFERUSAGE_VERTEX, 256  * 1024 * 1);
		batchedDrawState.indexBuffer = CreateStreamBuffer(device, BUFFERUSAGE_INDEX, sizeof(uint16) * LOVE_UINT16_MAX);
	}

	createQuadIndexBuffer();
	createFanIndexBuffer();

	// We always need a default shader.
	for (int i = 0; i < Shader::STANDARD_MAX_ENUM; i++)
	{
		auto stype = (Shader::StandardShader) i;
		if (!Shader::standardShaders[i])
		{
			std::vector<std::string> stages;
			Shader::CompileOptions opts;
			stages.push_back(Shader::getDefaultCode(stype, SHADERSTAGE_VERTEX));
			stages.push_back(Shader::getDefaultCode(stype, SHADERSTAGE_PIXEL));
			Shader::standardShaders[i] = newShader(stages, opts);
		}
	}

	// A shader should always be active, but the default shader shouldn't be
	// returned by getShader(), so we don't do setShader(defaultShader).
	if (!Shader::current)
		Shader::standardShaders[Shader::STANDARD_DEFAULT]->attach();

	auto window = Module::getInstance<love::window::Window>(M_WINDOW);

	if (window != nullptr)
	{
		window->setGraphics(this);

		// Recreate the window using the current renderer, if needed.
		if (window->isOpen())
		{
			int w, h;
			love::window::WindowSettings settings;
			window->getWindow(w, h, settings);
			window->setWindow(w, h, &settings);
		}
	}
}}

Graphics::~Graphics()
{ @autoreleasepool {
	submitCommandBuffer(SUBMIT_DONE);
	processCompletedCommandBuffers();

	// Wait for all active command buffers to complete before returning from
	// the destructor.
	for (id<MTLCommandBuffer> cmd : activeCommandBuffers)
	{
		if (cmd.status == MTLCommandBufferStatusNotEnqueued || cmd.status == MTLCommandBufferStatusEnqueued)
			[cmd commit];

		[cmd waitUntilCompleted];
	}

	uniformBuffer->release();
	defaultAttributesBuffer->release();
	passDesc = nil;
	commandQueue = nil;
	device = nil;

	for (auto &kvp : cachedSamplers)
		CFBridgingRelease(kvp.second);

	for (auto &kvp : cachedDepthStencilStates)
		CFBridgingRelease(kvp.second);

	graphicsInstance = nullptr;
}}

love::graphics::StreamBuffer *Graphics::newStreamBuffer(BufferUsage usage, size_t size)
{
	return CreateStreamBuffer(device, usage, size);
}

love::graphics::Texture *Graphics::newTexture(const Texture::Settings &settings, const Texture::Slices *data)
{
	return new Texture(this, device, settings, data);
}

love::graphics::Texture *Graphics::newTextureView(love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
{
	return new Texture(this, device, base, viewsettings);
}

love::graphics::ShaderStage *Graphics::newShaderStageInternal(ShaderStageType stage, const std::string &cachekey, const std::string &source, bool gles)
{
	return new ShaderStage(this, stage, source, gles, cachekey);
}

love::graphics::Shader *Graphics::newShaderInternal(StrongRef<love::graphics::ShaderStage> stages[SHADERSTAGE_MAX_ENUM], const Shader::CompileOptions &options)
{
	return new Shader(device, stages, options);
}

love::graphics::Buffer *Graphics::newBuffer(const Buffer::Settings &settings, const std::vector<Buffer::DataDeclaration> &format, const void *data, size_t size, size_t arraylength)
{
	return new Buffer(this, device, settings, format, data, size, arraylength);
}

love::graphics::GraphicsReadback *Graphics::newReadbackInternal(ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset)
{
	return new GraphicsReadback(this, method, buffer, offset, size, dest, destoffset);
}

love::graphics::GraphicsReadback *Graphics::newReadbackInternal(ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty)
{
	return new GraphicsReadback(this, method, texture, slice, mipmap, rect, dest, destx, desty);
}

void Graphics::backbufferChanged(const BackbufferSettings &settings)
{
	bool sizechanged = settings.width != backbufferSettings.width || settings.height != backbufferSettings.height
		|| settings.pixelWidth != backbufferSettings.pixelWidth || settings.pixelHeight != backbufferSettings.pixelHeight;

	bool dschanged = settings.stencil != backbufferSettings.stencil || settings.depth != backbufferSettings.depth;
	bool msaachanged = settings.msaa != backbufferSettings.msaa;

	backbufferSettings = settings;

	if (!isRenderTargetActive())
	{
		dirtyRenderState |= STATEBIT_VIEWPORT | STATEBIT_SCISSOR;
		resetProjection();
	}

	Texture::Settings ts;
	ts.width = settings.width;
	ts.height = settings.height;
	ts.dpiScale = (float)settings.pixelHeight / (float)settings.height;
	ts.msaa = getRequestedBackbufferMSAA();
	ts.renderTarget = true;
	ts.readable.set(false);

	if (sizechanged || msaachanged)
	{
		backbufferMSAA.set(nullptr);
		if (ts.msaa > 1)
		{
			ts.format = isGammaCorrect() ? PIXELFORMAT_BGRA8_sRGB : PIXELFORMAT_BGRA8_UNORM;
			backbufferMSAA.set(newTexture(ts), Acquire::NORETAIN);
		}
	}

	if (sizechanged || msaachanged || dschanged)
	{
		backbufferDepthStencil.set(nullptr);
		if (settings.stencil || settings.depth)
		{
			if (settings.stencil && settings.depth)
				ts.format = PIXELFORMAT_DEPTH24_UNORM_STENCIL8;
			else if (settings.stencil)
				ts.format = PIXELFORMAT_STENCIL8;
			else if (settings.depth)
				ts.format = PIXELFORMAT_DEPTH24_UNORM;
			backbufferDepthStencil.set(newTexture(ts), Acquire::NORETAIN);
		}
	}
}

bool Graphics::setMode(void *context, const BackbufferSettings &settings)
{ @autoreleasepool {
	this->metalLayer = (__bridge CAMetalLayer *) context;

	metalLayer.device = device;
	metalLayer.pixelFormat = isGammaCorrect() ? MTLPixelFormatBGRA8Unorm_sRGB : MTLPixelFormatBGRA8Unorm;

	// This is set to NO when there are pending screen captures.
	metalLayer.framebufferOnly = YES;

#ifdef LOVE_MACOS
	// Matches behaviour of SDL's OpenGL context when using the GL renderer.
	metalLayer.magnificationFilter = kCAFilterNearest;
#endif

	backbufferChanged(settings);

	created = true;

	// Restore the graphics state.
	restoreState(states.back());

	return true;
}}

void Graphics::unSetMode()
{ @autoreleasepool {
	if (!isCreated())
		return;

	flushBatchedDraws();

	submitCommandBuffer(SUBMIT_DONE);

	clearTemporaryResources();

	created = false;
	metalLayer = nil;
	activeDrawable = nil;
}}

void Graphics::setActive(bool enable)
{
	flushBatchedDraws();
	active = enable;
}

void Graphics::setShaderChanged()
{
	dirtyRenderState |= STATEBIT_SHADER;
	++shaderSwitches;
}

id<MTLCommandBuffer> Graphics::useCommandBuffer()
{
	if (commandBuffer == nil)
	{
		commandBuffer = [commandQueue commandBuffer];
		activeCommandBuffers.push_back(commandBuffer);
	}

	return commandBuffer;
}

void Graphics::processCompletedCommandBuffers()
{
	for (int i = (int) activeCommandBuffers.size() - 1; i >= 0; i--)
	{
		auto status = activeCommandBuffers[i].status;
		if (status == MTLCommandBufferStatusCompleted || status == MTLCommandBufferStatusError)
			activeCommandBuffers.erase(activeCommandBuffers.begin() + i);
	}
}

void Graphics::submitCommandBuffer(SubmitType type)
{
	submitAllEncoders(type);

	if (commandBuffer != nil)
	{
		[commandBuffer commit];
		commandBuffer = nil;
	}
}

void Graphics::submitAllEncoders(SubmitType type)
{
	submitRenderEncoder(type);
	submitBlitEncoder();
	submitComputeEncoder();
}

static inline void setAttachment(const Graphics::RenderTarget &rt, MTLRenderPassAttachmentDescriptor *desc, MTLStoreAction &storeaction, bool setload = true)
{
	bool isvolume = rt.texture->getTextureType() == TEXTURE_VOLUME;

	desc.texture = getMTLRenderTarget(rt.texture);
	desc.level = rt.mipmap;
	desc.slice = isvolume ? 0 : rt.slice;
	desc.depthPlane = isvolume ? rt.slice : 0;

	if (setload)
	{
		// Default to load until clear or discard is called.
		desc.loadAction = MTLLoadActionLoad;
	}

	desc.storeAction = MTLStoreActionUnknown;
	storeaction = MTLStoreActionStore;

	desc.resolveTexture = nil;

	if (rt.texture->getMSAA() > 1 && rt.texture->isReadable())
	{
		storeaction = MTLStoreActionStoreAndMultisampleResolve;
		desc.resolveTexture = getMTLTexture(rt.texture);
	}
}

id<MTLRenderCommandEncoder> Graphics::useRenderEncoder()
{
	if (renderEncoder == nil)
	{
		submitAllEncoders(SUBMIT_STORE);

		// Pass desc info for non-backbuffer render targets are set up in
		// setRenderTargetsInternal.
		const auto &rts = states.back().renderTargets;
		if (rts.getFirstTarget().texture.get() == nullptr)
		{
			if (activeDrawable == nil)
			{
				// This is reset to YES after each frame.
				// TODO: Does setting this reallocate memory?
				if (!pendingScreenshotCallbacks.empty())
					metalLayer.framebufferOnly = NO;

				activeDrawable = [metalLayer nextDrawable];
			}

			if (backbufferMSAA.get())
			{
				attachmentStoreActions.color[0] = MTLStoreActionMultisampleResolve;
				passDesc.colorAttachments[0].texture = getMTLRenderTarget(backbufferMSAA);
				passDesc.colorAttachments[0].resolveTexture = activeDrawable.texture;
			}
			else
			{
				attachmentStoreActions.color[0] = MTLStoreActionStore;
				passDesc.colorAttachments[0].texture = activeDrawable.texture;
				passDesc.colorAttachments[0].resolveTexture = nil;
			}

			passDesc.colorAttachments[0].storeAction = MTLStoreActionUnknown;
			passDesc.colorAttachments[0].level = 0;
			passDesc.colorAttachments[0].slice = 0;
			passDesc.colorAttachments[0].depthPlane = 0;

			RenderTarget rt(backbufferDepthStencil);
			if (rt.texture != nullptr && isPixelFormatDepth(rt.texture->getPixelFormat()))
				setAttachment(rt, passDesc.depthAttachment, attachmentStoreActions.depth, false);
			if (rt.texture != nullptr && isPixelFormatStencil(rt.texture->getPixelFormat()))
				setAttachment(rt, passDesc.stencilAttachment, attachmentStoreActions.stencil, false);
			attachmentStoreActions.depth = MTLStoreActionDontCare;
			attachmentStoreActions.stencil = MTLStoreActionDontCare;

			auto &key = lastRenderPipelineKey;
			key.colorRenderTargetFormats = isGammaCorrect() ? PIXELFORMAT_BGRA8_sRGB : PIXELFORMAT_BGRA8_UNORM;
			if (backbufferDepthStencil.get())
				key.depthStencilFormat = backbufferDepthStencil->getPixelFormat();
			else
				key.depthStencilFormat = PIXELFORMAT_UNKNOWN;
			key.msaa = backbufferMSAA ? (uint8) backbufferMSAA->getMSAA() : 1;
		}

		renderEncoder = [useCommandBuffer() renderCommandEncoderWithDescriptor:passDesc];

		renderBindings = {};

		id<MTLBuffer> defaultbuffer = getMTLBuffer(defaultAttributesBuffer);
		setBuffer(renderEncoder, renderBindings, SHADERSTAGE_VERTEX, DEFAULT_VERTEX_BUFFER_BINDING, defaultbuffer, 0);

		dirtyRenderState = STATEBIT_ALL;
		lastCullMode = CULL_MAX_ENUM;
	}

	return renderEncoder;
}

void Graphics::submitRenderEncoder(SubmitType type)
{
	if (renderEncoder != nil)
	{
		bool store = type == SUBMIT_STORE;
		const auto &actions = attachmentStoreActions;
		const auto &rts = states.back().renderTargets;
		bool isbackbuffer = rts.getFirstTarget().texture.get() == nullptr;

		if (isbackbuffer)
			[renderEncoder setColorStoreAction:(store ? MTLStoreActionStore : actions.color[0]) atIndex:0];

		for (size_t i = 0; i < rts.colors.size(); i++)
			[renderEncoder setColorStoreAction:(store ? MTLStoreActionStore : actions.color[i]) atIndex:i];

		love::graphics::Texture *ds = rts.depthStencil.texture.get();
		if (isbackbuffer)
			ds = backbufferDepthStencil;

		if ((rts.temporaryRTFlags & TEMPORARY_RT_DEPTH) != 0 || (ds != nullptr && isPixelFormatDepth(ds->getPixelFormat())))
			[renderEncoder setDepthStoreAction:store ? MTLStoreActionStore : actions.depth];

		if ((rts.temporaryRTFlags & TEMPORARY_RT_STENCIL) != 0 || (ds != nullptr && isPixelFormatStencil(ds->getPixelFormat())))
			[renderEncoder setStencilStoreAction:store ? MTLStoreActionStore : actions.stencil];

		[renderEncoder endEncoding];
		renderEncoder = nil;

		// Reset actions to load. The next clear/discard/etc will set more
		// appropriate actions if necessary.
		for (int i = 0; i < MAX_COLOR_RENDER_TARGETS; i++)
		{
			passDesc.colorAttachments[i].loadAction = MTLLoadActionLoad;
			if (type == SUBMIT_DONE)
			{
				passDesc.colorAttachments[i].texture = nil;
				passDesc.colorAttachments[i].resolveTexture = nil;
			}
		}

		passDesc.depthAttachment.loadAction = MTLLoadActionLoad;
		passDesc.stencilAttachment.loadAction = MTLLoadActionLoad;
		if (type == SUBMIT_DONE)
		{
			passDesc.depthAttachment.texture = nil;
			passDesc.depthAttachment.resolveTexture = nil;
			passDesc.stencilAttachment.texture = nil;
			passDesc.stencilAttachment.resolveTexture = nil;
		}
	}
}

id<MTLBlitCommandEncoder> Graphics::useBlitEncoder()
{
	if (blitEncoder == nil)
	{
		submitAllEncoders(SUBMIT_STORE);
		blitEncoder = [useCommandBuffer() blitCommandEncoder];
	}

	return blitEncoder;
}

void Graphics::submitBlitEncoder()
{
	if (blitEncoder != nil)
	{
		[blitEncoder endEncoding];
		blitEncoder = nil;
	}
}

id<MTLComputeCommandEncoder> Graphics::useComputeEncoder()
{
	if (computeEncoder == nil)
	{
		submitAllEncoders(SUBMIT_STORE);
		computeEncoder = [useCommandBuffer() computeCommandEncoder];
		renderBindings = {};
	}

	return computeEncoder;
}

void Graphics::submitComputeEncoder()
{
	if (computeEncoder != nil)
	{
		[computeEncoder endEncoding];
		computeEncoder = nil;
	}
}

static bool isClampOne(SamplerState::WrapMode w)
{
	return w == SamplerState::WRAP_CLAMP_ONE;
}

id<MTLSamplerState> Graphics::getCachedSampler(const SamplerState &s)
{ @autoreleasepool {
	uint64 key = s.toKey();

	auto it = cachedSamplers.find(key);
	if (it != cachedSamplers.end())
		return (__bridge id<MTLSamplerState>) it->second;

	MTLSamplerDescriptor *desc = [MTLSamplerDescriptor new];

	desc.minFilter = getMTLSamplerFilter(s.minFilter);
	desc.magFilter = getMTLSamplerFilter(s.magFilter);
	desc.mipFilter = getMTLSamplerMipFilter(s.mipmapFilter);
	desc.maxAnisotropy = std::max(1.0f, std::min((float)s.maxAnisotropy, 16.0f));

	desc.sAddressMode = getMTLSamplerAddressMode(s.wrapU);
	desc.tAddressMode = getMTLSamplerAddressMode(s.wrapV);
	desc.rAddressMode = getMTLSamplerAddressMode(s.wrapW);

	if (isClampOne(s.wrapU) || isClampOne(s.wrapV) || isClampOne(s.wrapW))
	{
		if (capabilities.features[FEATURE_CLAMP_ONE])
			desc.borderColor = MTLSamplerBorderColorOpaqueWhite;
		else
		{
			if (isClampOne(s.wrapU))
				desc.sAddressMode = MTLSamplerAddressModeClampToZero;
			if (isClampOne(s.wrapV))
				desc.tAddressMode = MTLSamplerAddressModeClampToZero;
			if (isClampOne(s.wrapW))
				desc.rAddressMode = MTLSamplerAddressModeClampToZero;
		}
	}

	desc.lodMinClamp = s.minLod;
	desc.lodMaxClamp = s.maxLod;

	// This isn't supported on some older iOS devices. Texture code checks for support.
	if (s.depthSampleMode.hasValue)
		desc.compareFunction = getMTLCompareFunction(getReversedCompareMode(s.depthSampleMode.value));

	id<MTLSamplerState> sampler = [device newSamplerStateWithDescriptor:desc];

	if (sampler != nil)
		cachedSamplers[key] = (void *) CFBridgingRetain(sampler);

	return sampler;
}}

bool Graphics::isDepthCompareSamplerSupported() const
{
	return families.mac[1] || families.macCatalyst[1] || families.apple[3];
}

id<MTLDepthStencilState> Graphics::getCachedDepthStencilState(const DepthState &depth, const StencilState &stencil)
{
	uint64 key = (depth.compare << 0) | ((uint32)depth.write << 8)
		| (stencil.action << 16) | (stencil.compare << 24)
		| ((uint64)std::max(0, std::min(255, stencil.value)) << 32)
		| ((uint64)std::min(255u, stencil.readMask) << 40)
		| ((uint64)std::min(255u, stencil.writeMask) << 48);

	auto it = cachedDepthStencilStates.find(key);
	if (it != cachedDepthStencilStates.end())
		return (__bridge id<MTLDepthStencilState>) it->second;

	MTLStencilDescriptor *stencildesc = [MTLStencilDescriptor new];

	/**
	 * GPUs do the comparison opposite to what makes sense for love's API. For
	 * example, if the compare function is GREATER then the stencil test will
	 * pass if the reference value is greater than the value in the stencil
	 * buffer. With our API it's more intuitive to assume that
	 * setStencilState(STENCIL_KEEP, COMPARE_GREATER, 4) will make it pass if the
	 * stencil buffer has a value greater than 4.
	 **/
	stencildesc.stencilCompareFunction = getMTLCompareFunction(getReversedCompareMode(stencil.compare));
	stencildesc.stencilFailureOperation = MTLStencilOperationKeep;
	stencildesc.depthFailureOperation = MTLStencilOperationKeep;
	stencildesc.depthStencilPassOperation = getMTLStencilOperation(stencil.action);
	stencildesc.readMask = stencil.readMask;
	stencildesc.writeMask = stencil.writeMask;

	MTLDepthStencilDescriptor *desc = [MTLDepthStencilDescriptor new];

	desc.depthCompareFunction = getMTLCompareFunction(depth.compare);
	desc.depthWriteEnabled = depth.write;
	desc.frontFaceStencil = stencildesc;
	desc.backFaceStencil = stencildesc;

	id<MTLDepthStencilState> mtlstate = [device newDepthStencilStateWithDescriptor:desc];

	if (mtlstate != nil)
		cachedDepthStencilStates[key] = (void *) CFBridgingRetain(mtlstate);

	return mtlstate;
}

void Graphics::applyRenderState(id<MTLRenderCommandEncoder> encoder, VertexAttributesID attributesID)
{
	const uint32 pipelineStateBits = STATEBIT_SHADER | STATEBIT_BLEND | STATEBIT_COLORMASK;

	uint32 dirtyState = dirtyRenderState;
	const auto &state = states.back();

	if (dirtyState & (STATEBIT_VIEWPORT | STATEBIT_SCISSOR))
	{
		int rtw = 0;
		int rth = 0;

		const auto &rt = state.renderTargets.getFirstTarget();
		if (rt.texture.get())
		{
			rtw = rt.texture->getPixelWidth(rt.mipmap);
			rth = rt.texture->getPixelHeight(rt.mipmap);
		}
		else
		{
			rtw = getPixelWidth();
			rth = getPixelHeight();
		}

		if (dirtyState & STATEBIT_VIEWPORT)
		{
			MTLViewport view;
			view.originX = 0.0;
			view.originY = 0.0;
			view.width = rtw;
			view.height = rth;
			view.znear = 0.0;
			view.zfar = 1.0;
			[encoder setViewport:view];
		}

		MTLScissorRect rect = {0, 0, (NSUInteger)rtw, (NSUInteger)rth};

		if (state.scissor)
		{
			double dpiscale = getCurrentDPIScale();
			rect.x = (NSUInteger)roundf(state.scissorRect.x*dpiscale);
			rect.y = (NSUInteger)roundf(state.scissorRect.y*dpiscale);
			rect.width = (NSUInteger)roundf(state.scissorRect.w*dpiscale);
			rect.height = (NSUInteger)roundf(state.scissorRect.h*dpiscale);

			if (rtw > 0 && (int)rect.x >= rtw)
				rect.x = rtw - 1;
			if (rth > 0 && (int)rect.y >= rth)
				rect.y = rth - 1;

			rect.width = std::min(rect.width, rtw - rect.x);
			rect.height = std::min(rect.height, rth - rect.y);
		}

		[encoder setScissorRect:rect];
	}

	if (dirtyState & STATEBIT_FACEWINDING)
	{
		auto winding = state.winding == WINDING_CCW ? MTLWindingCounterClockwise : MTLWindingClockwise;
		[encoder setFrontFacingWinding:winding];
	}

	if (dirtyState & STATEBIT_WIREFRAME)
	{
		auto mode = state.wireframe ? MTLTriangleFillModeLines : MTLTriangleFillModeFill;
		[encoder setTriangleFillMode:mode];
	}

	if (dirtyState & STATEBIT_CULLMODE)
	{
		auto mode = lastCullMode == CULL_BACK ? MTLCullModeBack :
			lastCullMode == CULL_FRONT ? MTLCullModeFront : MTLCullModeNone;
		[encoder setCullMode:mode];
	}

	if ((dirtyState & pipelineStateBits) != 0 || attributesID != lastRenderPipelineKey.vertexAttributesID)
	{
		auto &key = lastRenderPipelineKey;

		key.vertexAttributesID = attributesID;

		Shader *shader = (Shader *) Shader::current;
		id<MTLRenderPipelineState> pipeline = nil;

		if (shader)
		{
			key.blendStateKey = state.blend.toKey();
			key.colorChannelMask = state.colorMask;

			pipeline = shader->getCachedRenderPipeline(this, key);
		}

		[encoder setRenderPipelineState:pipeline];
	}

	if (dirtyState & (STATEBIT_DEPTH | STATEBIT_STENCIL))
	{
		DepthState depth;
		depth.compare = state.depthTest;
		depth.write = state.depthWrite;

		id<MTLDepthStencilState> mtlstate = getCachedDepthStencilState(depth, state.stencil);

		[encoder setDepthStencilState:mtlstate];
	}

	if (dirtyState & STATEBIT_STENCIL)
		[encoder setStencilReferenceValue:state.stencil.value];

	dirtyRenderState = 0;
}

bool Graphics::applyShaderUniforms(id<MTLComputeCommandEncoder> encoder, love::graphics::Shader *shader)
{
	Shader *s = (Shader *)shader;

#if defined(LOVE_MACOS) || TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
	size_t alignment = 256;
#else
	size_t alignment = 16;
#endif

	size_t size = s->getLocalUniformBufferSize();
	uint8 *bufferdata = s->getLocalUniformBufferData();

	if (uniformBuffer->getSize() < uniformBufferOffset + size)
	{
		size_t newsize = uniformBuffer->getSize() * 2;
		if (uniformBufferOffset > 0)
			uniformBuffer->nextFrame();
		uniformBuffer->release();
		uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_UNIFORM, newsize);
		uniformBufferData = {};
		uniformBufferOffset = 0;
	}

	if (uniformBufferData.data == nullptr)
	{
		uniformBufferData = uniformBuffer->map(uniformBuffer->getSize());
		uniformBufferGPUStart = uniformBuffer->getGPUReadOffset();
	}

	memcpy(uniformBufferData.data + uniformBufferOffset, bufferdata, size);

	id<MTLBuffer> buffer = getMTLBuffer(uniformBuffer);
	int uniformindex = Shader::getUniformBufferBinding();

	auto &bindings = renderBindings;
	setBuffer(encoder, bindings, uniformindex, buffer, uniformBufferGPUStart + uniformBufferOffset);

	uniformBufferOffset += alignUp(size, alignment);

	bool allWritableVariablesSet = true;

	for (const Shader::TextureBinding &b : s->getTextureBindings())
	{
		id<MTLTexture> texture = b.texture;
		auto samplertex = b.samplerTexture;

		uint8 texindex = b.textureStages[SHADERSTAGE_COMPUTE];
		uint8 sampindex = b.samplerStages[SHADERSTAGE_COMPUTE];

		if (texindex != LOVE_UINT8_MAX)
		{
			setTexture(encoder, bindings, texindex, texture);
			if ((b.access & Shader::ACCESS_WRITE) != 0 && texture == nil)
				allWritableVariablesSet = false;
		}

		if (sampindex != LOVE_UINT8_MAX)
			setSampler(encoder, bindings, sampindex, samplertex);
	}

	for (const Shader::BufferBinding &b : s->getBufferBindings())
	{
		uint8 index = b.stages[SHADERSTAGE_COMPUTE];
		if (index != LOVE_UINT8_MAX)
		{
			setBuffer(encoder, bindings, index, b.buffer, 0);
			if ((b.access & Shader::ACCESS_WRITE) != 0 && b.buffer == nil)
				allWritableVariablesSet = false;
		}
	}

	return allWritableVariablesSet;
}

bool Graphics::applyShaderUniforms(id<MTLRenderCommandEncoder> renderEncoder, love::graphics::Shader *shader, love::graphics::Texture *maintex)
{
	Shader *s = (Shader *)shader;

#if defined(LOVE_MACOS) || TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST
	size_t alignment = 256;
#else
	size_t alignment = 16;
#endif

	size_t size = s->getLocalUniformBufferSize();
	uint8 *bufferdata = s->getLocalUniformBufferData();

	auto builtins = (Shader::BuiltinUniformData *) (bufferdata + s->getBuiltinUniformDataOffset());

	builtins->transformMatrix = getTransform();
	builtins->projectionMatrix = getDeviceProjection();

	builtins->scaleParams.x = (float) getCurrentDPIScale();
	builtins->scaleParams.y = getPointSize();

	uint32 flags = Shader::CLIP_TRANSFORM_Z_NEG1_1_TO_0_1;
	builtins->clipSpaceParams = Shader::computeClipSpaceParams(flags);

	builtins->screenSizeParams = Vector4(getPixelWidth(), getPixelHeight(), 1.0f, 0.0f);
	auto rt = states.back().renderTargets.getFirstTarget();
	if (rt.texture.get())
	{
		builtins->screenSizeParams.x = rt.texture->getPixelWidth(rt.mipmap);
		builtins->screenSizeParams.y = rt.texture->getPixelHeight(rt.mipmap);
	}

	builtins->constantColor = getColor();
	gammaCorrectColor(builtins->constantColor);

	if (uniformBuffer->getSize() < uniformBufferOffset + size)
	{
		size_t newsize = uniformBuffer->getSize() * 2;
		if (uniformBufferOffset > 0)
			uniformBuffer->nextFrame();
		uniformBuffer->release();
		uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_UNIFORM, newsize);
		uniformBufferData = {};
		uniformBufferOffset = 0;
	}

	if (uniformBufferData.data == nullptr)
	{
		uniformBufferData = uniformBuffer->map(uniformBuffer->getSize());
		uniformBufferGPUStart = uniformBuffer->getGPUReadOffset();
	}

	memcpy(uniformBufferData.data + uniformBufferOffset, bufferdata, size);

	id<MTLBuffer> buffer = getMTLBuffer(uniformBuffer);
	int uniformindex = Shader::getUniformBufferBinding();

	auto &bindings = renderBindings;
	setBuffer(renderEncoder, bindings, SHADERSTAGE_VERTEX, uniformindex, buffer, uniformBufferGPUStart + uniformBufferOffset);
	setBuffer(renderEncoder, bindings, SHADERSTAGE_PIXEL, uniformindex, buffer, uniformBufferGPUStart + uniformBufferOffset);

	uniformBufferOffset += alignUp(size, alignment);

	bool allWritableVariablesSet = true;

	for (const Shader::TextureBinding &b : s->getTextureBindings())
	{
		id<MTLTexture> texture = b.texture;
		auto samplertex = b.samplerTexture;

		if (b.isMainTexture)
		{
			texture = getMTLTexture(maintex);
			samplertex = maintex;
		}

		uint8 texindex = b.textureStages[SHADERSTAGE_VERTEX];
		uint8 sampindex = b.samplerStages[SHADERSTAGE_VERTEX];

		if (texindex != LOVE_UINT8_MAX)
		{
			setTexture(renderEncoder, bindings, SHADERSTAGE_VERTEX, texindex, texture);

			if ((b.access & Shader::ACCESS_WRITE) != 0 && texture == nil)
				allWritableVariablesSet = false;
		}

		if (sampindex != LOVE_UINT8_MAX)
			setSampler(renderEncoder, bindings, SHADERSTAGE_VERTEX, sampindex, samplertex);

		texindex = b.textureStages[SHADERSTAGE_PIXEL];
		sampindex = b.samplerStages[SHADERSTAGE_PIXEL];

		if (texindex != LOVE_UINT8_MAX)
		{
			setTexture(renderEncoder, bindings, SHADERSTAGE_PIXEL, texindex, texture);

			if ((b.access & Shader::ACCESS_WRITE) != 0 && texture == nil)
				allWritableVariablesSet = false;
		}

		if (sampindex != LOVE_UINT8_MAX)
			setSampler(renderEncoder, bindings, SHADERSTAGE_PIXEL, sampindex, samplertex);
	}

	for (const Shader::BufferBinding &b : s->getBufferBindings())
	{
		uint8 index = b.stages[SHADERSTAGE_VERTEX];
		if (index != LOVE_UINT8_MAX)
		{
			setBuffer(renderEncoder, bindings, SHADERSTAGE_VERTEX, index, b.buffer, 0);

			if ((b.access & Shader::ACCESS_WRITE) != 0 && b.buffer == nil)
				allWritableVariablesSet = false;
		}

		index = b.stages[SHADERSTAGE_PIXEL];
		if (index != LOVE_UINT8_MAX)
		{
			setBuffer(renderEncoder, bindings, SHADERSTAGE_PIXEL, index, b.buffer, 0);

			if ((b.access & Shader::ACCESS_WRITE) != 0 && b.buffer == nil)
				allWritableVariablesSet = false;
		}
	}

	return allWritableVariablesSet;
}

static void setVertexBuffers(id<MTLRenderCommandEncoder> encoder, love::graphics::Shader *shader, const BufferBindings *buffers, Graphics::RenderEncoderBindings &bindings)
{
	Shader *s = (Shader *)shader;
	int firstBinding = s->getFirstVertexBufferBinding();
	uint32 allbits = buffers->useBits;
	uint32 i = 0;
	while (allbits)
	{
		uint32 bit = 1u << i;

		if (buffers->useBits & bit)
		{
			auto b = buffers->info[i];
			id<MTLBuffer> buffer = getMTLBuffer(b.buffer);
			setBuffer(encoder, bindings, SHADERSTAGE_VERTEX, firstBinding + i, buffer, b.offset);
		}

		i++;
		allbits >>= 1;
	}
}

void Graphics::draw(const DrawCommand &cmd)
{ @autoreleasepool {
	id<MTLRenderCommandEncoder> encoder = useRenderEncoder();

	if (cmd.cullMode != lastCullMode)
	{
		lastCullMode = cmd.cullMode;
		dirtyRenderState |= STATEBIT_CULLMODE;
	}

	applyRenderState(encoder, cmd.attributesID);
	if (!applyShaderUniforms(encoder, Shader::current, cmd.texture))
		return;

	setVertexBuffers(encoder, Shader::current, cmd.buffers, renderBindings);

	if (cmd.indirectBuffer != nullptr)
	{
		[encoder drawPrimitives:getMTLPrimitiveType(cmd.primitiveType)
				 indirectBuffer:getMTLBuffer(cmd.indirectBuffer)
		   indirectBufferOffset:cmd.indirectBufferOffset];
	}
	else
	{
		[encoder drawPrimitives:getMTLPrimitiveType(cmd.primitiveType)
					vertexStart:cmd.vertexStart
					vertexCount:cmd.vertexCount
				  instanceCount:cmd.instanceCount];
	}

	++drawCalls;
}}

void Graphics::draw(const DrawIndexedCommand &cmd)
{ @autoreleasepool {
	id<MTLRenderCommandEncoder> encoder = useRenderEncoder();

	if (cmd.cullMode != lastCullMode)
	{
		lastCullMode = cmd.cullMode;
		dirtyRenderState |= STATEBIT_CULLMODE;
	}

	applyRenderState(encoder, cmd.attributesID);
	if (!applyShaderUniforms(encoder, Shader::current, cmd.texture))
		return;

	setVertexBuffers(encoder, Shader::current, cmd.buffers, renderBindings);

	auto indexType = cmd.indexType == INDEX_UINT32 ? MTLIndexTypeUInt32 : MTLIndexTypeUInt16;

	if (cmd.indirectBuffer != nullptr)
	{
		[encoder drawIndexedPrimitives:getMTLPrimitiveType(cmd.primitiveType)
							 indexType:indexType
						   indexBuffer:getMTLBuffer(cmd.indexBuffer)
					 indexBufferOffset:cmd.indexBufferOffset
						indirectBuffer:getMTLBuffer(cmd.indirectBuffer)
				  indirectBufferOffset:cmd.indexBufferOffset];
	}
	else
	{
		[encoder drawIndexedPrimitives:getMTLPrimitiveType(cmd.primitiveType)
							indexCount:cmd.indexCount
							 indexType:indexType
						   indexBuffer:getMTLBuffer(cmd.indexBuffer)
					 indexBufferOffset:cmd.indexBufferOffset
						 instanceCount:cmd.instanceCount];
	}

	++drawCalls;
}}

static inline void advanceVertexOffsets(const VertexAttributes &attributes, BufferBindings &buffers, int vertexcount)
{
	// TODO: Figure out a better way to avoid touching the same buffer multiple
	// times, if multiple attributes share the buffer.
	uint32 touchedbuffers = 0;

	for (unsigned int i = 0; i < VertexAttributes::MAX; i++)
	{
		if (!attributes.isEnabled(i))
			continue;

		auto &attrib = attributes.attribs[i];

		uint32 bufferbit = 1u << attrib.bufferIndex;
		if ((touchedbuffers & bufferbit) == 0)
		{
			touchedbuffers |= bufferbit;
			const auto &layout = attributes.bufferLayouts[attrib.bufferIndex];
			buffers.info[attrib.bufferIndex].offset += layout.stride * vertexcount;
		}
	}
}

void Graphics::drawQuads(int start, int count, VertexAttributesID attributesID, const BufferBindings &buffers, love::graphics::Texture *texture)
{ @autoreleasepool {
	const int MAX_VERTICES_PER_DRAW = LOVE_UINT16_MAX;
	const int MAX_QUADS_PER_DRAW    = MAX_VERTICES_PER_DRAW / 4;

	id<MTLRenderCommandEncoder> encoder = useRenderEncoder();

	if (lastCullMode != CULL_NONE)
	{
		lastCullMode = CULL_NONE;
		dirtyRenderState |= STATEBIT_CULLMODE;
	}

	applyRenderState(encoder, attributesID);
	if (!applyShaderUniforms(encoder, Shader::current, texture))
		return;

	id<MTLBuffer> ib = getMTLBuffer(quadIndexBuffer);

	// Some older iOS devices don't support base vertex rendering.
	if (families.apple[3] || families.mac[1] || families.macCatalyst[1])
	{
		setVertexBuffers(encoder, Shader::current, &buffers, renderBindings);

		int basevertex = start * 4;

		for (int quadindex = 0; quadindex < count; quadindex += MAX_QUADS_PER_DRAW)
		{
			int quadcount = std::min(MAX_QUADS_PER_DRAW, count - quadindex);

			[encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
								indexCount:quadcount * 6
								 indexType:MTLIndexTypeUInt16
							   indexBuffer:ib
						 indexBufferOffset:0
							 instanceCount:1
								baseVertex:basevertex
							  baseInstance:0];

			++drawCalls;
			basevertex += quadcount * 4;
		}
	}
	else
	{
		VertexAttributes attributes;
		findVertexAttributes(attributesID, attributes);

		BufferBindings bufferscopy = buffers;
		if (start > 0)
			advanceVertexOffsets(attributes, bufferscopy, start * 4);

		for (int quadindex = 0; quadindex < count; quadindex += MAX_QUADS_PER_DRAW)
		{
			setVertexBuffers(encoder, Shader::current, &bufferscopy, renderBindings);

			int quadcount = std::min(MAX_QUADS_PER_DRAW, count - quadindex);

			[encoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle
								indexCount:quadcount * 6
								 indexType:MTLIndexTypeUInt16
							   indexBuffer:ib
						 indexBufferOffset:0];

			++drawCalls;

			if (count > MAX_QUADS_PER_DRAW)
				advanceVertexOffsets(attributes, bufferscopy, quadcount * 4);
		}
	}
}}

bool Graphics::dispatch(love::graphics::Shader *s, int x, int y, int z)
{ @autoreleasepool {
	auto shader = (Shader *) s;

	int tX, tY, tZ;
	shader->getLocalThreadgroupSize(&tX, &tY, &tZ);

	id<MTLComputePipelineState> pipeline = shader->getComputePipeline();
	if (pipeline == nil)
		return false;

	id<MTLComputeCommandEncoder> computeEncoder = useComputeEncoder();

	if (!applyShaderUniforms(computeEncoder, shader))
		return false;

	// TODO: track this state?
	[computeEncoder setComputePipelineState:pipeline];

	[computeEncoder dispatchThreadgroups:MTLSizeMake(x, y, z)
				   threadsPerThreadgroup:MTLSizeMake(tX, tY, tZ)];

	return true;
}}

bool Graphics::dispatch(love::graphics::Shader *s, love::graphics::Buffer *indirectargs, size_t argsoffset)
{
	auto shader = (Shader *) s;

	int tX, tY, tZ;
	shader->getLocalThreadgroupSize(&tX, &tY, &tZ);

	id<MTLComputePipelineState> pipeline = shader->getComputePipeline();
	if (pipeline == nil)
		return false;

	id<MTLComputeCommandEncoder> computeEncoder = useComputeEncoder();

	if (!applyShaderUniforms(computeEncoder, shader))
		return false;

	// TODO: track this state?
	[computeEncoder setComputePipelineState:pipeline];

	[computeEncoder dispatchThreadgroupsWithIndirectBuffer:getMTLBuffer(indirectargs)
									  indirectBufferOffset:argsoffset
									 threadsPerThreadgroup:MTLSizeMake(tX, tY, tZ)];

	return true;
}

void Graphics::setRenderTargetsInternal(const RenderTargets &rts, int /*pixelw*/, int /*pixelh*/, bool /*hasSRGBtexture*/)
{ @autoreleasepool {
	endPass(false);

	bool isbackbuffer = rts.getFirstTarget().texture == nullptr;

	// Set up render pass descriptor for the next useRenderEncoder call.
	// The backbuffer will be set up in useRenderEncoder rather than here.
	for (size_t i = 0; i < rts.colors.size(); i++)
	{
		auto desc = passDesc.colorAttachments[i];
		setAttachment(rts.colors[i], desc, attachmentStoreActions.color[i]);
		passDesc.colorAttachments[i] = desc;
	}

	for (size_t i = rts.colors.size(); i < MAX_COLOR_RENDER_TARGETS; i++)
	{
		passDesc.colorAttachments[i] = nil;
		passDesc.colorAttachments[i].loadAction = MTLLoadActionLoad;
	}

	passDesc.depthAttachment = nil;
	passDesc.depthAttachment.loadAction = MTLLoadActionLoad;
	passDesc.stencilAttachment = nil;
	passDesc.stencilAttachment.loadAction = MTLLoadActionLoad;

	auto ds = rts.depthStencil.texture;
	if (isbackbuffer && ds == nullptr)
		ds = backbufferDepthStencil;

	PixelFormat dsformat = PIXELFORMAT_UNKNOWN;
	if (ds != nullptr)
	{
		RenderTarget rt = rts.depthStencil;
		rt.texture = ds;
		dsformat = ds->getPixelFormat();

		if (isPixelFormatDepth(dsformat))
			setAttachment(rt, passDesc.depthAttachment, attachmentStoreActions.depth);

		if (isPixelFormatStencil(dsformat))
			setAttachment(rt, passDesc.stencilAttachment, attachmentStoreActions.stencil);
	}

	if (!isbackbuffer)
	{
		lastRenderPipelineKey.colorRenderTargetFormats = 0;
		for (size_t i = 0; i < rts.colors.size(); i++)
			lastRenderPipelineKey.colorRenderTargetFormats |= (rts.colors[i].texture->getPixelFormat()) << (8 * i);

		lastRenderPipelineKey.msaa = (uint8) rts.getFirstTarget().texture->getMSAA();
	}

	lastRenderPipelineKey.depthStencilFormat = dsformat;
	lastRenderPipelineKey.vertexAttributesID = VertexAttributesID();

	dirtyRenderState = STATEBIT_ALL;
}}

void Graphics::endPass(bool presenting)
{
	// Make sure the encoder gets set up, if nothing else has done it yet.
	useRenderEncoder();

	flushBatchedDraws();

	auto &rts = states.back().renderTargets;
	love::graphics::Texture *depthstencil = rts.depthStencil.texture.get();

	// Discard the depth/stencil buffer if we're using an internal cached one,
	// or if we're presenting the backbuffer to the display.
	if ((depthstencil == nullptr && (rts.temporaryRTFlags & (TEMPORARY_RT_DEPTH | TEMPORARY_RT_STENCIL)) != 0)
		|| (presenting && !rts.getFirstTarget().texture.get()))
	{
		attachmentStoreActions.depth = MTLStoreActionDontCare;
		attachmentStoreActions.stencil = MTLStoreActionDontCare;
	}

	submitRenderEncoder(SUBMIT_DONE);
}

void Graphics::clear(OptionalColorD c, OptionalInt stencil, OptionalDouble depth)
{ @autoreleasepool {
	bool activateRenderEncoder = false;

	if (c.hasValue || stencil.hasValue || depth.hasValue)
	{
		flushBatchedDraws();

		// Handle clearing mid-pass by starting a new pass.
		if (renderEncoder != nil)
		{
			submitRenderEncoder(SUBMIT_STORE);
			activateRenderEncoder = true;
		}
	}

	if (c.hasValue)
	{
		Colorf cf((float)c.value.r, (float)c.value.g, (float)c.value.b, (float)c.value.a);
		gammaCorrectColor(cf);
		auto color = MTLClearColorMake(cf.r, cf.g, cf.b, cf.a);
		for (int i = 0; i < MAX_COLOR_RENDER_TARGETS; i++)
		{
			passDesc.colorAttachments[i].clearColor = color;
			passDesc.colorAttachments[i].loadAction = MTLLoadActionClear;
		}
	}

	if (stencil.hasValue)
	{
		passDesc.stencilAttachment.clearStencil = stencil.value;
		passDesc.stencilAttachment.loadAction = MTLLoadActionClear;
	}

	if (depth.hasValue)
	{
		passDesc.depthAttachment.clearDepth = depth.value;
		passDesc.depthAttachment.loadAction = MTLLoadActionClear;
	}

	if (activateRenderEncoder)
		useRenderEncoder();
}}

void Graphics::clear(const std::vector<OptionalColorD> &colors, OptionalInt stencil, OptionalDouble depth)
{ @autoreleasepool {
	if (colors.size() == 0 && !stencil.hasValue && !depth.hasValue)
		return;

	bool activateRenderEncoder = false;

	int ncolorcanvases = (int) states.back().renderTargets.colors.size();
	int ncolors = (int) colors.size();

	if (ncolors <= 1 && ncolorcanvases <= 1)
	{
		clear(ncolors > 0 ? colors[0] : OptionalColorD(), stencil, depth);
		return;
	}

	flushBatchedDraws();

	// Handle clearing mid-pass by starting a new pass.
	if (renderEncoder != nil)
	{
		submitRenderEncoder(SUBMIT_STORE);
		activateRenderEncoder = true;
	}

	for (int i = 0; i < ncolors; i++)
	{
		if (!colors[i].hasValue)
			continue;

		const ColorD &cd = colors[i].value;
		Colorf cf((float)cd.r, (float)cd.g, (float)cd.b, (float)cd.a);
		gammaCorrectColor(cf);

		passDesc.colorAttachments[i].clearColor = MTLClearColorMake(cf.r, cf.g, cf.b, cf.a);
		passDesc.colorAttachments[i].loadAction = MTLLoadActionClear;
	}

	if (stencil.hasValue)
	{
		passDesc.stencilAttachment.clearStencil = stencil.value;
		passDesc.stencilAttachment.loadAction = MTLLoadActionClear;
	}

	if (depth.hasValue)
	{
		passDesc.depthAttachment.clearDepth = depth.value;
		passDesc.depthAttachment.loadAction = MTLLoadActionClear;
	}

	if (activateRenderEncoder)
		useRenderEncoder();
}}

void Graphics::discard(const std::vector<bool> &colorbuffers, bool depthstencil)
{ @autoreleasepool {
	flushBatchedDraws();

	// TODO
	if (renderEncoder != nil)
		return;

	size_t maxcolor = isRenderTargetActive() ? states.back().renderTargets.colors.size() : 1;
	size_t ncolor = std::min(maxcolor, colorbuffers.size());

	for (size_t i = 0; i < ncolor; i++)
	{
		if (colorbuffers[i])
			passDesc.colorAttachments[i].loadAction = MTLLoadActionDontCare;
	}

	if (depthstencil)
	{
		passDesc.stencilAttachment.loadAction = MTLLoadActionDontCare;
		passDesc.depthAttachment.loadAction = MTLLoadActionDontCare;
	}
}}

void Graphics::present(void *screenshotCallbackData)
{ @autoreleasepool {
	if (!isActive())
		return;

	if (isRenderTargetActive())
		throw love::Exception("present cannot be called while a render target is active.");

	deprecations.draw(this);

	// endPass calls useRenderEncoder, which makes sure activeDrawable is set
	// when possible.
	endPass(true);

	id<MTLBuffer> screenshotbuffer = nil;

	int w = activeDrawable.texture.width;
	int h = activeDrawable.texture.height;

	if (!pendingScreenshotCallbacks.empty())
	{
		size_t size = w * h * 4;

		screenshotbuffer = [device newBufferWithLength:size options:MTLResourceStorageModeShared];
		if (screenshotbuffer == nil)
			throw love::Exception("Out of graphics memory.");

		auto blitencoder = useBlitEncoder();

		[blitencoder copyFromTexture:activeDrawable.texture
						 sourceSlice:0
						 sourceLevel:0
						sourceOrigin:MTLOriginMake(0, 0, 0)
						  sourceSize:MTLSizeMake(w, h, 1)
							toBuffer:screenshotbuffer
				   destinationOffset:0
			  destinationBytesPerRow:w * 4
			destinationBytesPerImage:size];

		submitBlitEncoder();
	}

	for (StreamBuffer *buffer : batchedDrawState.vb)
		buffer->nextFrame();
	batchedDrawState.indexBuffer->nextFrame();

	uniformBuffer->nextFrame();
	uniformBufferData = {};
	uniformBufferOffset = 0;

	id<MTLCommandBuffer> cmd = getCommandBuffer();

	if (cmd != nil && activeDrawable != nil)
		[cmd presentDrawable:activeDrawable];

	submitCommandBuffer(SUBMIT_DONE);

	activeDrawable = nil;

	if (!pendingScreenshotCallbacks.empty())
	{
		[cmd waitUntilCompleted];

		size_t size = w * h * 4;

		auto imagemodule = Module::getInstance<love::image::Image>(M_IMAGE);

		for (int i = 0; i < (int) pendingScreenshotCallbacks.size(); i++)
		{
			const auto &info = pendingScreenshotCallbacks[i];
			image::ImageData *img = nullptr;

			try
			{
				img = imagemodule->newImageData(w, h, PIXELFORMAT_RGBA8_UNORM, screenshotbuffer.contents);
			}
			catch (love::Exception &)
			{
				info.callback(&info, nullptr, nullptr);
				for (int j = i + 1; j < (int) pendingScreenshotCallbacks.size(); j++)
				{
					const auto &ninfo = pendingScreenshotCallbacks[j];
					ninfo.callback(&ninfo, nullptr, nullptr);
				}
				pendingScreenshotCallbacks.clear();
				throw;
			}

			uint8 *screenshot = (uint8 *) img->getData();

			// Convert from BGRA to RGBA and replace alpha with full opacity.
			for (size_t i = 0; i < size; i += 4)
			{
				uint8 r = screenshot[i + 2];
				screenshot[i + 2] = screenshot[i + 0];
				screenshot[i + 0] = r;
				screenshot[i + 3] = 255;
			}

			info.callback(&info, img, screenshotCallbackData);
			img->release();
		}

		pendingScreenshotCallbacks.clear();
	}

	auto window = Module::getInstance<love::window::Window>(M_WINDOW);
	if (window != nullptr)
		window->swapBuffers();

	// This is set to NO when there are pending screen captures.
	metalLayer.framebufferOnly = YES;

	// Reset the per-frame stat counts.
	drawCalls = 0;
	shaderSwitches = 0;
	renderTargetSwitchCount = 0;
	drawCallsBatched = 0;

	updatePendingReadbacks();
	updateTemporaryResources();
	processCompletedCommandBuffers();
}}

int Graphics::getBackbufferMSAA() const
{
	return backbufferMSAA.get() ? backbufferMSAA->getMSAA() : 0;
}

void Graphics::setColor(Colorf c)
{
	c.r = std::min(std::max(c.r, 0.0f), 1.0f);
	c.g = std::min(std::max(c.g, 0.0f), 1.0f);
	c.b = std::min(std::max(c.b, 0.0f), 1.0f);
	c.a = std::min(std::max(c.a, 0.0f), 1.0f);

	states.back().color = c;
}

void Graphics::setScissor(const FRect &rect)
{
	flushBatchedDraws();

	DisplayState &state = states.back();
	state.scissor = true;
	state.scissorRect = rect;
	dirtyRenderState |= STATEBIT_SCISSOR;
}

void Graphics::setScissor()
{
	DisplayState &state = states.back();
	if (state.scissor)
	{
		flushBatchedDraws();
		state.scissor = false;
		dirtyRenderState |= STATEBIT_SCISSOR;
	}
}

void Graphics::setStencilState(const StencilState &s)
{
	validateStencilState(s);

	flushBatchedDraws();

	states.back().stencil = s;
	dirtyRenderState |= STATEBIT_STENCIL;
}

void Graphics::setDepthMode(CompareMode compare, bool write)
{
	validateDepthState(write);

	DisplayState &state = states.back();

	if (state.depthTest != compare || state.depthWrite != write)
	{
		flushBatchedDraws();
		state.depthTest = compare;
		state.depthWrite = write;
		dirtyRenderState |= STATEBIT_DEPTH;
	}
}

void Graphics::setFrontFaceWinding(Winding winding)
{
	if (states.back().winding != winding)
	{
		flushBatchedDraws();
		states.back().winding = winding;
		dirtyRenderState |= STATEBIT_FACEWINDING;
	}
}

void Graphics::setColorMask(ColorChannelMask mask)
{
	if (states.back().colorMask != mask)
	{
		flushBatchedDraws();
		states.back().colorMask = mask;
		dirtyRenderState |= STATEBIT_COLORMASK;
	}
}

void Graphics::setBlendState(const BlendState &blend)
{
	if (!(blend == states.back().blend))
	{
		flushBatchedDraws();
		states.back().blend = blend;
		dirtyRenderState |= STATEBIT_BLEND;
	}
}

void Graphics::setPointSize(float size)
{
	if (size != states.back().pointSize)
		flushBatchedDraws();

	states.back().pointSize = size;
}

void Graphics::setWireframe(bool enable)
{
	if (enable != states.back().wireframe)
	{
		flushBatchedDraws();
		states.back().wireframe = enable;
		dirtyRenderState |= STATEBIT_WIREFRAME;
	}
}

bool Graphics::isPixelFormatSupported(PixelFormat format, uint32 usage)
{
	format = getSizedFormat(format);

	const uint32 sample = PIXELFORMATUSAGEFLAGS_SAMPLE;
	const uint32 filter = PIXELFORMATUSAGEFLAGS_LINEAR;
	const uint32 rt = PIXELFORMATUSAGEFLAGS_RENDERTARGET;
	const uint32 blend = PIXELFORMATUSAGEFLAGS_BLEND;
	const uint32 msaa = PIXELFORMATUSAGEFLAGS_MSAA;
	const uint32 commonsample = PIXELFORMATUSAGEFLAGS_SAMPLE | PIXELFORMATUSAGEFLAGS_LINEAR;
	const uint32 commonrender = PIXELFORMATUSAGEFLAGS_RENDERTARGET | PIXELFORMATUSAGEFLAGS_BLEND | PIXELFORMATUSAGEFLAGS_MSAA;
	const uint32 computewrite = PIXELFORMATUSAGEFLAGS_COMPUTEWRITE;
	const uint32 all = commonsample | commonrender | computewrite;

	uint32 flags = PIXELFORMATUSAGEFLAGS_NONE;

	if (isPixelFormatCompressed(format) && (usage & rt) != 0)
		return false;

	// https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
	switch (format)
	{
		case PIXELFORMAT_UNKNOWN:
		case PIXELFORMAT_NORMAL:
		case PIXELFORMAT_HDR:
			break;

		case PIXELFORMAT_R8_UNORM:
			flags |= all;
			break;
		case PIXELFORMAT_R16_UNORM:
			if (families.apple[1])
				flags |= commonsample | commonrender | computewrite;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= all;
			break;
		case PIXELFORMAT_R16_FLOAT:
			flags |= all;
			break;
		case PIXELFORMAT_R32_FLOAT:
			if (families.apple[1])
				flags |= sample | rt | blend | computewrite;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= (all & ~(msaa | filter));
			if (@available(macOS 11.0, iOS 14.0, *))
			{
				if (device.supports32BitFloatFiltering)
					flags |= filter;
				if (device.supports32BitMSAA)
					flags |= msaa;
			}
			break;

		case PIXELFORMAT_RG8_UNORM:
			flags |= all;
			break;
		case PIXELFORMAT_LA8_UNORM:
			// As of early 2024, the VM device doesn't properly support texture swizzles
			// (observed on GitHub's runners) which is required for LA8 support.
			if (!isVMDevice)
			{
				if (families.apple[1] || families.mac[2] || families.macCatalyst[2])
					flags |= commonsample;
			}
			break;
		case PIXELFORMAT_RG16_UNORM:
			if (families.apple[1])
				flags |= commonsample | rt | blend | msaa | computewrite;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= all;
			break;
		case PIXELFORMAT_RG16_FLOAT:
			flags |= all;
			break;
		case PIXELFORMAT_RG32_FLOAT:
			if (families.apple[1])
				flags |= sample | rt | blend | computewrite;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= (all & ~(msaa | filter));
			if (@available(macOS 11.0, iOS 14.0, *))
			{
				if (device.supports32BitFloatFiltering)
					flags |= filter;
				if (device.supports32BitMSAA)
					flags |= msaa;
			}
			break;

		case PIXELFORMAT_RGBA8_UNORM:
		case PIXELFORMAT_BGRA8_UNORM:
			flags |= all;
			break;
		case PIXELFORMAT_RGBA8_sRGB:
		case PIXELFORMAT_BGRA8_sRGB:
			if (families.apple[1] || families.mac[1] || families.macCatalyst[1])
				flags |= commonsample | commonrender;
			if (families.apple[2])
				flags |= all;
			break;

		case PIXELFORMAT_RGBA16_UNORM:
			if (families.apple[1])
				flags |= commonsample | rt | msaa | blend | computewrite;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= all;
			break;
		case PIXELFORMAT_RGBA16_FLOAT:
			flags |= all;
			break;
		case PIXELFORMAT_RGBA32_FLOAT:
			if (families.apple[1])
				flags |= sample | rt | computewrite;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= (all & ~(msaa | filter));
			if (@available(macOS 11.0, iOS 14.0, *))
			{
				if (device.supports32BitFloatFiltering)
					flags |= filter;
				if (device.supports32BitMSAA)
					flags |= msaa;
			}
			break;

		case PIXELFORMAT_R8_INT:
		case PIXELFORMAT_R8_UINT:
		case PIXELFORMAT_RG8_INT:
		case PIXELFORMAT_RG8_UINT:
		case PIXELFORMAT_RGBA8_INT:
		case PIXELFORMAT_RGBA8_UINT:
		case PIXELFORMAT_R16_INT:
		case PIXELFORMAT_R16_UINT:
		case PIXELFORMAT_RG16_INT:
		case PIXELFORMAT_RG16_UINT:
		case PIXELFORMAT_RGBA16_INT:
		case PIXELFORMAT_RGBA16_UINT:
		case PIXELFORMAT_R32_INT:
		case PIXELFORMAT_R32_UINT:
		case PIXELFORMAT_RG32_INT:
		case PIXELFORMAT_RG32_UINT:
		case PIXELFORMAT_RGBA32_INT:
		case PIXELFORMAT_RGBA32_UINT:
		case PIXELFORMAT_RGB10A2_UINT:
			// If MSAA support for int formats is added this should be split up.
			flags |= sample | rt | computewrite;
			if (format == PIXELFORMAT_R32_INT || format == PIXELFORMAT_R32_UINT)
			{
				if (@available(macOS 14.0, iOS 17.0, *))
					flags |= PIXELFORMATUSAGEFLAGS_SHADERATOMICS;
			}
			break;

		case PIXELFORMAT_RGBA4_UNORM:
		case PIXELFORMAT_RGB5A1_UNORM:
		case PIXELFORMAT_RGB565_UNORM:
			if (families.apple[1])
				flags |= commonsample | rt | blend | msaa; // | resolve
			break;
		case PIXELFORMAT_RGB10A2_UNORM:
		case PIXELFORMAT_RG11B10_FLOAT:
			if (families.apple[1])
				flags |= commonsample | rt | blend | msaa; // | resolve
			if (families.apple[3])
				flags |= all;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= all;
			break;

		case PIXELFORMAT_STENCIL8:
			flags |= rt | sample | msaa;
			break;
		case PIXELFORMAT_DEPTH16_UNORM:
			flags |= rt | commonsample | msaa;
			//if (families.apple[3] || families.mac[1] || families.macCatalyst[1])
			//	flags |= resolve;
			break;
		case PIXELFORMAT_DEPTH24_UNORM: // This is emulated via 32f.
		case PIXELFORMAT_DEPTH32_FLOAT:
			if (families.apple[1])
				flags |= rt | sample | msaa;
			if (families.apple[3])
				flags |= rt | sample | msaa; // | resolve;
			if (families.mac[1] || families.macCatalyst[1])
				flags |= rt | commonsample | msaa; // | resolve;
			break;
		case PIXELFORMAT_DEPTH24_UNORM_STENCIL8:
			// Note: this falls back to 32f_s8 on some systems.
			flags |= rt | sample | msaa;
			break;
		case PIXELFORMAT_DEPTH32_FLOAT_STENCIL8:
			if (families.apple[1])
				flags |= rt | sample | msaa;
			if (families.apple[3])
				flags |= rt | sample | msaa; // | resolve
			if (families.mac[1] || families.macCatalyst[1])
				flags |= rt | commonsample | msaa; // | resolve
			break;

		case PIXELFORMAT_DXT1_UNORM:
		case PIXELFORMAT_DXT1_sRGB:
		case PIXELFORMAT_DXT3_UNORM:
		case PIXELFORMAT_DXT3_sRGB:
		case PIXELFORMAT_DXT5_UNORM:
		case PIXELFORMAT_DXT5_sRGB:
		case PIXELFORMAT_BC4_UNORM:
		case PIXELFORMAT_BC4_SNORM:
		case PIXELFORMAT_BC5_UNORM:
		case PIXELFORMAT_BC5_SNORM:
		case PIXELFORMAT_BC6H_UFLOAT:
		case PIXELFORMAT_BC6H_FLOAT:
		case PIXELFORMAT_BC7_UNORM:
		case PIXELFORMAT_BC7_sRGB:
			if (families.mac[1] || families.macCatalyst[1])
				flags |= commonsample;
			break;

		case PIXELFORMAT_PVR1_RGB2_UNORM:
		case PIXELFORMAT_PVR1_RGB2_sRGB:
		case PIXELFORMAT_PVR1_RGB4_UNORM:
		case PIXELFORMAT_PVR1_RGB4_sRGB:
		case PIXELFORMAT_PVR1_RGBA2_UNORM:
		case PIXELFORMAT_PVR1_RGBA2_sRGB:
		case PIXELFORMAT_PVR1_RGBA4_UNORM:
		case PIXELFORMAT_PVR1_RGBA4_sRGB:
			if (families.apple[1])
				flags |= commonsample;
			break;

		case PIXELFORMAT_ETC1_UNORM:
		case PIXELFORMAT_ETC2_RGB_UNORM:
		case PIXELFORMAT_ETC2_RGB_sRGB:
		case PIXELFORMAT_ETC2_RGBA_UNORM:
		case PIXELFORMAT_ETC2_RGBA_sRGB:
		case PIXELFORMAT_ETC2_RGBA1_UNORM:
		case PIXELFORMAT_ETC2_RGBA1_sRGB:
		case PIXELFORMAT_EAC_R_UNORM:
		case PIXELFORMAT_EAC_R_SNORM:
		case PIXELFORMAT_EAC_RG_UNORM:
		case PIXELFORMAT_EAC_RG_SNORM:
			if (families.apple[1])
				flags |= commonsample;
			break;

		case PIXELFORMAT_ASTC_4x4_UNORM:
		case PIXELFORMAT_ASTC_5x4_UNORM:
		case PIXELFORMAT_ASTC_5x5_UNORM:
		case PIXELFORMAT_ASTC_6x5_UNORM:
		case PIXELFORMAT_ASTC_6x6_UNORM:
		case PIXELFORMAT_ASTC_8x5_UNORM:
		case PIXELFORMAT_ASTC_8x6_UNORM:
		case PIXELFORMAT_ASTC_8x8_UNORM:
		case PIXELFORMAT_ASTC_10x5_UNORM:
		case PIXELFORMAT_ASTC_10x6_UNORM:
		case PIXELFORMAT_ASTC_10x8_UNORM:
		case PIXELFORMAT_ASTC_10x10_UNORM:
		case PIXELFORMAT_ASTC_12x10_UNORM:
		case PIXELFORMAT_ASTC_12x12_UNORM:
		case PIXELFORMAT_ASTC_4x4_sRGB:
		case PIXELFORMAT_ASTC_5x4_sRGB:
		case PIXELFORMAT_ASTC_5x5_sRGB:
		case PIXELFORMAT_ASTC_6x5_sRGB:
		case PIXELFORMAT_ASTC_6x6_sRGB:
		case PIXELFORMAT_ASTC_8x5_sRGB:
		case PIXELFORMAT_ASTC_8x6_sRGB:
		case PIXELFORMAT_ASTC_8x8_sRGB:
		case PIXELFORMAT_ASTC_10x5_sRGB:
		case PIXELFORMAT_ASTC_10x6_sRGB:
		case PIXELFORMAT_ASTC_10x8_sRGB:
		case PIXELFORMAT_ASTC_10x10_sRGB:
		case PIXELFORMAT_ASTC_12x10_sRGB:
		case PIXELFORMAT_ASTC_12x12_sRGB:
			if (families.apple[2])
				flags |= commonsample;
			break;

		case PIXELFORMAT_MAX_ENUM:
			break;
	}

	return (usage & flags) == usage;
}

Renderer Graphics::getRenderer() const
{
	return RENDERER_METAL;
}

bool Graphics::usesGLSLES() const
{
#ifdef LOVE_IOS
	return true;
#else
	return false;
#endif
}

Graphics::RendererInfo Graphics::getRendererInfo() const
{
	RendererInfo info;
	info.name = "Metal";
	info.version = "2.1"; // TODO
	info.vendor = ""; // TODO
	info.device = device.name.UTF8String;
	return info;
}

int Graphics::getClosestMSAASamples(int requestedsamples)
{
	// We currently rely on StoreAndMultisampleResolve (unfortunately), which
	// isn't supported by old phone GPUs.
	if (!families.apple[3] && !families.mac[1] && !families.macCatalyst[1])
		return 1;

	const int checkmsaa[] = {32, 16, 8, 4, 2};
	for (int samples : checkmsaa)
	{
		if (samples <= requestedsamples && [device supportsTextureSampleCount:samples])
			return samples;
	}
	return 1;
}

void Graphics::initCapabilities()
{
	for (NSInteger i = 0; i < 7; i++)
	{
		MTLGPUFamily family = (MTLGPUFamily) (MTLGPUFamilyApple1 + i);
		if ([device supportsFamily:family])
			families.apple[1 + i] = true;
	}

	for (NSInteger i = 0; i < 2; i++)
	{
		MTLGPUFamily family = (MTLGPUFamily) (MTLGPUFamilyMac1 + i);
		if ([device supportsFamily:family])
			families.mac[1 + i] = true;
	}

	for (NSInteger i = 0; i < 3; i++)
	{
		MTLGPUFamily family = (MTLGPUFamily) (MTLGPUFamilyCommon1 + i);
		if ([device supportsFamily:family])
			families.common[1 + i] = true;
	}

	for (NSInteger i = 0; i < 2; i++)
	{
		MTLGPUFamily family = (MTLGPUFamily) (MTLGPUFamilyMacCatalyst1 + i);
		if ([device supportsFamily:family])
			families.macCatalyst[1 + i] = true;
	}

	capabilities.features[FEATURE_MULTI_RENDER_TARGET_FORMATS] = true;
	capabilities.features[FEATURE_CLAMP_ZERO] = true;
	capabilities.features[FEATURE_CLAMP_ONE] = false;
	if (@available(macOS 10.12, iOS 14.0, *))
	{
		// Requires "border color" feature.
		if (families.mac[1] || families.macCatalyst[1] || families.apple[7])
			capabilities.features[FEATURE_CLAMP_ONE] = true;
	}
	capabilities.features[FEATURE_LIGHTEN] = true;
	capabilities.features[FEATURE_FULL_NPOT] = true;
	capabilities.features[FEATURE_PIXEL_SHADER_HIGHP] = true;
	capabilities.features[FEATURE_SHADER_DERIVATIVES] = true;
	capabilities.features[FEATURE_GLSL3] = true;
	capabilities.features[FEATURE_GLSL4] = true;
	capabilities.features[FEATURE_INSTANCING] = true;
	capabilities.features[FEATURE_TEXEL_BUFFER] = true;
	capabilities.features[FEATURE_COPY_TEXTURE_TO_BUFFER] = true;

	if (families.mac[1] || families.macCatalyst[1] || families.apple[3])
		capabilities.features[FEATURE_INDIRECT_DRAW] = true;
	else
		capabilities.features[FEATURE_INDIRECT_DRAW] = false;

	// Apple 3 devices support read/write to buffers in functions, while Apple 4 supports read/write to images.
	// So let's err on the safe side and check support for Apple 4.
	if (families.apple[4])
	{
		capabilities.features[FEATURE_VERTEX_WRITE] = true;
		capabilities.features[FEATURE_PIXEL_WRITE] = true;
	}
	else
	{
		capabilities.features[FEATURE_VERTEX_WRITE] = false;
		capabilities.features[FEATURE_PIXEL_WRITE] = false;
	}
	
	static_assert(FEATURE_MAX_ENUM == 15, "Graphics::initCapabilities must be updated when adding a new graphics feature!");

	// https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
	capabilities.limits[LIMIT_POINT_SIZE] = 511;
	capabilities.limits[LIMIT_TEXTURE_LAYERS] = 2048;
	capabilities.limits[LIMIT_VOLUME_TEXTURE_SIZE] = 2048;
	if (families.mac[1] || families.macCatalyst[1] || families.apple[3])
	{
		capabilities.limits[LIMIT_TEXTURE_SIZE] = 16384;
		capabilities.limits[LIMIT_CUBE_TEXTURE_SIZE] = 16384;
	}
	else
	{
		capabilities.limits[LIMIT_TEXTURE_SIZE] = 8192;
		capabilities.limits[LIMIT_CUBE_TEXTURE_SIZE] = 8192;
	}

	// TODO: metal doesn't have a good API to query this?
	capabilities.limits[LIMIT_TEXEL_BUFFER_SIZE] = 128 * 1024 * 1024;

	if (@available(macOS 10.14, iOS 12.0, *))
	{
		NSUInteger buffersize = [device maxBufferLength];
		capabilities.limits[LIMIT_SHADER_STORAGE_BUFFER_SIZE] = buffersize;
	}
	else
	{
		capabilities.limits[LIMIT_SHADER_STORAGE_BUFFER_SIZE] = 128 * 1024 * 1024;
	}

	capabilities.limits[LIMIT_THREADGROUPS_X] = LOVE_INT32_MAX; // TODO: is there a real limit?
	capabilities.limits[LIMIT_THREADGROUPS_Y] = LOVE_INT32_MAX;
	capabilities.limits[LIMIT_THREADGROUPS_Z] = LOVE_INT32_MAX;

	if (families.mac[1] || families.macCatalyst[1] || families.apple[2])
		capabilities.limits[LIMIT_RENDER_TARGETS] = 8;
	else
		capabilities.limits[LIMIT_RENDER_TARGETS] = 4;
	capabilities.limits[LIMIT_TEXTURE_MSAA] = getClosestMSAASamples(32);
	capabilities.limits[LIMIT_ANISOTROPY] = 16.0f;
	static_assert(LIMIT_MAX_ENUM == 13, "Graphics::initCapabilities must be updated when adding a new system limit!");

	for (int i = 0; i < TEXTURE_MAX_ENUM; i++)
		capabilities.textureTypes[i] = true;
}

void Graphics::getAPIStats(int &shaderswitches) const
{
	shaderswitches = shaderSwitches;
}

} // metal
} // graphics
} // love