123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 | /**
* 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 "Texture.h"
#include "Graphics.h"
namespace love
{
namespace graphics
{
namespace metal
{
static MTLTextureType getMTLTextureType(TextureType type, int msaa)
{
switch (type)
{
case TEXTURE_2D: return msaa > 1 ? MTLTextureType2DMultisample : MTLTextureType2D;
case TEXTURE_VOLUME: return MTLTextureType3D;
case TEXTURE_2D_ARRAY: return MTLTextureType2DArray;
case TEXTURE_CUBE: return MTLTextureTypeCube;
case TEXTURE_MAX_ENUM: return MTLTextureType2D;
}
return MTLTextureType2D;
}
Texture::Texture(love::graphics::Graphics *gfxbase, id<MTLDevice> device, const Settings &settings, const Slices *data)
: love::graphics::Texture(gfxbase, settings, data)
{ @autoreleasepool {
auto gfx = (Graphics *) gfxbase;
MTLTextureDescriptor *desc = [MTLTextureDescriptor new];
int w = pixelWidth;
int h = pixelHeight;
desc.width = w;
desc.height = h;
desc.depth = depth;
desc.arrayLength = layers;
desc.mipmapLevelCount = mipmapCount;
desc.textureType = getMTLTextureType(texType, 1);
auto formatdesc = Metal::convertPixelFormat(device, format);
desc.pixelFormat = formatdesc.format;
if (formatdesc.swizzled)
desc.swizzle = formatdesc.swizzle;
desc.storageMode = MTLStorageModePrivate;
if (readable)
desc.usage |= MTLTextureUsageShaderRead;
if (renderTarget)
desc.usage |= MTLTextureUsageRenderTarget;
if (computeWrite)
desc.usage |= MTLTextureUsageShaderWrite;
for (PixelFormat viewformat : viewFormats)
{
if (getLinearPixelFormat(viewformat) != getLinearPixelFormat(format))
{
desc.usage |= MTLTextureUsagePixelFormatView;
break;
}
}
if (@available(macOS 14.0, iOS 17.0, *))
{
// FIXME: This is imperfect, r32i views of r32f source textures aren't covered
// (and don't appear to be supported in atomic ops in general, on Metal).
// Note: This disables lossless compression, but according to the docs
// MTLTextureUsageShaderWrite does as well, so it would already be off in this
// situation.
if (computeWrite && gfx->isPixelFormatSupported(format, PIXELFORMATUSAGEFLAGS_SHADERATOMICS))
desc.usage |= MTLTextureUsageShaderAtomic;
}
texture = [device newTextureWithDescriptor:desc];
if (texture == nil)
throw love::Exception("Out of graphics memory.");
if (!debugName.empty())
texture.label = @(debugName.c_str());
actualMSAASamples = gfx->getClosestMSAASamples(getRequestedMSAA());
if (actualMSAASamples > 1)
{
desc.sampleCount = actualMSAASamples;
desc.textureType = getMTLTextureType(texType, actualMSAASamples);
desc.usage &= ~MTLTextureUsageShaderRead;
msaaTexture = [device newTextureWithDescriptor:desc];
if (msaaTexture == nil)
{
texture = nil;
throw love::Exception("Out of graphics memory.");
}
if (!debugName.empty())
msaaTexture.label = [@(debugName.c_str()) stringByAppendingString:@" (MSAA buffer)"];
}
int mipcount = getMipmapCount();
bool cangeneratemips = true;
if (isPixelFormatDepthStencil(format) || isPixelFormatCompressed(format))
cangeneratemips = false;
// generateMipmapsForTexture is only supported for color renderable +
// filterable formats.
uint32 genmipsflags = PIXELFORMATUSAGEFLAGS_LINEAR | PIXELFORMATUSAGEFLAGS_RENDERTARGET;
if (!gfx->isPixelFormatSupported(format, (PixelFormatUsageFlags) genmipsflags))
cangeneratemips = false;
bool shouldgeneratemips = false;
MTLRenderPassDescriptor *passdesc = nil;
// Initialize texture.
for (int mip = 0; mip < mipcount; mip++)
{
for (int slice = 0; slice < getSliceCount(mip); slice++)
{
auto imgd = data != nullptr ? data->get(slice, mip) : nullptr;
if (imgd != nullptr)
{
uploadImageData(imgd, mip, slice, 0, 0);
}
else if (mip > 0 && cangeneratemips)
{
// Handled in the generateMipmaps call below.
shouldgeneratemips = true;
continue;
}
else if (isRenderTarget())
{
// Clear to transparent black.
gfx->submitAllEncoders(Graphics::SUBMIT_STORE);
id<MTLCommandBuffer> cmd = gfx->useCommandBuffer();
if (passdesc == nil)
passdesc = [MTLRenderPassDescriptor renderPassDescriptor];
auto configattachment = [&](MTLRenderPassAttachmentDescriptor *attachment)
{
attachment.texture = texture;
attachment.level = mip;
attachment.slice = texType == TEXTURE_VOLUME ? 0 : slice;
attachment.depthPlane = texType == TEXTURE_VOLUME ? slice : 0;
attachment.loadAction = MTLLoadActionClear;
attachment.storeAction = MTLStoreActionStore;
if (actualMSAASamples > 1)
{
attachment.texture = msaaTexture;
attachment.resolveTexture = texture;
attachment.storeAction = MTLStoreActionStoreAndMultisampleResolve;
}
};
if (isPixelFormatDepth(format))
{
configattachment(passdesc.depthAttachment);
passdesc.depthAttachment.clearDepth = 1.0;
}
if (isPixelFormatStencil(format))
{
configattachment(passdesc.stencilAttachment);
passdesc.stencilAttachment.clearStencil = 0;
}
if (!isPixelFormatDepthStencil(format))
{
configattachment(passdesc.colorAttachments[0]);
passdesc.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0);
}
id<MTLRenderCommandEncoder> encoder = [cmd renderCommandEncoderWithDescriptor:passdesc];
[encoder endEncoding];
}
else if (getMSAA() <= 1 && !isPixelFormatDepthStencil(format) && !isPixelFormatCompressed(format))
{
// Initialize to transparent black.
size_t datasize = getPixelFormatSliceSize(format, w, h);
Rect r = {0, 0, getPixelWidth(mip), getPixelHeight(mip)};
// Special case: memset(0) if data is null.
// TODO: make this a more sensible part of the internal Texture API?
uploadByteData(nullptr, datasize, mip, slice, r);
}
else
{
// Shouldn't be possible to get here.
throw love::Exception("Could not initialize texture to transparent black.");
}
}
}
updateGraphicsMemorySize(true);
// Non-readable textures can't have mipmaps (enforced in the base class),
// so generateMipmaps here is fine - when they aren't already initialized.
if (shouldgeneratemips)
generateMipmaps();
setSamplerState(samplerState);
}}
Texture::Texture(love::graphics::Graphics *gfx, id<MTLDevice> device, love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
: love::graphics::Texture(gfx, base, viewsettings)
{
id<MTLTexture> basetex = ((Texture *) base)->texture;
auto formatdesc = Metal::convertPixelFormat(device, format);
int slices = texType == TEXTURE_CUBE ? 6 : getLayerCount();
if (formatdesc.swizzled)
{
texture = [basetex newTextureViewWithPixelFormat:formatdesc.format
textureType:getMTLTextureType(texType, 1)
levels:NSMakeRange(parentView.startMipmap, mipmapCount)
slices:NSMakeRange(parentView.startLayer, slices)
swizzle:formatdesc.swizzle];
}
else
{
texture = [basetex newTextureViewWithPixelFormat:formatdesc.format
textureType:getMTLTextureType(texType, 1)
levels:NSMakeRange(parentView.startMipmap, mipmapCount)
slices:NSMakeRange(parentView.startLayer, slices)];
}
if (texture == nil)
throw love::Exception("Could not create Metal texture view.");
if (!debugName.empty())
texture.label = @(debugName.c_str());
setSamplerState(samplerState);
}
Texture::~Texture()
{ @autoreleasepool {
texture = nil;
msaaTexture = nil;
sampler = nil;
}}
void Texture::uploadByteData(const void *data, size_t size, int level, int slice, const Rect &r)
{ @autoreleasepool {
auto gfx = Graphics::getInstance();
id<MTLBuffer> buffer = [gfx->device newBufferWithLength:size options:MTLResourceStorageModeShared];
if (data != nullptr)
memcpy(buffer.contents, data, size);
id<MTLBlitCommandEncoder> encoder = gfx->useBlitEncoder();
int z = 0;
if (texType == TEXTURE_VOLUME)
{
z = slice;
slice = 0;
}
MTLBlitOption options = MTLBlitOptionNone;
switch (format)
{
case PIXELFORMAT_PVR1_RGB2_UNORM:
case PIXELFORMAT_PVR1_RGB4_UNORM:
case PIXELFORMAT_PVR1_RGBA2_UNORM:
case PIXELFORMAT_PVR1_RGBA4_UNORM:
if (@available(macOS 11.0, iOS 9.0, *))
options |= MTLBlitOptionRowLinearPVRTC;
break;
default:
break;
}
size_t rowSize = 0;
if (isCompressed())
rowSize = getPixelFormatCompressedBlockRowSize(format, r.w);
else
rowSize = getPixelFormatUncompressedRowSize(format, r.w);
// TODO: Verify this is correct for compressed formats at small sizes.
size_t sliceSize = getPixelFormatSliceSize(format, r.w, r.h);
// Special case: memset(0) if data is null.
// TODO: make this a more sensible part of the internal Texture API?
if (data == nullptr)
[encoder fillBuffer:buffer range:NSMakeRange(0, size) value:0];
[encoder copyFromBuffer:buffer
sourceOffset:0
sourceBytesPerRow:rowSize
sourceBytesPerImage:sliceSize
sourceSize:MTLSizeMake(r.w, r.h, 1)
toTexture:texture
destinationSlice:slice
destinationLevel:level
destinationOrigin:MTLOriginMake(r.x, r.y, z)
options:options];
}}
void Texture::generateMipmapsInternal()
{ @autoreleasepool {
id<MTLBlitCommandEncoder> encoder = Graphics::getInstance()->useBlitEncoder();
[encoder generateMipmapsForTexture:texture];
}}
void Texture::copyFromBuffer(love::graphics::Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect)
{ @autoreleasepool {
id<MTLBlitCommandEncoder> encoder = Graphics::getInstance()->useBlitEncoder();
id<MTLBuffer> buffer = (__bridge id<MTLBuffer>)(void *) source->getHandle();
size_t rowSize = 0;
if (isCompressed())
rowSize = getPixelFormatCompressedBlockRowSize(format, sourcewidth);
else
rowSize = getPixelFormatUncompressedRowSize(format, sourcewidth);
int z = texType == TEXTURE_VOLUME ? slice : 0;
MTLBlitOption options = MTLBlitOptionNone;
if (isPixelFormatDepthStencil(format))
options = MTLBlitOptionDepthFromDepthStencil;
[encoder copyFromBuffer:buffer
sourceOffset:sourceoffset
sourceBytesPerRow:rowSize
sourceBytesPerImage:size
sourceSize:MTLSizeMake(rect.w, rect.h, 1)
toTexture:texture
destinationSlice:texType == TEXTURE_VOLUME ? 0 : slice
destinationLevel:mipmap
destinationOrigin:MTLOriginMake(rect.x, rect.y, z)
options:options];
}}
void Texture::copyToBuffer(love::graphics::Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth, size_t size)
{ @autoreleasepool {
id<MTLBlitCommandEncoder> encoder = Graphics::getInstance()->useBlitEncoder();
id<MTLBuffer> buffer = (__bridge id<MTLBuffer>)(void *) dest->getHandle();
size_t rowSize = 0;
if (isCompressed())
rowSize = getPixelFormatCompressedBlockRowSize(format, destwidth);
else
rowSize = getPixelFormatUncompressedRowSize(format, destwidth);
int z = texType == TEXTURE_VOLUME ? slice : 0;
MTLBlitOption options = MTLBlitOptionNone;
if (isPixelFormatDepthStencil(format))
options = MTLBlitOptionDepthFromDepthStencil;
[encoder copyFromTexture:texture
sourceSlice:texType == TEXTURE_VOLUME ? 0 : slice
sourceLevel:mipmap
sourceOrigin:MTLOriginMake(rect.x, rect.y, z)
sourceSize:MTLSizeMake(rect.w, rect.h, 1)
toBuffer:buffer
destinationOffset:destoffset
destinationBytesPerRow:rowSize
destinationBytesPerImage:size
options:options];
}}
void Texture::setSamplerState(const SamplerState &s)
{ @autoreleasepool {
if (s.depthSampleMode.hasValue && !Graphics::getInstance()->isDepthCompareSamplerSupported())
throw love::Exception("Depth comparison sampling in shaders is not supported on this system.");
samplerState = validateSamplerState(s);
sampler = Graphics::getInstance()->getCachedSampler(samplerState);
}}
} // metal
} // graphics
} // love
|