Merge pull request #1494 from billhollings/fix-subpass-mtltex-mem-leak

Fix memory leak of dummy MTLTexture in render subpasses that use no attachments.
diff --git a/Docs/Whats_New.md b/Docs/Whats_New.md
index 234f9c6..3fabb28 100644
--- a/Docs/Whats_New.md
+++ b/Docs/Whats_New.md
@@ -22,6 +22,7 @@
 - Do not use `MTLEvent` for `VkSemaphore` under *Rosetta2*.
 - Support compiling *MSL 2.4* in runtime pipelines and `MoltenVKShaderConverterTool`.
 - Fix issue where *MSL 2.3* only available on *Apple Silicon*, even on *macOS*.
+- Fix memory leak of dummy `MTLTexture` in render subpasses that use no attachments.
 - Fix Metal object retain-release errors in assignment operators.
 - Update to latest SPIRV-Cross:
 	- MSL: Add 64 bit support for `OpSwitch`.
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm b/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm
index 98ba2c1..2a98a87 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm
@@ -64,8 +64,7 @@
 	cmdEncoder->beginRenderpass(this,
 								_contents,
 								_renderPass,
-								_framebuffer->getExtent2D(),
-								_framebuffer->getLayerCount(),
+								_framebuffer,
 								_renderArea,
 								_clearValues.contents(),
 								_attachments.contents());
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm b/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm
index a4d81da..72e36e5 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm
@@ -1234,7 +1234,7 @@
 	simd::float4 vertices[vtxCnt];
 	simd::float4 clearColors[kMVKClearAttachmentCount];
 
-	VkExtent2D fbExtent = cmdEncoder->_framebufferExtent;
+	VkExtent2D fbExtent = cmdEncoder->getFramebufferExtent();
 #if MVK_MACOS_OR_IOS
 	// I need to know if the 'renderTargetWidth' and 'renderTargetHeight' properties
 	// actually do something, but [MTLRenderPassDescriptor instancesRespondToSelector: @selector(renderTargetWidth)]
@@ -1255,7 +1255,7 @@
     // Populate the render pipeline state attachment key with info from the subpass and framebuffer.
 	_rpsKey.mtlSampleCount = mvkSampleCountFromVkSampleCountFlagBits(subpass->getSampleCount());
 	if (cmdEncoder->_canUseLayeredRendering &&
-		(cmdEncoder->_framebufferLayerCount > 1 || cmdEncoder->getSubpass()->isMultiview())) {
+		(cmdEncoder->getFramebufferLayerCount() > 1 || cmdEncoder->getSubpass()->isMultiview())) {
 		_rpsKey.enableLayeredRendering();
 	}
 
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
index 489f74a..7803bf2 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
@@ -283,8 +283,7 @@
 	void beginRenderpass(MVKCommand* passCmd,
 						 VkSubpassContents subpassContents,
 						 MVKRenderPass* renderPass,
-						 VkExtent2D framebufferExtent,
-						 uint32_t framebufferLayerCount,
+						 MVKFramebuffer* framebuffer,
 						 VkRect2D& renderArea,
 						 MVKArrayRef<VkClearValue> clearValues,
 						 MVKArrayRef<MVKImageView*> attachments);
@@ -307,6 +306,12 @@
 	/** Returns the render subpass that is currently active. */
 	MVKRenderSubpass* getSubpass();
 
+	/** The extent of current framebuffer.*/
+	VkExtent2D getFramebufferExtent();
+
+	/** The layer count of current framebuffer.*/
+	uint32_t getFramebufferLayerCount();
+
 	/** Returns the index of the currently active multiview subpass, or zero if the current render pass is not multiview. */
 	uint32_t getMultiviewPassIndex();
 
@@ -483,12 +488,6 @@
 	/** Indicates whether the current draw is an indexed draw. */
 	bool _isIndexedDraw;
 
-	/** The extent of current framebuffer.*/
-	VkExtent2D _framebufferExtent;
-
-	/** The layer count of current framebuffer.*/
-	uint32_t _framebufferLayerCount;
-
 #pragma mark Construction
 
 	MVKCommandEncoder(MVKCommandBuffer* cmdBuffer);
@@ -513,6 +512,7 @@
 
 	VkSubpassContents _subpassContents;
 	MVKRenderPass* _renderPass;
