Merge pull request #1566 from billhollings/VK_EXT_sample_locations

Add support for VK_EXT_sample_locations extension.
diff --git a/Docs/MoltenVK_Runtime_UserGuide.md b/Docs/MoltenVK_Runtime_UserGuide.md
index e6f9874..b09d066 100644
--- a/Docs/MoltenVK_Runtime_UserGuide.md
+++ b/Docs/MoltenVK_Runtime_UserGuide.md
@@ -307,6 +307,7 @@
 - `VK_EXT_post_depth_coverage` *(iOS and macOS, requires family 4 (A11) or better Apple GPU)*
 - `VK_EXT_private_data `
 - `VK_EXT_robustness2`
+- `VK_EXT_sample_locations`
 - `VK_EXT_scalar_block_layout`
 - `VK_EXT_shader_stencil_export` *(requires Mac GPU family 2 or iOS GPU family 5)*
 - `VK_EXT_shader_viewport_index_layer`
diff --git a/Docs/Whats_New.md b/Docs/Whats_New.md
index 80c235a..8c0b556 100644
--- a/Docs/Whats_New.md
+++ b/Docs/Whats_New.md
@@ -18,6 +18,8 @@
 
 Released TBD
 
+- Add support for extensions:
+	- `VK_EXT_sample_locations`
 - Fixes to pipeline layout compatibility.
 - Reinstate memory barriers on non-Apple GPUs, which were inadvertently disabled in an earlier update.
 - Support base vertex instance support in shader conversion.
@@ -29,6 +31,7 @@
 - Fixes to optimize resource objects retained by descriptors beyond their lifetimes.
 - `MoltenVKShaderConverter` tool defaults to the highest MSL version supported on runtime OS.
 - Update *glslang* version, to use `python3` in *glslang* scripts, to replace missing `python` on *macOS 12.3*.
+- Update `VK_MVK_MOLTENVK_SPEC_VERSION` to version `34`.
 - Update to latest SPIRV-Cross:
 	- MSL: Support input/output blocks containing nested struct arrays.
 	- MSL: Use var name instead of var-type name for flattened interface members.
diff --git a/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h b/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
index 583aef9..d4163a9 100644
--- a/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
+++ b/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
@@ -55,7 +55,7 @@
 #define MVK_MAKE_VERSION(major, minor, patch)    (((major) * 10000) + ((minor) * 100) + (patch))
 #define MVK_VERSION     MVK_MAKE_VERSION(MVK_VERSION_MAJOR, MVK_VERSION_MINOR, MVK_VERSION_PATCH)
 
-#define VK_MVK_MOLTENVK_SPEC_VERSION            33
+#define VK_MVK_MOLTENVK_SPEC_VERSION            34
 #define VK_MVK_MOLTENVK_EXTENSION_NAME          "VK_MVK_moltenvk"
 
 /** Identifies the level of logging MoltenVK should be limited to outputting. */
@@ -786,7 +786,7 @@
 	 * command buffer submission, to a physically removed GPU. In the case where this error does
 	 * not impact the VkPhysicalDevice, Vulkan requires that the app destroy and re-create a new
 	 * VkDevice. However, not all apps (including CTS) respect that requirement, leading to what
-	 * might be a transient command submission failure causing an unexpected catastophic app failure.
+	 * might be a transient command submission failure causing an unexpected catastrophic app failure.
 	 *
 	 * If this setting is enabled, in the case of a VK_ERROR_DEVICE_LOST error that does NOT impact
 	 * the VkPhysicalDevice, MoltenVK will log the error, but will not mark the VkDevice as lost,
@@ -929,6 +929,7 @@
 	VkBool32 descriptorSetArgumentBuffers;			/**< If true, a Metal argument buffer can be assigned to a descriptor set, and used on any pipeline and pipeline stage. If false, a different Metal argument buffer must be used for each pipeline-stage/descriptor-set combination. */
 	MVKFloatRounding clearColorFloatRounding;		/**< Identifies the type of rounding Metal uses for MTLClearColor float to integer conversions. */
 	MVKCounterSamplingFlags counterSamplingPoints;	/**< Identifies the points where pipeline GPU counter sampling may occur. */
+	VkBool32 programmableSamplePositions;			/**< If true, programmable MSAA sample positions are supported. */
 } MVKPhysicalDeviceMetalFeatures;
 
 /** MoltenVK performance of a particular type of activity. */
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.h b/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.h
index 15c40fc..dcb3b02 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.h
@@ -46,6 +46,7 @@
 
 protected:
 
+	MVKSmallVector<MVKSmallVector<MTLSamplePosition>> _subpassSamplePositions;
 	MVKRenderPass* _renderPass;
 	MVKFramebuffer* _framebuffer;
 	VkRect2D _renderArea;
@@ -138,6 +139,25 @@
 
 
 #pragma mark -
