Download
401 lines 12.4 KiBTextModified 2026-08-25 raw
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