+	MVKFramebuffer* _framebuffer;
 	MVKCommand* _lastMultiviewPassCmd;
 	uint32_t _renderSubpassIndex;
 	uint32_t _multiviewPassIndex;
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
index c9a176c..b363fe2 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
@@ -17,6 +17,7 @@
  */
 
 #include "MVKCommandBuffer.h"
+#include "MVKFramebuffer.h"
 #include "MVKCommandPool.h"
 #include "MVKQueue.h"
 #include "MVKPipeline.h"
@@ -250,6 +251,7 @@
 
 void MVKCommandEncoder::encode(id<MTLCommandBuffer> mtlCmdBuff,
 							   MVKCommandEncodingContext* pEncodingContext) {
+	_framebuffer = nullptr;
 	_renderPass = nullptr;
 	_subpassContents = VK_SUBPASS_CONTENTS_INLINE;
 	_renderSubpassIndex = 0;
@@ -289,17 +291,15 @@
 void MVKCommandEncoder::beginRenderpass(MVKCommand* passCmd,
 										VkSubpassContents subpassContents,
 										MVKRenderPass* renderPass,
-										VkExtent2D framebufferExtent,
-										uint32_t framebufferLayerCount,
+										MVKFramebuffer* framebuffer,
 										VkRect2D& renderArea,
 										MVKArrayRef<VkClearValue> clearValues,
 										MVKArrayRef<MVKImageView*> attachments) {
 	_renderPass = renderPass;
-	_framebufferExtent = framebufferExtent;
-	_framebufferLayerCount = framebufferLayerCount;
+	_framebuffer = framebuffer;
 	_renderArea = renderArea;
 	_isRenderingEntireAttachment = (mvkVkOffset2DsAreEqual(_renderArea.offset, {0,0}) &&
-									mvkVkExtent2DsAreEqual(_renderArea.extent, _framebufferExtent));
+									mvkVkExtent2DsAreEqual(_renderArea.extent, getFramebufferExtent()));
 	_clearValues.assign(clearValues.begin(), clearValues.end());
 	_attachments.assign(attachments.begin(), attachments.end());
 	setSubpass(passCmd, subpassContents, 0);
@@ -346,8 +346,7 @@
     MTLRenderPassDescriptor* mtlRPDesc = [MTLRenderPassDescriptor renderPassDescriptor];
 	getSubpass()->populateMTLRenderPassDescriptor(mtlRPDesc,
 												  _multiviewPassIndex,
-												  _framebufferExtent,
-												  _framebufferLayerCount,
+												  _framebuffer,
 												  _attachments.contents(),
 												  _clearValues.contents(),
 												  _isRenderingEntireAttachment,
@@ -359,7 +358,7 @@
 		mtlRPDesc.visibilityResultBuffer = _pEncodingContext->visibilityResultBuffer->_mtlBuffer;
 	}
 
-	VkExtent2D fbExtent = _framebufferExtent;
+	VkExtent2D fbExtent = getFramebufferExtent();
     mtlRPDesc.renderTargetWidthMVK = max(min(_renderArea.offset.x + _renderArea.extent.width, fbExtent.width), 1u);
     mtlRPDesc.renderTargetHeightMVK = max(min(_renderArea.offset.y + _renderArea.extent.height, fbExtent.height), 1u);
     if (_canUseLayeredRendering) {
@@ -381,7 +380,7 @@
             // We need to use the view count for this multiview pass.
 			renderTargetArrayLength = getSubpass()->getViewCountInMetalPass(_multiviewPassIndex);
         } else {
-			renderTargetArrayLength = _framebufferLayerCount;
+			renderTargetArrayLength = getFramebufferLayerCount();
         }
         // Metal does not allow layered render passes where some RTs are 3D and others are 2D.
         if (!(found3D && found2D) || renderTargetArrayLength > 1) {
@@ -434,6 +433,10 @@
 	return mvkMTLRenderCommandEncoderLabel(cmdUse);
 }
 
+VkExtent2D MVKCommandEncoder::getFramebufferExtent() { return _framebuffer ? _framebuffer->getExtent2D() : VkExtent2D{0,0}; }
+
+uint32_t MVKCommandEncoder::getFramebufferLayerCount() { return _framebuffer ? _framebuffer->getLayerCount() : 0; }
+
 void MVKCommandEncoder::bindPipeline(VkPipelineBindPoint pipelineBindPoint, MVKPipeline* pipeline) {
     switch (pipelineBindPoint) {
         case VK_PIPELINE_BIND_POINT_GRAPHICS:
@@ -530,7 +533,7 @@
 		VkClearRect clearRect;
 		clearRect.rect = _renderArea;
 		clearRect.baseArrayLayer = 0;
-		clearRect.layerCount = _framebufferLayerCount;
+		clearRect.layerCount = getFramebufferLayerCount();
 
 		// Create and execute a temporary clear attachments command.
 		// To be threadsafe...do NOT acquire and return the command from the pool.
@@ -577,8 +580,7 @@
 	endMetalRenderEncoding();
 
 	_renderPass = nullptr;
-	_framebufferExtent = {};
-	_framebufferLayerCount = 0;
+	_framebuffer = nullptr;
 	_attachments.clear();
 	_renderSubpassIndex = 0;
 }
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.h b/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.h
index 928da95..39beb73 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.h
@@ -21,6 +21,9 @@
 #include "MVKDevice.h"
 #include "MVKImage.h"
 #include "MVKSmallVector.h"
+#include <mutex>
+
+class MVKRenderSubpass;
 
 
 #pragma mark MVKFramebuffer
@@ -45,16 +48,25 @@
 	/** Returns the attachments.  */
 	MVKArrayRef<MVKImageView*> getAttachments() { return _attachments.contents(); }
 
+	/**
+	 * Returns a MTLTexture for use as a dummy texture when a render subpass,
+	 * that is compatible with the specified subpass, has no attachments.
+	 */
+	id<MTLTexture> getDummyAttachmentMTLTexture(MVKRenderSubpass* subpass, uint32_t passIdx);
+
 #pragma mark Construction
 
-	/** Constructs an instance for the specified device. */
 	MVKFramebuffer(MVKDevice* device, const VkFramebufferCreateInfo* pCreateInfo);
 
+	~MVKFramebuffer() override;
+
 protected:
 	void propagateDebugName() override {}
 
+	MVKSmallVector<MVKImageView*, 4> _attachments;
+	id<MTLTexture> _mtlDummyTex = nil;
+	std::mutex _lock;
 	VkExtent2D _extent;
 	uint32_t _layerCount;
-	MVKSmallVector<MVKImageView*, 4> _attachments;
 };
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.mm b/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.mm
index 91d3bde..f65e0ae 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKFramebuffer.mm
@@ -17,11 +17,68 @@
  */
 
 #include "MVKFramebuffer.h"
+#include "MVKRenderPass.h"
+
+using namespace std;
 
 
 #pragma mark MVKFramebuffer
 
-#pragma mark Construction
+id<MTLTexture> MVKFramebuffer::getDummyAttachmentMTLTexture(MVKRenderSubpass* subpass, uint32_t passIdx) {
+	if (_mtlDummyTex) { return _mtlDummyTex; }
+
+	// Lock and check again in case another thread has created the texture.
+	lock_guard<mutex> lock(_lock);
+	if (_mtlDummyTex) { return _mtlDummyTex; }
+
+	VkExtent2D fbExtent = getExtent2D();
+	uint32_t fbLayerCount = getLayerCount();
+	uint32_t sampleCount = mvkSampleCountFromVkSampleCountFlagBits(subpass->getDefaultSampleCount());
+	MTLTextureDescriptor* mtlTexDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: MTLPixelFormatR8Unorm width: fbExtent.width height: fbExtent.height mipmapped: NO];
+	if (subpass->isMultiview()) {
+#if MVK_MACOS_OR_IOS
+		if (sampleCount > 1 && getDevice()->_pMetalFeatures->multisampleLayeredRendering) {
+			mtlTexDesc.textureType = MTLTextureType2DMultisampleArray;
+			mtlTexDesc.sampleCount = sampleCount;
+		} else {
+			mtlTexDesc.textureType = MTLTextureType2DArray;
+		}
+#else
+		mtlTexDesc.textureType = MTLTextureType2DArray;
+#endif
+		mtlTexDesc.arrayLength = subpass->getViewCountInMetalPass(passIdx);
+	} else if (fbLayerCount > 1) {
+#if MVK_MACOS
+		if (sampleCount > 1 && getDevice()->_pMetalFeatures->multisampleLayeredRendering) {
+			mtlTexDesc.textureType = MTLTextureType2DMultisampleArray;
+			mtlTexDesc.sampleCount = sampleCount;
+		} else {
+			mtlTexDesc.textureType = MTLTextureType2DArray;
+		}
+#else
+		mtlTexDesc.textureType = MTLTextureType2DArray;
+#endif
+		mtlTexDesc.arrayLength = fbLayerCount;
+	} else if (sampleCount > 1) {
+		mtlTexDesc.textureType = MTLTextureType2DMultisample;
+		mtlTexDesc.sampleCount = sampleCount;
+	}
+#if MVK_IOS
+	if ([getMTLDevice() supportsFeatureSet: MTLFeatureSet_iOS_GPUFamily1_v3]) {
+		mtlTexDesc.storageMode = MTLStorageModeMemoryless;
+	} else {
+		mtlTexDesc.storageMode = MTLStorageModePrivate;
+	}
+#else
+	mtlTexDesc.storageMode = MTLStorageModePrivate;
+#endif
+	mtlTexDesc.usage = MTLTextureUsageRenderTarget;
+
+	_mtlDummyTex = [getMTLDevice() newTextureWithDescriptor: mtlTexDesc];	// retained
+	[_mtlDummyTex setPurgeableState: MTLPurgeableStateVolatile];
+
+	return _mtlDummyTex;
+}
 
 MVKFramebuffer::MVKFramebuffer(MVKDevice* device,
 							   const VkFramebufferCreateInfo* pCreateInfo) : MVKVulkanAPIDeviceObject(device) {
@@ -36,3 +93,8 @@
 		}
 	}
 }