+#pragma mark MVKCmdSetSampleLocations
+
+/** Vulkan command to dynamically set custom sample locations. */
+class MVKCmdSetSampleLocations : public MVKCommand {
+
+public:
+	VkResult setContent(MVKCommandBuffer* cmdBuff,
+						const VkSampleLocationsInfoEXT* pSampleLocationsInfo);
+
+	void encode(MVKCommandEncoder* cmdEncoder) override;
+
+protected:
+	MVKCommandTypePool<MVKCommand>* getTypePool(MVKCommandPool* cmdPool) override;
+
+	MVKSmallVector<MTLSamplePosition, 8> _samplePositions;
+};
+
+
+#pragma mark -
 #pragma mark MVKCmdExecuteCommands
 
 /**
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm b/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm
index 56f0d49..f621be8 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdRenderPass.mm
@@ -36,6 +36,30 @@
 	_renderPass = (MVKRenderPass*)pRenderPassBegin->renderPass;
 	_framebuffer = (MVKFramebuffer*)pRenderPassBegin->framebuffer;
 	_renderArea = pRenderPassBegin->renderArea;
+	_subpassSamplePositions.clear();
+
+	for (const auto* next = (VkBaseInStructure*)pRenderPassBegin->pNext; next; next = next->pNext) {
+		switch (next->sType) {
+			case VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT: {
+				// Build an array of arrays, one array of sample positions for each subpass index.
+				// For subpasses not included in VkRenderPassSampleLocationsBeginInfoEXT, the resulting array of samples will be empty.
+				_subpassSamplePositions.resize(_renderPass->getSubpassCount());
+				auto* pRPSampLocnsInfo = (VkRenderPassSampleLocationsBeginInfoEXT*)next;
+				for (uint32_t spSLIdx = 0; spSLIdx < pRPSampLocnsInfo->postSubpassSampleLocationsCount; spSLIdx++) {
+					auto& spsl = pRPSampLocnsInfo->pPostSubpassSampleLocations[spSLIdx];
+					uint32_t spIdx = spsl.subpassIndex;
+					auto& spSampPosns = _subpassSamplePositions[spIdx];
+					for (uint32_t slIdx = 0; slIdx < spsl.sampleLocationsInfo.sampleLocationsCount; slIdx++) {
+						auto& sl = spsl.sampleLocationsInfo.pSampleLocations[slIdx];
+						spSampPosns.push_back(MTLSamplePositionMake(sl.x, sl.y));
+					}
+				}
+				break;
+			}
+			default:
+				break;
+		}
+	}
 
 	return VK_SUCCESS;
 }
@@ -61,13 +85,23 @@
 template <size_t N_CV, size_t N_A>
 void MVKCmdBeginRenderPass<N_CV, N_A>::encode(MVKCommandEncoder* cmdEncoder) {
 //	MVKLogDebug("Encoding vkCmdBeginRenderPass(). Elapsed time: %.6f ms.", mvkGetElapsedMilliseconds());
+
+	// Convert the sample position array of arrays to an array of array-references,
+	// so that it can be passed to the command encoder.
+	size_t spSPCnt = _subpassSamplePositions.size();
+	MVKArrayRef<MTLSamplePosition> spSPRefs[spSPCnt];
+	for (uint32_t spSPIdx = 0; spSPIdx < spSPCnt; spSPIdx++) {
+		spSPRefs[spSPIdx] = _subpassSamplePositions[spSPIdx].contents();
+	}
+	
 	cmdEncoder->beginRenderpass(this,
 								_contents,
 								_renderPass,
 								_framebuffer,
 								_renderArea,
 								_clearValues.contents(),
-								_attachments.contents());
+								_attachments.contents(),
+								MVKArrayRef(spSPRefs, spSPCnt));
 }
 
 template class MVKCmdBeginRenderPass<1, 0>;
@@ -130,6 +164,24 @@
 		cmdEncoder->endRenderpass();
 }
 
+#pragma mark -
+#pragma mark MVKCmdSetSampleLocations
+
+VkResult MVKCmdSetSampleLocations::setContent(MVKCommandBuffer* cmdBuff,
+											  const VkSampleLocationsInfoEXT* pSampleLocationsInfo) {
+
+	for (uint32_t slIdx = 0; slIdx < pSampleLocationsInfo->sampleLocationsCount; slIdx++) {
+		auto& sl = pSampleLocationsInfo->pSampleLocations[slIdx];
+		_samplePositions.push_back(MTLSamplePositionMake(sl.x, sl.y));
+	}
+
+	return VK_SUCCESS;
+}
+
+void MVKCmdSetSampleLocations::encode(MVKCommandEncoder* cmdEncoder) {
+	cmdEncoder->setDynamicSamplePositions(_samplePositions.contents());
+}
+
 
 #pragma mark -
 #pragma mark MVKCmdExecuteCommands
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
index 9ad5e61..49a74ed 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
@@ -191,74 +191,6 @@
 #pragma mark -
 #pragma mark MVKCommandEncoder
 
-// The following commands can be issued both inside and outside a renderpass and their state must
-// span multiple MTLRenderCommandEncoders, to allow state to be set before a renderpass, and to
-// allow more than one MTLRenderCommandEncoder to be used for a single Vulkan renderpass or subpass.
-//
-// + vkCmdBindPipeline() : _graphicsPipelineState & _computePipelineState
-// + vkCmdBindDescriptorSets() : _graphicsResourcesState & _computeResourcesState
-// + vkCmdBindVertexBuffers() : _graphicsResourcesState
-// + vkCmdBindIndexBuffer() : _graphicsResourcesState
-// + vkCmdPushConstants() : _vertexPushConstants & _tessCtlPushConstants & _tessEvalPushConstants & _fragmentPushConstants & _computePushConstants
-// + vkCmdSetViewport() : _viewportState
-// + vkCmdSetDepthBias() : _depthBiasState
-// + vkCmdSetScissor() : _scissorState
-// + vkCmdSetStencilCompareMask() : _depthStencilState
-// + vkCmdSetStencilWriteMask() : _depthStencilState
-// + vkCmdSetStencilReference() : _stencilReferenceValueState
-// + vkCmdSetBlendConstants() : _blendColorState
-// + vkCmdBeginQuery() : _occlusionQueryState
-// + vkCmdEndQuery() : _occlusionQueryState
-// + vkCmdPipelineBarrier() : handled via textureBarrier and MTLBlitCommandEncoder
-// + vkCmdWriteTimestamp() : doesn't affect MTLCommandEncoders
-// + vkCmdExecuteCommands() : state managed by embedded commands
-// - vkCmdSetLineWidth() - unsupported by Metal
-// - vkCmdSetDepthBounds() - unsupported by Metal
-// - vkCmdWaitEvents() - unsupported by Metal
-
-// The above list of Vulkan commands covers the following corresponding MTLRenderCommandEncoder state:
-// + setBlendColorRed : _blendColorState
-// + setCullMode : _graphicsPipelineState
-// + setDepthBias : _depthBiasState
-// + setDepthClipMode : _graphicsPipelineState
-// + setDepthStencilState : _depthStencilState
-// + setFrontFacingWinding : _graphicsPipelineState
-// + setRenderPipelineState : _graphicsPipelineState
-// + setScissorRect : _scissorState
-// + setStencilFrontReferenceValue : _stencilReferenceValueState
-// + setStencilReferenceValue (unused) : _stencilReferenceValueState
-// + setTriangleFillMode : _graphicsPipelineState
-// + setViewport : _viewportState
-// + setVisibilityResultMode : _occlusionQueryState
-// + setVertexBuffer : _graphicsResourcesState & _vertexPushConstants & _tessEvalPushConstants
-// + setVertexBuffers (unused) : _graphicsResourcesState
-// + setVertexBytes : _vertexPushConstants & _tessEvalPushConstants
-// + setVertexBufferOffset (unused) : _graphicsResourcesState
-// + setVertexTexture : _graphicsResourcesState
-// + setVertexTextures (unused) : _graphicsResourcesState
-// + setVertexSamplerState : _graphicsResourcesState
-// + setVertexSamplerStates : (unused) : _graphicsResourcesState
-// + setFragmentBuffer : _graphicsResourcesState & _fragmentPushConstants
-// + setFragmentBuffers (unused) : _graphicsResourcesState
-// + setFragmentBytes : _fragmentPushConstants
-// + setFragmentBufferOffset (unused) : _graphicsResourcesState
-// + setFragmentTexture : _graphicsResourcesState
-// + setFragmentTextures (unused) : _graphicsResourcesState
-// + setFragmentSamplerState : _graphicsResourcesState
-// + setFragmentSamplerStates : (unused) : _graphicsResourcesState
-
-// The above list of Vulkan commands covers the following corresponding MTLComputeCommandEncoder state:
-// + setComputePipelineState : _computePipelineState & _graphicsPipelineState
-// + setBuffer : _computeResourcesState & _computePushConstants & _graphicsResourcesState & _tessCtlPushConstants
-// + setBuffers (unused) : _computeResourcesState & _graphicsResourcesState
-// + setBytes : _computePushConstants & _tessCtlPushConstants
-// + setBufferOffset (unused) : _computeResourcesState & _graphicsResourcesState
-// + setTexture : _computeResourcesState & _graphicsResourcesState
-// + setTextures (unused) : _computeResourcesState & _graphicsResourcesState
-// + setSamplerState : _computeResourcesState & _graphicsResourcesState
-// + setSamplerStates : (unused) : _computeResourcesState & _graphicsResourcesState
-
-
 /*** Holds a collection of active queries for each query pool. */
 typedef std::unordered_map<MVKQueryPool*, MVKSmallVector<uint32_t, kMVKDefaultQueryCount>> MVKActivatedQueries;
 
