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
|