+
+MVKFramebuffer::~MVKFramebuffer() {
+	[_mtlDummyTex release];
+}
+
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h
index 93695ba..6dfb228 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h
@@ -76,6 +76,9 @@
 	/** Returns the Vulkan sample count of the attachments used in this subpass. */
 	VkSampleCountFlagBits getSampleCount();
 
+	/** Returns the default sample count for when there are no attachments used in this subpass. */
+	VkSampleCountFlagBits getDefaultSampleCount() { return _defaultSampleCount; }
+
 	/** Sets the default sample count for when there are no attachments used in this subpass. */
 	void setDefaultSampleCount(VkSampleCountFlagBits count) { _defaultSampleCount = count; }
 
@@ -104,8 +107,7 @@
 	 */
 	void populateMTLRenderPassDescriptor(MTLRenderPassDescriptor* mtlRPDesc,
 										 uint32_t passIdx,
-										 VkExtent2D framebufferExtent,
-										 uint32_t framebufferLayerCount,
+										 MVKFramebuffer* framebuffer,
 										 const MVKArrayRef<MVKImageView*> attachments,
 										 const MVKArrayRef<VkClearValue> clearValues,
 										 bool isRenderingEntireAttachment,
@@ -161,7 +163,6 @@
 	VkAttachmentReference2 _depthStencilResolveAttachment;
 	VkResolveModeFlagBits _depthResolveMode = VK_RESOLVE_MODE_NONE;
 	VkResolveModeFlagBits _stencilResolveMode = VK_RESOLVE_MODE_NONE;
-	id<MTLTexture> _mtlDummyTex = nil;
 	VkSampleCountFlagBits _defaultSampleCount = VK_SAMPLE_COUNT_1_BIT;
 };
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm
index 47dc9c3..7e1a929 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm
@@ -196,8 +196,7 @@
 
 void MVKRenderSubpass::populateMTLRenderPassDescriptor(MTLRenderPassDescriptor* mtlRPDesc,
 													   uint32_t passIdx,
-													   VkExtent2D framebufferExtent,
-													   uint32_t framebufferLayerCount,
+													   MVKFramebuffer* framebuffer,
 													   const MVKArrayRef<MVKImageView*> attachments,
 													   const MVKArrayRef<VkClearValue> clearValues,
 													   bool isRenderingEntireAttachment,
@@ -326,67 +325,22 @@
 		}
 	}
 