@@ -293,7 +225,8 @@
 						 MVKFramebuffer* framebuffer,
 						 VkRect2D& renderArea,
 						 MVKArrayRef<VkClearValue> clearValues,
-						 MVKArrayRef<MVKImageView*> attachments);
+						 MVKArrayRef<MVKImageView*> attachments,
+						 MVKArrayRef<MVKArrayRef<MTLSamplePosition>> subpassSamplePositions);
 
 	/** Begins the next render subpass. */
 	void beginNextSubpass(MVKCommand* subpassCmd, VkSubpassContents renderpassContents);
@@ -301,6 +234,9 @@
 	/** Begins the next multiview Metal render pass. */
 	void beginNextMultiviewPass();
 
+	/** Sets the dynamic custom sample positions to use when rendering. */
+	void setDynamicSamplePositions(MVKArrayRef<MTLSamplePosition> dynamicSamplePositions);
+
 	/** Begins a Metal render pass for the current render subpass. */
 	void beginMetalRenderPass(MVKCommandUse cmdUse);
 
@@ -509,6 +445,7 @@
 	void encodeTimestampStageCounterSamples();
 	bool hasTimestampStageCounterQueries() { return !_timestampStageCounterQueries.empty(); }
 	id<MTLFence> getStageCountersMTLFence();
