/** * Copyright (c) 2006-2022 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" #import 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 getMTLTexture(love::graphics::Texture *tex) { return tex ? (__bridge id)(void *) tex->getHandle() : nil; } static inline id getMTLRenderTarget(love::graphics::Texture *tex) { return tex ? (__bridge id)(void *) tex->getRenderTargetHandle() : nil; } static inline id getMTLBuffer(love::graphics::Resource *res) { return res ? (__bridge id)(void *) res->getHandle() : nil; } static inline void setBuffer(id encoder, Graphics::RenderEncoderBindings &bindings, ShaderStageType stage, int index, id 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 encoder, Graphics::RenderEncoderBindings &bindings, int index, id 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 encoder, Graphics::RenderEncoderBindings &bindings, ShaderStageType stage, int index, id 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 encoder, Graphics::RenderEncoderBindings &bindings, int index, id 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 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 sampler = (__bridge id) 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 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 sampler = (__bridge id) s; [encoder setSamplerState:sampler atIndex:index]; } } love::graphics::Graphics *createInstance() { love::graphics::Graphics *instance = nullptr; try { instance = new Graphics(); } catch (love::Exception &e) { printf("Cannot create Metal renderer: %s\n", e.what()); } return instance; } struct DefaultVertexAttributes { float floats[4]; int ints[4]; }; Graphics *Graphics::graphicsInstance = nullptr; Graphics::Graphics() : device(nil) , commandQueue(nil) , commandBuffer(nil) , renderEncoder(nil) , blitEncoder(nil) , metalLayer(nil) , activeDrawable(nil) , passDesc(nil) , dirtyRenderState(STATEBIT_ALL) , lastCullMode(CULL_MAX_ENUM) , lastRenderPipelineKey() , windowHasStencil(false) , shaderSwitches(0) , requestedBackbufferMSAA(0) , attachmentStoreActions() , renderBindings() , uniformBufferOffset(0) , defaultAttributesBuffer(nullptr) , defaultTextures() , families() { @autoreleasepool { if (@available(macOS 10.15, iOS 13.0, *)) { graphicsInstance = this; device = MTLCreateSystemDefaultDevice(); if (device == nil) throw love::Exception("Metal is not supported on this system."); } else { throw love::Exception("LOVE's Metal graphics backend requires macOS 10.15+ or iOS 13+."); } commandQueue = [device newCommandQueue]; passDesc = [MTLRenderPassDescriptor new]; initCapabilities(); uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_VERTEX, 1024 * 1024 * 1); { std::vector dataformat = { {"floats", DATAFORMAT_FLOAT_VEC4, 0}, {"ints", DATAFORMAT_INT32_VEC4, 0}, }; DefaultVertexAttributes defaults = { {0.0f, 0.0f, 0.0f, 1.0f}, {0, 0, 0, 1}, }; Buffer::Settings attribsettings(BUFFERUSAGEFLAG_VERTEX, BUFFERDATAUSAGE_STATIC); defaultAttributesBuffer = newBuffer(attribsettings, dataformat, &defaults, sizeof(DefaultVertexAttributes), 0); } uint8 defaultpixel[] = {255, 255, 255, 255}; for (int i = 0; i < TEXTURE_MAX_ENUM; i++) { Texture::Settings settings; settings.type = (TextureType) i; settings.format = PIXELFORMAT_RGBA8_UNORM; defaultTextures[i] = newTexture(settings); Rect r = {0, 0, 1, 1}; defaultTextures[i]->replacePixels(defaultpixel, sizeof(defaultpixel), 0, 0, r, false); } 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 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(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); delete uniformBuffer; delete defaultAttributesBuffer; passDesc = nil; commandQueue = nil; device = nil; for (int i = 0; i < TEXTURE_MAX_ENUM; i++) defaultTextures[i]->release(); 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::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 stages[SHADERSTAGE_MAX_ENUM]) { return new Shader(device, stages); } love::graphics::Buffer *Graphics::newBuffer(const Buffer::Settings &settings, const std::vector &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); } Matrix4 Graphics::computeDeviceProjection(const Matrix4 &projection, bool /*rendertotexture*/) const { uint32 flags = DEVICE_PROJECTION_FLIP_Y; return calculateDeviceProjection(projection, flags); } void Graphics::setViewportSize(int width, int height, int pixelwidth, int pixelheight) { this->width = width; this->height = height; this->pixelWidth = pixelwidth; this->pixelHeight = pixelheight; if (!isRenderTargetActive()) { dirtyRenderState |= STATEBIT_VIEWPORT | STATEBIT_SCISSOR; resetProjection(); } Texture::Settings settings; settings.width = width; settings.height = height; settings.dpiScale = (float)pixelheight / (float)height; settings.msaa = getRequestedBackbufferMSAA(); settings.renderTarget = true; settings.readable.set(false); backbufferMSAA.set(nullptr); if (settings.msaa > 1) { settings.format = isGammaCorrect() ? PIXELFORMAT_BGRA8_UNORM_sRGB : PIXELFORMAT_BGRA8_UNORM; backbufferMSAA.set(newTexture(settings), Acquire::NORETAIN); } settings.format = PIXELFORMAT_DEPTH24_UNORM_STENCIL8; backbufferDepthStencil.set(newTexture(settings), Acquire::NORETAIN); } bool Graphics::setMode(void *context, int width, int height, int pixelwidth, int pixelheight, bool windowhasstencil, int msaa) { @autoreleasepool { this->width = width; this->height = height; this->metalLayer = (__bridge CAMetalLayer *) context; this->windowHasStencil = windowhasstencil; this->requestedBackbufferMSAA = msaa; metalLayer.device = device; metalLayer.pixelFormat = isGammaCorrect() ? MTLPixelFormatBGRA8Unorm_sRGB : MTLPixelFormatBGRA8Unorm; // Explicitly turn off color matching, for now (eg don't make sRGB content // appear correct on P3 displays). It looks better with color matching, but // having it off matches the OpenGL backend and there's some cost to it. // TODO: revisit this? metalLayer.colorspace = nil; // 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 setViewportSize(width, height, pixelwidth, pixelheight); 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 Graphics::useCommandBuffer() { if (commandBuffer == nil) { commandBuffer = [commandQueue commandBuffer]; Graphics *pthis = this; pthis->retain(); [commandBuffer addCompletedHandler:^(id _Nonnull) { pthis->completeCommandBufferIndex.fetch_add(1, std::memory_order_relaxed); pthis->release(); }]; } return commandBuffer; } 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 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); setAttachment(rt, passDesc.depthAttachment, attachmentStoreActions.depth, false); setAttachment(rt, passDesc.stencilAttachment, attachmentStoreActions.stencil, false); attachmentStoreActions.depth = MTLStoreActionDontCare; attachmentStoreActions.stencil = MTLStoreActionDontCare; auto &key = lastRenderPipelineKey; key.colorRenderTargetFormats = isGammaCorrect() ? PIXELFORMAT_BGRA8_UNORM_sRGB : PIXELFORMAT_BGRA8_UNORM; key.depthStencilFormat = backbufferDepthStencil->getPixelFormat(); key.msaa = backbufferMSAA ? (uint8) backbufferMSAA->getMSAA() : 1; } renderEncoder = [useCommandBuffer() renderCommandEncoderWithDescriptor:passDesc]; renderBindings = {}; id 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]; if (rts.depthStencil.texture.get() || rts.temporaryRTFlags != 0 || isbackbuffer) { [renderEncoder setDepthStoreAction:store ? MTLStoreActionStore : actions.depth]; [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; passDesc.colorAttachments[i].texture = nil; passDesc.colorAttachments[i].resolveTexture = nil; } passDesc.depthAttachment.loadAction = MTLLoadActionLoad; passDesc.depthAttachment.texture = nil; passDesc.depthAttachment.resolveTexture = nil; passDesc.stencilAttachment.loadAction = MTLLoadActionLoad; passDesc.stencilAttachment.texture = nil; passDesc.stencilAttachment.resolveTexture = nil; } } id Graphics::useBlitEncoder() { if (blitEncoder == nil) { submitAllEncoders(SUBMIT_STORE); blitEncoder = [useCommandBuffer() blitCommandEncoder]; } return blitEncoder; } void Graphics::submitBlitEncoder() { if (blitEncoder != nil) { [blitEncoder endEncoding]; blitEncoder = nil; } } id 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 Graphics::getCachedSampler(const SamplerState &s) { @autoreleasepool { uint64 key = s.toKey(); auto it = cachedSamplers.find(key); if (it != cachedSamplers.end()) return (__bridge id) 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; // TODO: This isn't supported on some older iOS devices... if (s.depthSampleMode.hasValue) desc.compareFunction = getMTLCompareFunction(s.depthSampleMode.value); id sampler = [device newSamplerStateWithDescriptor:desc]; if (sampler != nil) cachedSamplers[key] = (void *) CFBridgingRetain(sampler); return sampler; }} id 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) 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 * setStencilMode(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 mtlstate = [device newDepthStencilStateWithDescriptor:desc]; if (mtlstate != nil) cachedDepthStencilStates[key] = (void *) CFBridgingRetain(mtlstate); return mtlstate; } void Graphics::applyRenderState(id encoder, const VertexAttributes &attributes) { 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(); rth = rt.texture->getPixelHeight(); } 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)(state.scissorRect.x*dpiscale); rect.y = (NSUInteger)(state.scissorRect.y*dpiscale); rect.width = (NSUInteger)(state.scissorRect.w*dpiscale); rect.height = (NSUInteger)(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 || !(attributes == lastRenderPipelineKey.vertexAttributes)) { auto &key = lastRenderPipelineKey; key.vertexAttributes = attributes; Shader *shader = (Shader *) Shader::current; id pipeline = nil; if (shader) { key.blend = state.blend; key.colorChannelMask = state.colorMask; pipeline = shader->getCachedRenderPipeline(key); } [encoder setRenderPipelineState:pipeline]; } if (dirtyState & (STATEBIT_DEPTH | STATEBIT_STENCIL)) { DepthState depth; depth.compare = state.depthTest; depth.write = state.depthWrite; id mtlstate = getCachedDepthStencilState(depth, state.stencil); [encoder setDepthStencilState:mtlstate]; } if (dirtyState & STATEBIT_STENCIL) [encoder setStencilReferenceValue:state.stencil.value]; dirtyRenderState = 0; } void Graphics::applyShaderUniforms(id encoder, love::graphics::Shader *shader) { Shader *s = (Shader *)shader; #ifdef LOVE_MACOS 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; uniformBuffer->release(); uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_VERTEX, newsize); uniformBufferData = {}; uniformBufferOffset = 0; } if (uniformBufferData.data == nullptr) uniformBufferData = uniformBuffer->map(uniformBuffer->getSize()); memcpy(uniformBufferData.data + uniformBufferOffset, bufferdata, size); id buffer = getMTLBuffer(uniformBuffer); int uniformindex = Shader::getUniformBufferBinding(); auto &bindings = renderBindings; setBuffer(encoder, bindings, uniformindex, buffer, uniformBufferOffset); uniformBufferOffset += alignUp(size, alignment); for (const Shader::TextureBinding &b : s->getTextureBindings()) { id 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 (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); } } void Graphics::applyShaderUniforms(id renderEncoder, love::graphics::Shader *shader, love::graphics::Texture *maintex) { Shader *s = (Shader *)shader; #ifdef LOVE_MACOS 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(); // The normal matrix is the transpose of the inverse of the rotation portion // (top-left 3x3) of the transform matrix. { Matrix3 normalmatrix = Matrix3(builtins->transformMatrix).transposedInverse(); const float *e = normalmatrix.getElements(); for (int i = 0; i < 3; i++) { builtins->normalMatrix[i].x = e[i * 3 + 0]; builtins->normalMatrix[i].y = e[i * 3 + 1]; builtins->normalMatrix[i].z = e[i * 3 + 2]; builtins->normalMatrix[i].w = 0.0f; } } // Store DPI scale in an unused component of another vector. builtins->normalMatrix[0].w = (float) getCurrentDPIScale(); // Same with point size. builtins->normalMatrix[1].w = getPointSize(); builtins->screenSizeParams = Vector4(getPixelWidth(), getPixelHeight(), 1.0f, 0.0f); auto rt = states.back().renderTargets.getFirstTarget().texture.get(); if (rt != nullptr) { builtins->screenSizeParams.x = rt->getPixelWidth(); builtins->screenSizeParams.y = rt->getPixelHeight(); } builtins->constantColor = getColor(); gammaCorrectColor(builtins->constantColor); if (uniformBuffer->getSize() < uniformBufferOffset + size) { size_t newsize = uniformBuffer->getSize() * 2; uniformBuffer->release(); uniformBuffer = CreateStreamBuffer(device, BUFFERUSAGE_VERTEX, newsize); uniformBufferData = {}; uniformBufferOffset = 0; } if (uniformBufferData.data == nullptr) uniformBufferData = uniformBuffer->map(uniformBuffer->getSize()); memcpy(uniformBufferData.data + uniformBufferOffset, bufferdata, size); id buffer = getMTLBuffer(uniformBuffer); int uniformindex = Shader::getUniformBufferBinding(); auto &bindings = renderBindings; setBuffer(renderEncoder, bindings, SHADERSTAGE_VERTEX, uniformindex, buffer, uniformBufferOffset); setBuffer(renderEncoder, bindings, SHADERSTAGE_PIXEL, uniformindex, buffer, uniformBufferOffset); uniformBufferOffset += alignUp(size, alignment); for (const Shader::TextureBinding &b : s->getTextureBindings()) { id texture = b.texture; auto samplertex = b.samplerTexture; if (b.isMainTexture) { if (maintex == nullptr) { auto texinfo = shader->getMainTextureInfo(); if (texinfo != nullptr && texinfo->textureType != TEXTURE_MAX_ENUM) maintex = defaultTextures[texinfo->textureType]; } 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 (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 (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); index = b.stages[SHADERSTAGE_PIXEL]; if (index != LOVE_UINT8_MAX) setBuffer(renderEncoder, bindings, SHADERSTAGE_PIXEL, index, b.buffer, 0); } } static void setVertexBuffers(id 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 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 encoder = useRenderEncoder(); if (cmd.cullMode != lastCullMode) { lastCullMode = cmd.cullMode; dirtyRenderState |= STATEBIT_CULLMODE; } applyRenderState(encoder, *cmd.attributes); applyShaderUniforms(encoder, Shader::current, cmd.texture); setVertexBuffers(encoder, Shader::current, cmd.buffers, renderBindings); [encoder drawPrimitives:getMTLPrimitiveType(cmd.primitiveType) vertexStart:cmd.vertexStart vertexCount:cmd.vertexCount instanceCount:cmd.instanceCount]; ++drawCalls; }} void Graphics::draw(const DrawIndexedCommand &cmd) { @autoreleasepool { id encoder = useRenderEncoder(); if (cmd.cullMode != lastCullMode) { lastCullMode = cmd.cullMode; dirtyRenderState |= STATEBIT_CULLMODE; } applyRenderState(encoder, *cmd.attributes); applyShaderUniforms(encoder, Shader::current, cmd.texture); setVertexBuffers(encoder, Shader::current, cmd.buffers, renderBindings); auto indexType = cmd.indexType == INDEX_UINT32 ? MTLIndexTypeUInt32 : MTLIndexTypeUInt16; [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, const VertexAttributes &attributes, 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 encoder = useRenderEncoder(); if (lastCullMode != CULL_NONE) { lastCullMode = CULL_NONE; dirtyRenderState |= STATEBIT_CULLMODE; } applyRenderState(encoder, attributes); applyShaderUniforms(encoder, Shader::current, texture); id 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 { 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(int x, int y, int z) { @autoreleasepool { // Set by higher level code before calling dispatch(x, y, z). auto shader = (Shader *) Shader::current; int tX, tY, tZ; shader->getLocalThreadgroupSize(&tX, &tY, &tZ); id pipeline = shader->getComputePipeline(); if (pipeline == nil) return false; id computeEncoder = useComputeEncoder(); applyShaderUniforms(computeEncoder, shader); // TODO: track this state? [computeEncoder setComputePipelineState:pipeline]; [computeEncoder dispatchThreadgroups:MTLSizeMake(x, y, z) threadsPerThreadgroup:MTLSizeMake(tX, tY, tZ)]; return true; }} void Graphics::setRenderTargetsInternal(const RenderTargets &rts, int /*pixelw*/, int /*pixelh*/, bool /*hasSRGBtexture*/) { @autoreleasepool { endPass(); 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.depthAttachment = nil; passDesc.stencilAttachment = nil; 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.vertexAttributes = VertexAttributes(); dirtyRenderState = STATEBIT_ALL; }} void Graphics::endPass() { // 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 this is the backbuffer. if ((depthstencil == nullptr && (rts.temporaryRTFlags & (TEMPORARY_RT_DEPTH | TEMPORARY_RT_STENCIL)) != 0) || !rts.getFirstTarget().texture.get()) { attachmentStoreActions.depth = MTLStoreActionDontCare; attachmentStoreActions.stencil = MTLStoreActionDontCare; } submitRenderEncoder(SUBMIT_DONE); for (const auto &rt : rts.colors) { if (rt.texture->getMipmapsMode() == Texture::MIPMAPS_AUTO && rt.mipmap == 0) rt.texture->generateMipmaps(); } } void Graphics::clear(OptionalColorD c, OptionalInt stencil, OptionalDouble depth) { @autoreleasepool { if (c.hasValue || stencil.hasValue || depth.hasValue) { flushBatchedDraws(); // Handle clearing mid-pass by starting a new pass. if (renderEncoder != nil) { submitRenderEncoder(SUBMIT_STORE); useRenderEncoder(); } } 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; } }} void Graphics::clear(const std::vector &colors, OptionalInt stencil, OptionalDouble depth) { @autoreleasepool { if (colors.size() == 0 && !stencil.hasValue && !depth.hasValue) return; 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); useRenderEncoder(); } 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; } }} void Graphics::discard(const std::vector &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(); id screenshotbuffer = nil; if (!pendingScreenshotCallbacks.empty()) { int w = activeDrawable.texture.width; int h = activeDrawable.texture.height; 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, 0) 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 cmd = getCommandBuffer(); if (cmd != nil && activeDrawable != nil) [cmd presentDrawable:activeDrawable]; submitCommandBuffer(SUBMIT_DONE); if (!pendingScreenshotCallbacks.empty()) { [cmd waitUntilCompleted]; int w = activeDrawable.texture.width; int h = activeDrawable.texture.height; size_t size = w * h * 4; auto imagemodule = Module::getInstance(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(M_WINDOW); if (window != nullptr) window->swapBuffers(); // This is set to NO when there are pending screen captures. metalLayer.framebufferOnly = YES; activeDrawable = nil; // Reset the per-frame stat counts. drawCalls = 0; shaderSwitches = 0; renderTargetSwitchCount = 0; drawCallsBatched = 0; updatePendingReadbacks(); updateTemporaryResources(); }} int Graphics::getRequestedBackbufferMSAA() const { return requestedBackbufferMSAA; } 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 Rect &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::setStencilMode(StencilAction action, CompareMode compare, int value, uint32 readmask, uint32 writemask) { DisplayState &state = states.back(); if (action != STENCIL_KEEP) { const auto &rts = state.renderTargets; love::graphics::Texture *dstexture = rts.depthStencil.texture.get(); if (!isRenderTargetActive() && !windowHasStencil) throw love::Exception("The window must have stenciling enabled to draw to the main screen's stencil buffer."); else if (isRenderTargetActive() && (rts.temporaryRTFlags & TEMPORARY_RT_STENCIL) == 0 && (dstexture == nullptr || !isPixelFormatStencil(dstexture->getPixelFormat()))) throw love::Exception("Drawing to the stencil buffer with a Canvas active requires either stencil=true or a custom stencil-type Canvas to be used, in setCanvas."); } flushBatchedDraws(); state.stencil.action = action; state.stencil.compare = compare; state.stencil.value = value; state.stencil.readMask = readmask; state.stencil.writeMask = writemask; dirtyRenderState |= STATEBIT_STENCIL; } void Graphics::setDepthMode(CompareMode compare, bool 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; } } PixelFormat Graphics::getSizedFormat(PixelFormat format, bool /*rendertarget*/, bool /*readable*/) const { switch (format) { case PIXELFORMAT_NORMAL: if (isGammaCorrect()) return PIXELFORMAT_RGBA8_UNORM_sRGB; else return PIXELFORMAT_RGBA8_UNORM; case PIXELFORMAT_HDR: return PIXELFORMAT_RGBA16_FLOAT; default: return format; } } bool Graphics::isPixelFormatSupported(PixelFormat format, uint32 usage, bool sRGB) { bool rendertarget = (usage & PIXELFORMATUSAGEFLAGS_RENDERTARGET) != 0; bool readable = (usage & PIXELFORMATUSAGEFLAGS_SAMPLE) != 0; format = getSizedFormat(format, rendertarget, readable); if (sRGB) format = getSRGBPixelFormat(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) && rendertarget) 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: // Requires texture swizzle support. if (@available(macOS 10.15, iOS 13, *)) { 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_UNORM_sRGB: case PIXELFORMAT_BGRA8_UNORM_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: // If MSAA support for int formats is added this should be split up. flags |= rt | computewrite; 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_DXT3_UNORM: case PIXELFORMAT_DXT5_UNORM: 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: if (families.mac[1] || families.macCatalyst[1]) flags |= commonsample; break; case PIXELFORMAT_PVR1_RGB2_UNORM: case PIXELFORMAT_PVR1_RGB4_UNORM: case PIXELFORMAT_PVR1_RGBA2_UNORM: case PIXELFORMAT_PVR1_RGBA4_UNORM: if (families.apple[1]) flags |= commonsample; break; case PIXELFORMAT_ETC1_UNORM: case PIXELFORMAT_ETC2_RGB_UNORM: case PIXELFORMAT_ETC2_RGBA_UNORM: case PIXELFORMAT_ETC2_RGBA1_UNORM: 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: case PIXELFORMAT_ASTC_5x4: case PIXELFORMAT_ASTC_5x5: case PIXELFORMAT_ASTC_6x5: case PIXELFORMAT_ASTC_6x6: case PIXELFORMAT_ASTC_8x5: case PIXELFORMAT_ASTC_8x6: case PIXELFORMAT_ASTC_8x8: case PIXELFORMAT_ASTC_10x5: case PIXELFORMAT_ASTC_10x6: case PIXELFORMAT_ASTC_10x8: case PIXELFORMAT_ASTC_10x10: case PIXELFORMAT_ASTC_12x10: case PIXELFORMAT_ASTC_12x12: 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() { if (@available(macOS 10.15, iOS 13.0, *)) { 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; } } else { // TODO: feature set API } 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_BLEND_MINMAX] = 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_INDEX_BUFFER_32BIT] = true; capabilities.features[FEATURE_COPY_BUFFER] = true; capabilities.features[FEATURE_COPY_BUFFER_TO_TEXTURE] = true; capabilities.features[FEATURE_COPY_TEXTURE_TO_BUFFER] = true; capabilities.features[FEATURE_COPY_RENDER_TARGET_TO_BUFFER] = true; static_assert(FEATURE_MAX_ENUM == 17, "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