-	_mtlDummyTex = nil;
+	// Vulkan supports rendering without attachments, but older Metal does not.
+	// If Metal does not support rendering without attachments, create a dummy attachment to pass Metal validation.
 	if (caUsedCnt == 0 && dsRPAttIdx == VK_ATTACHMENT_UNUSED) {
-		uint32_t sampleCount = mvkSampleCountFromVkSampleCountFlagBits(_defaultSampleCount);
         if (_renderPass->getDevice()->_pMetalFeatures->renderWithoutAttachments) {
-            // We support having no attachments.
 #if MVK_MACOS_OR_IOS
-            mtlRPDesc.defaultRasterSampleCount = sampleCount;
+            mtlRPDesc.defaultRasterSampleCount = mvkSampleCountFromVkSampleCountFlagBits(_defaultSampleCount);
 #endif
-            return;
-        }
-
-		// Add a dummy attachment so this passes validation.
-		VkExtent2D fbExtent = framebufferExtent;
-		MTLTextureDescriptor* mtlTexDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: MTLPixelFormatR8Unorm width: fbExtent.width height: fbExtent.height mipmapped: NO];
-		if (isMultiview()) {
-#if MVK_MACOS_OR_IOS
-			if (sampleCount > 1 && _renderPass->getDevice()->_pMetalFeatures->multisampleLayeredRendering) {
-				mtlTexDesc.textureType = MTLTextureType2DMultisampleArray;
-				mtlTexDesc.sampleCount = sampleCount;
-			} else {
-				mtlTexDesc.textureType = MTLTextureType2DArray;
-			}
-#else
-			mtlTexDesc.textureType = MTLTextureType2DArray;
-#endif
-			mtlTexDesc.arrayLength = getViewCountInMetalPass(passIdx);
-		} else if (framebufferLayerCount > 1) {
-#if MVK_MACOS
-			if (sampleCount > 1 && _renderPass->getDevice()->_pMetalFeatures->multisampleLayeredRendering) {
-				mtlTexDesc.textureType = MTLTextureType2DMultisampleArray;
-				mtlTexDesc.sampleCount = sampleCount;
-			} else {
-				mtlTexDesc.textureType = MTLTextureType2DArray;
-			}
-#else
-			mtlTexDesc.textureType = MTLTextureType2DArray;
-#endif
-			mtlTexDesc.arrayLength = framebufferLayerCount;
-		} else if (sampleCount > 1) {
-			mtlTexDesc.textureType = MTLTextureType2DMultisample;
-			mtlTexDesc.sampleCount = sampleCount;
-		}
-#if MVK_IOS
-		if ([_renderPass->getMTLDevice() supportsFeatureSet: MTLFeatureSet_iOS_GPUFamily1_v3]) {
-			mtlTexDesc.storageMode = MTLStorageModeMemoryless;
 		} else {
-			mtlTexDesc.storageMode = MTLStorageModePrivate;
+			MTLRenderPassColorAttachmentDescriptor* mtlColorAttDesc = mtlRPDesc.colorAttachments[0];
+			mtlColorAttDesc.texture = framebuffer->getDummyAttachmentMTLTexture(this, passIdx);
+			mtlColorAttDesc.level = 0;
+			mtlColorAttDesc.slice = 0;
+			mtlColorAttDesc.depthPlane = 0;
+			mtlColorAttDesc.loadAction = MTLLoadActionDontCare;
+			mtlColorAttDesc.storeAction = MTLStoreActionDontCare;
 		}
-#else
-		mtlTexDesc.storageMode = MTLStorageModePrivate;
-#endif
-		mtlTexDesc.usage = MTLTextureUsageRenderTarget;
-		_mtlDummyTex = [_renderPass->getMTLDevice() newTextureWithDescriptor: mtlTexDesc];  // not retained
-		[_mtlDummyTex setPurgeableState: MTLPurgeableStateVolatile];
-		MTLRenderPassColorAttachmentDescriptor* mtlColorAttDesc = mtlRPDesc.colorAttachments[0];
-		mtlColorAttDesc.texture = _mtlDummyTex;
-		mtlColorAttDesc.level = 0;
-		mtlColorAttDesc.slice = 0;
-		mtlColorAttDesc.depthPlane = 0;
-		mtlColorAttDesc.loadAction = MTLLoadActionDontCare;
-		mtlColorAttDesc.storeAction = MTLStoreActionDontCare;
 	}
 }