+	MVKArrayRef<MTLSamplePosition> getCustomSamplePositions();
 
 	typedef struct GPUCounterQuery {
 		MVKGPUCounterQueryPool* queryPool = nullptr;
@@ -526,6 +463,8 @@
 	MVKSmallVector<GPUCounterQuery, 16> _timestampStageCounterQueries;
 	MVKSmallVector<VkClearValue, kMVKDefaultAttachmentCount> _clearValues;
 	MVKSmallVector<MVKImageView*, kMVKDefaultAttachmentCount> _attachments;
+	MVKSmallVector<MTLSamplePosition> _dynamicSamplePositions;
+	MVKSmallVector<MVKSmallVector<MTLSamplePosition>> _subpassSamplePositions;
 	id<MTLComputeCommandEncoder> _mtlComputeEncoder;
 	MVKCommandUse _mtlComputeEncoderUse;
 	id<MTLBlitCommandEncoder> _mtlBlitEncoder;
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
index 574cb98..e27ccb5 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
@@ -322,7 +322,8 @@
 										MVKFramebuffer* framebuffer,
 										VkRect2D& renderArea,
 										MVKArrayRef<VkClearValue> clearValues,
-										MVKArrayRef<MVKImageView*> attachments) {
+										MVKArrayRef<MVKImageView*> attachments,
+										MVKArrayRef<MVKArrayRef<MTLSamplePosition>> subpassSamplePositions) {
 	_renderPass = renderPass;
 	_framebuffer = framebuffer;
 	_renderArea = renderArea;
@@ -330,6 +331,14 @@
 									mvkVkExtent2DsAreEqual(_renderArea.extent, getFramebufferExtent()));
 	_clearValues.assign(clearValues.begin(), clearValues.end());
 	_attachments.assign(attachments.begin(), attachments.end());
+
+	// Copy the sample positions array of arrays, one array of sample positions for each subpass index.
+	_subpassSamplePositions.resize(subpassSamplePositions.size);
+	for (uint32_t spSPIdx = 0; spSPIdx < subpassSamplePositions.size; spSPIdx++) {
+		_subpassSamplePositions[spSPIdx].assign(subpassSamplePositions[spSPIdx].begin(),
+												subpassSamplePositions[spSPIdx].end());
+	}
+
 	setSubpass(passCmd, subpassContents, 0);
 }
 
@@ -365,6 +374,10 @@
 
 uint32_t MVKCommandEncoder::getMultiviewPassIndex() { return _multiviewPassIndex; }
 
+void MVKCommandEncoder::setDynamicSamplePositions(MVKArrayRef<MTLSamplePosition> dynamicSamplePositions) {
+	_dynamicSamplePositions.assign(dynamicSamplePositions.begin(), dynamicSamplePositions.end());
+}
+
 // Creates _mtlRenderEncoder and marks cached render state as dirty so it will be set into the _mtlRenderEncoder.
 void MVKCommandEncoder::beginMetalRenderPass(MVKCommandUse cmdUse) {
 
@@ -416,6 +429,14 @@
         }
     }
 
+	// If programmable sample positions are supported, set them into the render pass descriptor.
+	// If no custom sample positions are established, size will be zero,
+	// and Metal will default to using default sample postions.
+	if (_pDeviceMetalFeatures->programmableSamplePositions) {
+		auto cstmSampPosns = getCustomSamplePositions();
+		[mtlRPDesc setSamplePositions: cstmSampPosns.data count: cstmSampPosns.size];
+	}
+
     _mtlRenderEncoder = [_mtlCmdBuffer renderCommandEncoderWithDescriptor: mtlRPDesc];     // not retained
 	setLabelIfNotNil(_mtlRenderEncoder, getMTLRenderCommandEncoderName(cmdUse));
 
@@ -439,6 +460,18 @@
     _occlusionQueryState.beginMetalRenderPass();
 }
 
+// If custom sample positions have been set, return them, otherwise return an empty array.
+// For Metal, VkPhysicalDeviceSampleLocationsPropertiesEXT::variableSampleLocations is false.
+// As such, Vulkan requires that sample positions must be established at the beginning of
+// a renderpass, and that both pipeline and dynamic sample locations must be the same as those
+// set for each subpass. Therefore, the only sample positions of use are those set for each
+// subpass when the renderpass begins. The pipeline and dynamic sample positions are ignored.
+MVKArrayRef<MTLSamplePosition> MVKCommandEncoder::getCustomSamplePositions() {
+	return (_renderSubpassIndex < _subpassSamplePositions.size()
+			? _subpassSamplePositions[_renderSubpassIndex].contents()
+			: MVKArrayRef<MTLSamplePosition>());
+}
+
 void MVKCommandEncoder::encodeStoreActions(bool storeOverride) {
 	getSubpass()->encodeStoreActions(this,
 									 _isRenderingEntireAttachment,
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def b/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def
index d5785ff..6c995bb 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def
@@ -78,6 +78,7 @@
 MVK_CMD_TYPE_POOLS_FROM_5_THRESHOLDS(BeginRenderPass, 1, 2, 0, 1, 2)
 MVK_CMD_TYPE_POOL(NextSubpass)
 MVK_CMD_TYPE_POOL(EndRenderPass)
+MVK_CMD_TYPE_POOL(SetSampleLocations)
 MVK_CMD_TYPE_POOLS_FROM_THRESHOLD(ExecuteCommands, 1)
 MVK_CMD_TYPE_POOLS_FROM_2_THRESHOLDS(BindDescriptorSetsStatic, 1, 4)
 MVK_CMD_TYPE_POOLS_FROM_THRESHOLD(BindDescriptorSetsDynamic, 4)
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
index ceb76a5..3cbf9cb 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
@@ -128,6 +128,10 @@
 	/** Populates the specified structure with the format properties of this device. */
 	void getFormatProperties(VkFormat format, VkFormatProperties2* pFormatProperties);
 
+	/** Populates the specified structure with the multisample properties of this device. */
+	void getMultisampleProperties(VkSampleCountFlagBits samples,
+								  VkMultisamplePropertiesEXT* pMultisampleProperties);
+
 	/** Populates the image format properties supported on this device. */
     VkResult getImageFormatProperties(VkFormat format,
                                       VkImageType type,
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
index b22b8b8..17bc185 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
@@ -75,6 +75,9 @@
 static const uint32_t kAMDRadeonRX6800DeviceId = 0x73bf;
 static const uint32_t kAMDRadeonRX6700DeviceId = 0x73df;
 
+static const VkExtent2D kMetalSamplePositionGridSize = { 1, 1 };
+static const VkExtent2D kMetalSamplePositionGridSizeNotSupported = { 0, 0 };
+
 #pragma clang diagnostic pop
 
 
@@ -457,6 +460,16 @@
 				portabilityProps->minVertexInputBindingStrideAlignment = (uint32_t)_metalFeatures.vertexStrideAlignment;
 				break;
 			}
+			case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLE_LOCATIONS_PROPERTIES_EXT: {
+				auto* sampLocnProps = (VkPhysicalDeviceSampleLocationsPropertiesEXT*)next;
+				sampLocnProps->sampleLocationSampleCounts = _metalFeatures.supportedSampleCounts;
+				sampLocnProps->maxSampleLocationGridSize = kMetalSamplePositionGridSize;
+				sampLocnProps->sampleLocationCoordinateRange[0] = 0.0;
+				sampLocnProps->sampleLocationCoordinateRange[1] = (15.0 / 16.0);
+				sampLocnProps->sampleLocationSubPixelBits = 4;
+				sampLocnProps->variableSampleLocations = VK_FALSE;
+				break;
+			}
 			default:
 				break;
 		}
@@ -526,6 +539,15 @@
 	getFormatProperties(format, &pFormatProperties->formatProperties);
 }
 
+void MVKPhysicalDevice::getMultisampleProperties(VkSampleCountFlagBits samples,
+												 VkMultisamplePropertiesEXT* pMultisampleProperties) {
+	if (pMultisampleProperties) {
+		pMultisampleProperties->maxSampleLocationGridSize = (mvkIsOnlyAnyFlagEnabled(samples, _metalFeatures.supportedSampleCounts)
+															 ? kMetalSamplePositionGridSize
+															 : kMetalSamplePositionGridSizeNotSupported);
+	}
+}
+
 VkResult MVKPhysicalDevice::getImageFormatProperties(VkFormat format,
 													 VkImageType type,
 													 VkImageTiling tiling,
@@ -1519,9 +1541,12 @@
 
 #endif
 
-    // Note the selector name, which is different from the property name.
+	if ( [_mtlDevice respondsToSelector: @selector(areProgrammableSamplePositionsSupported)] ) {
+		_metalFeatures.programmableSamplePositions = _mtlDevice.areProgrammableSamplePositionsSupported;
+	}
+
     if ( [_mtlDevice respondsToSelector: @selector(areRasterOrderGroupsSupported)] ) {
-        _metalFeatures.rasterOrderGroups = _mtlDevice.rasterOrderGroupsSupported;
+        _metalFeatures.rasterOrderGroups = _mtlDevice.areRasterOrderGroupsSupported;
     }
 #if MVK_XCODE_12
 	if ( [_mtlDevice respondsToSelector: @selector(supportsPullModelInterpolation)] ) {
@@ -2738,6 +2763,9 @@
 	if (!_metalFeatures.samplerMirrorClampToEdge) {
 		pWritableExtns->vk_KHR_sampler_mirror_clamp_to_edge.enabled = false;
 	}
+	if (!_metalFeatures.programmableSamplePositions) {
+		pWritableExtns->vk_EXT_sample_locations.enabled = false;
+	}
 	if (!_metalFeatures.rasterOrderGroups) {
 		pWritableExtns->vk_EXT_fragment_shader_interlock.enabled = false;
 	}
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
index 414fb62..1313cd0 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
@@ -655,6 +655,8 @@
 	ADD_DVC_EXT_ENTRY_POINT(vkDestroyPrivateDataSlotEXT, EXT_PRIVATE_DATA);
 	ADD_DVC_EXT_ENTRY_POINT(vkGetPrivateDataEXT, EXT_PRIVATE_DATA);
 	ADD_DVC_EXT_ENTRY_POINT(vkSetPrivateDataEXT, EXT_PRIVATE_DATA);
+	ADD_DVC_EXT_ENTRY_POINT(vkGetPhysicalDeviceMultisamplePropertiesEXT, EXT_SAMPLE_LOCATIONS);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetSampleLocationsEXT, EXT_SAMPLE_LOCATIONS);
 	ADD_DVC_EXT_ENTRY_POINT(vkGetRefreshCycleDurationGOOGLE, GOOGLE_DISPLAY_TIMING);
 	ADD_DVC_EXT_ENTRY_POINT(vkGetPastPresentationTimingGOOGLE, GOOGLE_DISPLAY_TIMING);
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h
index a7f271d..21ffc44 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h
@@ -203,9 +203,6 @@
 	MVKBitArray stages[4] = {};
 };
 
-/** The number of dynamic states possible in Vulkan. */
-static const uint32_t kMVKVkDynamicStateCount = 32;
-
 /** Represents an Vulkan graphics pipeline. */
 class MVKGraphicsPipeline : public MVKPipeline {
 
@@ -259,6 +256,12 @@
 	/** Returns true if the tessellation control shader needs a buffer to store its per-patch output. */
 	bool needsTessCtlPatchOutputBuffer() { return _needsTessCtlPatchOutputBuffer; }
 
+	/** Returns whether this pipeline has custom sample positions enabled. */
+	bool isUsingCustomSamplePositions() { return _isUsingCustomSamplePositions; }
+
+	/** Returns the custom samples used by this pipeline. */
+	MVKArrayRef<MTLSamplePosition> getCustomSamplePositions() { return _customSamplePositions.contents(); }
+
 	/** Returns the Metal vertex buffer index to use for the specified vertex attribute binding number.  */
 	uint32_t getMetalBufferIndexForVertexAttributeBinding(uint32_t binding) { return _device->getMetalBufferIndexForVertexAttributeBinding(binding); }
 
@@ -287,6 +290,7 @@
 
     id<MTLRenderPipelineState> getOrCompilePipeline(MTLRenderPipelineDescriptor* plDesc, id<MTLRenderPipelineState>& plState);
     id<MTLComputePipelineState> getOrCompilePipeline(MTLComputePipelineDescriptor* plDesc, id<MTLComputePipelineState>& plState, const char* compilerType);
+	void initCustomSamplePositions(const VkGraphicsPipelineCreateInfo* pCreateInfo);
     void initMTLRenderPipelineState(const VkGraphicsPipelineCreateInfo* pCreateInfo, const SPIRVTessReflectionData& reflectData);
     void initShaderConversionConfig(SPIRVToMSLConversionConfiguration& shaderConfig, const VkGraphicsPipelineCreateInfo* pCreateInfo, const SPIRVTessReflectionData& reflectData);
     void addVertexInputToShaderConversionConfig(SPIRVToMSLConversionConfiguration& shaderConfig, const VkGraphicsPipelineCreateInfo* pCreateInfo);
@@ -323,6 +327,8 @@
 
 	MVKSmallVector<VkViewport, kMVKCachedViewportScissorCount> _viewports;
 	MVKSmallVector<VkRect2D, kMVKCachedViewportScissorCount> _scissors;
+	MVKSmallVector<VkDynamicState> _dynamicState;
+	MVKSmallVector<MTLSamplePosition> _customSamplePositions;
 	MVKSmallVector<MVKTranslatedVertexBinding> _translatedVertexBindings;
 	MVKSmallVector<MVKZeroDivisorVertexBinding> _zeroDivisorVertexBindings;
 	MVKSmallVector<MVKStagedMTLArgumentEncoders> _mtlArgumentEncoders;
@@ -350,7 +356,6 @@
 	uint32_t _tessCtlPatchOutputBufferIndex = 0;
 	uint32_t _tessCtlLevelBufferIndex = 0;
 
-	bool _dynamicStateEnabled[kMVKVkDynamicStateCount];
 	bool _needsVertexSwizzleBuffer = false;
 	bool _needsVertexBufferSizeBuffer = false;
 	bool _needsVertexDynamicOffsetBuffer = false;
@@ -372,6 +377,7 @@
 	bool _isRasterizing = false;
 	bool _isRasterizingColor = false;
 	bool _isRasterizingDepthStencil = false;
+	bool _isUsingCustomSamplePositions = false;
 };
 
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm
index bf545e3..2751bde 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm
@@ -309,17 +309,18 @@
 }
 
 bool MVKGraphicsPipeline::supportsDynamicState(VkDynamicState state) {
-
-    // First test if this dynamic state is explicitly turned off
-    if ( (state >= kMVKVkDynamicStateCount) || !_dynamicStateEnabled[state] ) { return false; }
-
-    // Some dynamic states have other restrictions
-    switch (state) {
-        case VK_DYNAMIC_STATE_DEPTH_BIAS:
-            return _rasterInfo.depthBiasEnable;
-        default:
-            return true;
-    }
+	for (auto& ds : _dynamicState) {
+		if (state == ds) {
+			// Some dynamic states have other restrictions
+			switch (state) {
+				case VK_DYNAMIC_STATE_DEPTH_BIAS:
+					return _rasterInfo.depthBiasEnable;
+				default:
+					return true;
+			}
+		}
+	}
+	return false;
 }
 
 static const char vtxCompilerType[] = "Vertex stage pipeline for tessellation";
@@ -408,13 +409,11 @@
 		}
 	}
 
-	// Track dynamic state in _dynamicStateEnabled array
-	mvkClear(_dynamicStateEnabled, kMVKVkDynamicStateCount);	// start with all dynamic state disabled
+	// Track dynamic state
 	const VkPipelineDynamicStateCreateInfo* pDS = pCreateInfo->pDynamicState;
 	if (pDS) {
 		for (uint32_t i = 0; i < pDS->dynamicStateCount; i++) {
-			VkDynamicState ds = pDS->pDynamicStates[i];
-			_dynamicStateEnabled[ds] = true;
+			_dynamicState.push_back(pDS->pDynamicStates[i]);
 		}
 	}
 
@@ -457,6 +456,9 @@
 		}
 	}
 
+	// Must run after _isRasterizing and _dynamicState are populated
+	initCustomSamplePositions(pCreateInfo);
+
 	// Render pipeline state
 	initMTLRenderPipelineState(pCreateInfo, reflectData);
 
@@ -472,7 +474,7 @@
 		for (uint32_t vpIdx = 0; vpIdx < vpCnt; vpIdx++) {
 			// If viewport is dyanamic, we still add a dummy so that the count will be tracked.
 			VkViewport vp;
-			if ( !_dynamicStateEnabled[VK_DYNAMIC_STATE_VIEWPORT] ) { vp = pVPState->pViewports[vpIdx]; }
+			if ( !supportsDynamicState(VK_DYNAMIC_STATE_VIEWPORT) ) { vp = pVPState->pViewports[vpIdx]; }
 			_viewports.push_back(vp);
 		}
 
@@ -481,7 +483,7 @@
 		for (uint32_t sIdx = 0; sIdx < sCnt; sIdx++) {
 			// If scissor is dyanamic, we still add a dummy so that the count will be tracked.
 			VkRect2D sc;
-			if ( !_dynamicStateEnabled[VK_DYNAMIC_STATE_SCISSOR] ) { sc = pVPState->pScissors[sIdx]; }
+			if ( !supportsDynamicState(VK_DYNAMIC_STATE_SCISSOR) ) { sc = pVPState->pScissors[sIdx]; }
 			_scissors.push_back(sc);
 		}
 	}
@@ -512,6 +514,31 @@
 	return plState;
 }
 
+// Must run after _isRasterizing and _dynamicState are populated
+void MVKGraphicsPipeline::initCustomSamplePositions(const VkGraphicsPipelineCreateInfo* pCreateInfo) {
+
+	// Must ignore allowed bad pMultisampleState pointer if rasterization disabled
+	if ( !(_isRasterizing && pCreateInfo->pMultisampleState) ) { return; }
+
+	for (const auto* next = (VkBaseInStructure*)pCreateInfo->pMultisampleState->pNext; next; next = next->pNext) {
+		switch (next->sType) {
+			case VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT: {
+				auto* pSampLocnsCreateInfo = (VkPipelineSampleLocationsStateCreateInfoEXT*)next;
+				_isUsingCustomSamplePositions = pSampLocnsCreateInfo->sampleLocationsEnable;
+				if (_isUsingCustomSamplePositions && !supportsDynamicState(VK_DYNAMIC_STATE_SAMPLE_LOCATIONS_EXT)) {
+					for (uint32_t slIdx = 0; slIdx < pSampLocnsCreateInfo->sampleLocationsInfo.sampleLocationsCount; slIdx++) {
+						auto& sl = pSampLocnsCreateInfo->sampleLocationsInfo.pSampleLocations[slIdx];
+						_customSamplePositions.push_back(MTLSamplePositionMake(sl.x, sl.y));
+					}
+				}
+				break;
+			}
+			default:
+				break;
+		}
+	}
+}
+
 // Constructs the underlying Metal render pipeline.
 void MVKGraphicsPipeline::initMTLRenderPipelineState(const VkGraphicsPipelineCreateInfo* pCreateInfo, const SPIRVTessReflectionData& reflectData) {
 	_mtlTessVertexStageState = nil;
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h
index 81f1b6e..2d1c698 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.h
@@ -261,8 +261,11 @@
     /** Returns the granularity of the render area of this instance.  */
     VkExtent2D getRenderAreaGranularity();
 
-	/** Returns the format of the color attachment at the specified index. */
-	MVKRenderSubpass* getSubpass(uint32_t subpassIndex);
+	/** Returns the number of subpasses. */
+	size_t getSubpassCount() { return _subpasses.size(); }
+
+	/** Returns the subpass at the specified index. */
+	MVKRenderSubpass* getSubpass(uint32_t subpassIndex) { return &_subpasses[subpassIndex]; }
 
 	/** Returns whether or not this render pass is a multiview render pass. */
 	bool isMultiview() const;
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm
index 6d9c04a..ee92899 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKRenderPass.mm
@@ -818,8 +818,6 @@
     return { 1, 1 };
 }
 
-MVKRenderSubpass* MVKRenderPass::getSubpass(uint32_t subpassIndex) { return &_subpasses[subpassIndex]; }
-
 bool MVKRenderPass::isMultiview() const { return _subpasses[0].isMultiview(); }
 
 MVKRenderPass::MVKRenderPass(MVKDevice* device,
diff --git a/MoltenVK/MoltenVK/Layers/MVKExtensions.def b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
index baf519f..0638098 100644
--- a/MoltenVK/MoltenVK/Layers/MVKExtensions.def
+++ b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
@@ -95,6 +95,7 @@
 MVK_EXTENSION(EXT_post_depth_coverage,             EXT_POST_DEPTH_COVERAGE,              DEVICE,   11.0,  11.0)
 MVK_EXTENSION(EXT_private_data,                    EXT_PRIVATE_DATA,                     DEVICE,   10.11,  8.0)
 MVK_EXTENSION(EXT_robustness2,                     EXT_ROBUSTNESS_2,                     DEVICE,   10.11,  8.0)
+MVK_EXTENSION(EXT_sample_locations,                EXT_SAMPLE_LOCATIONS,                 DEVICE,   10.13, 11.0)
 MVK_EXTENSION(EXT_scalar_block_layout,             EXT_SCALAR_BLOCK_LAYOUT,              DEVICE,   10.11,  8.0)
 MVK_EXTENSION(EXT_shader_stencil_export,           EXT_SHADER_STENCIL_EXPORT,            DEVICE,   10.14, 12.0)
 MVK_EXTENSION(EXT_shader_viewport_index_layer,     EXT_SHADER_VIEWPORT_INDEX_LAYER,      DEVICE,   10.11,  8.0)
diff --git a/MoltenVK/MoltenVK/Utility/MVKFoundation.h b/MoltenVK/MoltenVK/Utility/MVKFoundation.h
index 5e79965..30d0a4b 100644
--- a/MoltenVK/MoltenVK/Utility/MVKFoundation.h
+++ b/MoltenVK/MoltenVK/Utility/MVKFoundation.h
@@ -442,6 +442,11 @@
 	const Type* end() const { return &data[size]; }
 	const Type& operator[]( const size_t i ) const { return data[i]; }
 	Type& operator[]( const size_t i ) { return data[i]; }
+	MVKArrayRef<Type>& operator=(const MVKArrayRef<Type>& other) {
+		data = other.data;
+		*(size_t*)&size = other.size;
+		return *this;
+	}
 	MVKArrayRef() : MVKArrayRef(nullptr, 0) {}
 	MVKArrayRef(Type* d, size_t s) : data(d), size(s) {}
 };
diff --git a/MoltenVK/MoltenVK/Vulkan/vulkan.mm b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
index b266cf3..93d7c08 100644
--- a/MoltenVK/MoltenVK/Vulkan/vulkan.mm
+++ b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
@@ -3099,6 +3099,29 @@
 }
 
 #pragma mark -
+#pragma mark VK_EXT_sample_locations extension
+
+void vkGetPhysicalDeviceMultisamplePropertiesEXT(
+	VkPhysicalDevice                            physicalDevice,
+	VkSampleCountFlagBits                       samples,
+	VkMultisamplePropertiesEXT*                 pMultisampleProperties) {
+
+	MVKTraceVulkanCallStart();
+	MVKPhysicalDevice* mvkPD = MVKPhysicalDevice::getMVKPhysicalDevice(physicalDevice);
+	mvkPD->getMultisampleProperties(samples, pMultisampleProperties);
+	MVKTraceVulkanCallEnd();
+}
+
+void vkCmdSetSampleLocationsEXT(
+	VkCommandBuffer                             commandBuffer,
+	const VkSampleLocationsInfoEXT*             pSampleLocationsInfo) {
+
+	MVKTraceVulkanCallStart();
+	MVKAddCmd(SetSampleLocations, commandBuffer, pSampleLocationsInfo);
+	MVKTraceVulkanCallEnd();
+}
+
+#pragma mark -
 #pragma mark iOS & macOS surface extensions
 
 MVK_PUBLIC_VULKAN_SYMBOL VkResult vkCreate_PLATFORM_SurfaceMVK(