Add support for VK_EXT_extended_dynamic_state3 extension.

- Move patch point tracking from pipeline state to render state, and
  remove MVKPipelineCommandEncoderState subclasses no longer needed.
- Move sample location tracking from renderpass input to pipeline
  static or dynamic state tracking.
- Restart Metal render pass when sample locations change, and enable
  VkPhysicalDeviceSampleLocationsPropertiesEXT::variableSampleLocations.
- Fix regression that broke VK_POLYGON_MODE_LINE (unrelated).
- Fix regression in marking MVKRenderingCommandEncoderState
  dirty after vkCmdClearAttachments() (unrelated).
diff --git a/Docs/MoltenVK_Runtime_UserGuide.md b/Docs/MoltenVK_Runtime_UserGuide.md
index c2f5498..9b00360 100644
--- a/Docs/MoltenVK_Runtime_UserGuide.md
+++ b/Docs/MoltenVK_Runtime_UserGuide.md
@@ -375,6 +375,8 @@
   - *Requires Metal 3.1 for `VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE`.*
 - `VK_EXT_extended_dynamic_state2`
   - *Primitive restart is always enabled, as Metal does not support disabling it (`VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE_EXT`).*
+- `VK_EXT_extended_dynamic_state3`
+  - *Metal does not support `VK_POLYGON_MODE_POINT`*
 - `VK_EXT_external_memory_host`
 - `VK_EXT_fragment_shader_interlock`
   - *Requires Metal 2.0 and Raster Order Groups.*
diff --git a/Docs/Whats_New.md b/Docs/Whats_New.md
index f69d420..f7c4815 100644
--- a/Docs/Whats_New.md
+++ b/Docs/Whats_New.md
@@ -18,6 +18,10 @@
 
 Released TBD
 
+- Add support for extensions:
+	- `VK_EXT_extended_dynamic_state3` *(Metal does not support `VK_POLYGON_MODE_POINT`)*
+- Fix regression that broke `VK_POLYGON_MODE_LINE`.
+- Fix regression in marking rendering state dirty after `vkCmdClearAttachments()`.
 - Reduce disk space consumed after running `fetchDependencies` script by removing intermediate file caches.
 - Update to latest SPIRV-Cross:
   - MSL: Fix regression error in argument buffer runtime arrays.
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm b/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm
index a7930a4..87515ba 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdDraw.mm
@@ -144,10 +144,9 @@
 
 void MVKCmdDraw::encode(MVKCommandEncoder* cmdEncoder) {
 
-    if (_vertexCount == 0 || _instanceCount == 0) {
-        // Nothing to do.
-        return;
-    }
+	if (_vertexCount == 0 || _instanceCount == 0) { return; }	// Nothing to do.
+
+	cmdEncoder->restartMetalRenderPassIfNeeded();
 
 	auto* pipeline = cmdEncoder->_graphicsPipelineState.getGraphicsPipeline();
 
@@ -172,7 +171,7 @@
 	} tessParams;
     uint32_t outControlPointCount = 0;
     if (pipeline->isTessellationPipeline()) {
-        tessParams.inControlPointCount = cmdEncoder->_graphicsPipelineState.getPatchControlPoints();
+        tessParams.inControlPointCount = cmdEncoder->_renderingState.getPatchControlPoints();
         outControlPointCount = pipeline->getOutputControlPointCount();
         tessParams.patchCount = mvkCeilingDivide(_vertexCount, tessParams.inControlPointCount) * _instanceCount;
     }
@@ -369,10 +368,9 @@
 
 void MVKCmdDrawIndexed::encode(MVKCommandEncoder* cmdEncoder) {
 
-    if (_indexCount == 0 || _instanceCount == 0) {
-        // Nothing to do.
-        return;
-    }
+	if (_indexCount == 0 || _instanceCount == 0) { return; }	// Nothing to do.
+
+	cmdEncoder->restartMetalRenderPassIfNeeded();
 
 	auto* pipeline = cmdEncoder->_graphicsPipelineState.getGraphicsPipeline();
 
@@ -401,7 +399,7 @@
 	} tessParams;
     uint32_t outControlPointCount = 0;
     if (pipeline->isTessellationPipeline()) {
-        tessParams.inControlPointCount = cmdEncoder->_graphicsPipelineState.getPatchControlPoints();
+        tessParams.inControlPointCount = cmdEncoder->_renderingState.getPatchControlPoints();
         outControlPointCount = pipeline->getOutputControlPointCount();
         tessParams.patchCount = mvkCeilingDivide(_indexCount, tessParams.inControlPointCount) * _instanceCount;
     }
@@ -649,6 +647,8 @@
 
 void MVKCmdDrawIndirect::encode(MVKCommandEncoder* cmdEncoder) {
 
+	cmdEncoder->restartMetalRenderPassIfNeeded();
+
 	auto* pipeline = cmdEncoder->_graphicsPipelineState.getGraphicsPipeline();
 
 	// Metal doesn't support triangle fans, so encode it as indexed indirect triangles instead.
@@ -686,7 +686,7 @@
         // encoding and execution. So we don't know how big to make the buffers.
         // We must assume an arbitrarily large number of vertices may be submitted.
         // But not too many, or we'll exhaust available VRAM.
-        inControlPointCount = cmdEncoder->_graphicsPipelineState.getPatchControlPoints();
+        inControlPointCount = cmdEncoder->_renderingState.getPatchControlPoints();
         outControlPointCount = pipeline->getOutputControlPointCount();
         vertexCount = kMVKMaxDrawIndirectVertexCount;
         patchCount = mvkCeilingDivide(vertexCount, inControlPointCount);
@@ -990,6 +990,7 @@
 }
 
 void MVKCmdDrawIndexedIndirect::encode(MVKCommandEncoder* cmdEncoder) {
+	cmdEncoder->restartMetalRenderPassIfNeeded();
 	encode(cmdEncoder, cmdEncoder->_graphicsResourcesState._mtlIndexBufferBinding);
 }
 
@@ -1034,7 +1035,7 @@
         // encoding and execution. So we don't know how big to make the buffers.
         // We must assume an arbitrarily large number of vertices may be submitted.
         // But not too many, or we'll exhaust available VRAM.
-        inControlPointCount = cmdEncoder->_graphicsPipelineState.getPatchControlPoints();
+        inControlPointCount = cmdEncoder->_renderingState.getPatchControlPoints();
         outControlPointCount = pipeline->getOutputControlPointCount();
         vertexCount = kMVKMaxDrawIndirectVertexCount;
         patchCount = mvkCeilingDivide(vertexCount, inControlPointCount);
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h
index 7f1df4b..16e4863 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.h
@@ -46,7 +46,6 @@
 
 protected:
 
-	MVKSmallVector<MVKSmallVector<MTLSamplePosition>> _subpassSamplePositions;
 	MVKRenderPass* _renderPass;
 	MVKFramebuffer* _framebuffer;
 	VkRect2D _renderArea;
@@ -203,7 +202,26 @@
 protected:
 	MVKCommandTypePool<MVKCommand>* getTypePool(MVKCommandPool* cmdPool) override;
 
-	MVKSmallVector<MTLSamplePosition, 8> _samplePositions;
+	MVKSmallVector<VkSampleLocationEXT, kMVKMaxSampleCount> _sampleLocations;
+};
+
+
+#pragma mark -
+#pragma mark MVKCmdSetSampleLocationsEnable
+
+/** Vulkan command to dynamically enable custom sample locations. */
+class MVKCmdSetSampleLocationsEnable : public MVKCommand {
+
+public:
+	VkResult setContent(MVKCommandBuffer* cmdBuff,
+						VkBool32 sampleLocationsEnable);
+
+	void encode(MVKCommandEncoder* cmdEncoder) override;
+
+protected:
+	MVKCommandTypePool<MVKCommand>* getTypePool(MVKCommandPool* cmdPool) override;
+
+	VkBool32 _sampleLocationsEnable;
 };
 
 
@@ -367,6 +385,25 @@
 
 
 #pragma mark -
+#pragma mark MVKCmdSetDepthClipEnable
+
+/** Vulkan command to dynamically enable depth clip. */
+class MVKCmdSetDepthClipEnable : public MVKCommand {
+
+public:
+	VkResult setContent(MVKCommandBuffer* cmdBuff,
+						VkBool32 depthClipEnable);
+
+	void encode(MVKCommandEncoder* cmdEncoder) override;
+
+protected:
+	MVKCommandTypePool<MVKCommand>* getTypePool(MVKCommandPool* cmdPool) override;
+
+	VkBool32 _depthClipEnable;
+};
+
+
+#pragma mark -
 #pragma mark MVKCmdSetDepthCompareOp
 
 /** Vulkan command to dynamically set the depth compare operation. */
@@ -552,6 +589,25 @@
 
 
 #pragma mark -
+#pragma mark MVKCmdSetPolygonMode
+
+/** Vulkan command to dynamically set the polygon mode. */
+class MVKCmdSetPolygonMode : public MVKCommand {
+
+public:
+	VkResult setContent(MVKCommandBuffer* cmdBuff,
+						VkPolygonMode polygonMode);
+
+	void encode(MVKCommandEncoder* cmdEncoder) override;
+
+protected:
+	MVKCommandTypePool<MVKCommand>* getTypePool(MVKCommandPool* cmdPool) override;
+
+	VkPolygonMode _polygonMode;
+};
+
+
+#pragma mark -
 #pragma mark MVKCmdSetPrimitiveTopology
 
 /** Vulkan command to dynamically set the primitive topology. */
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm
index c4bb754..a2492ac 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdRendering.mm
@@ -36,30 +36,6 @@
 	_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;
-		}
-	}
 
 	cmdBuff->_currentSubpassInfo.beginRenderpass(_renderPass);
 
@@ -86,15 +62,6 @@
 
 template <size_t N_CV, size_t N_A>
 void MVKCmdBeginRenderPass<N_CV, N_A>::encode(MVKCommandEncoder* cmdEncoder) {
-
-	// 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,
@@ -102,7 +69,7 @@
 								_renderArea,
 								_clearValues.contents(),
 								_attachments.contents(),
-								MVKArrayRef(spSPRefs, spSPCnt));
+								kMVKCommandUseBeginRenderPass);
 }
 
 template class MVKCmdBeginRenderPass<1, 0>;
@@ -217,17 +184,29 @@
 
 VkResult MVKCmdSetSampleLocations::setContent(MVKCommandBuffer* cmdBuff,
 											  const VkSampleLocationsInfoEXT* pSampleLocationsInfo) {
-
+	_sampleLocations.clear();
 	for (uint32_t slIdx = 0; slIdx < pSampleLocationsInfo->sampleLocationsCount; slIdx++) {
-		auto& sl = pSampleLocationsInfo->pSampleLocations[slIdx];
-		_samplePositions.push_back(MTLSamplePositionMake(sl.x, sl.y));
+		_sampleLocations.push_back(pSampleLocationsInfo->pSampleLocations[slIdx]);
 	}
-
 	return VK_SUCCESS;
 }
 
 void MVKCmdSetSampleLocations::encode(MVKCommandEncoder* cmdEncoder) {
-	cmdEncoder->setDynamicSamplePositions(_samplePositions.contents());
+	cmdEncoder->_renderingState.setSampleLocations(_sampleLocations.contents(), true);
+}
+
+
+#pragma mark -
+#pragma mark MVKCmdSetSampleLocationsEnable
+
+VkResult MVKCmdSetSampleLocationsEnable::setContent(MVKCommandBuffer* cmdBuff,
+													VkBool32 sampleLocationsEnable) {
+	_sampleLocationsEnable = sampleLocationsEnable;
+	return VK_SUCCESS;
+}
+
+void MVKCmdSetSampleLocationsEnable::encode(MVKCommandEncoder* cmdEncoder) {
+	cmdEncoder->_renderingState.setSampleLocationsEnable(_sampleLocationsEnable, true);
 }
 
 
@@ -240,7 +219,7 @@
 										  uint32_t viewportCount,
 										  const VkViewport* pViewports) {
 	_firstViewport = firstViewport;
-	_viewports.clear();	// Clear for reuse
+	_viewports.clear();
 	_viewports.reserve(viewportCount);
 	for (uint32_t vpIdx = 0; vpIdx < viewportCount; vpIdx++) {
 		_viewports.push_back(pViewports[vpIdx]);
@@ -267,7 +246,7 @@
 										 uint32_t scissorCount,
 										 const VkRect2D* pScissors) {
 	_firstScissor = firstScissor;
-	_scissors.clear();	// Clear for reuse
+	_scissors.clear();
 	_scissors.reserve(scissorCount);
 	for (uint32_t sIdx = 0; sIdx < scissorCount; sIdx++) {
 		_scissors.push_back(pScissors[sIdx]);
@@ -363,6 +342,20 @@
 
 
 #pragma mark -
+#pragma mark MVKCmdSetDepthClipEnable
+
+VkResult MVKCmdSetDepthClipEnable::setContent(MVKCommandBuffer* cmdBuff,
+											  VkBool32 depthClipEnable) {
+	_depthClipEnable = depthClipEnable;
+	return VK_SUCCESS;
+}
+
+void MVKCmdSetDepthClipEnable::encode(MVKCommandEncoder* cmdEncoder) {
+	cmdEncoder->_renderingState.setDepthClipEnable(_depthClipEnable, true);
+}
+
+
+#pragma mark -
 #pragma mark MVKCmdSetDepthCompareOp
 
 VkResult MVKCmdSetDepthCompareOp::setContent(MVKCommandBuffer* cmdBuff,
@@ -501,7 +494,21 @@
 }
 
 void MVKCmdSetPatchControlPoints::encode(MVKCommandEncoder* cmdEncoder) {
-	cmdEncoder->_graphicsPipelineState.setPatchControlPoints(_patchControlPoints);
+	cmdEncoder->_renderingState.setPatchControlPoints(_patchControlPoints, true);
+}
+
+
+#pragma mark -
+#pragma mark MVKCmdSetPolygonMode
+
+VkResult MVKCmdSetPolygonMode::setContent(MVKCommandBuffer* cmdBuff,
+										  VkPolygonMode polygonMode) {
+	_polygonMode = polygonMode;
+	return VK_SUCCESS;
+}
+
+void MVKCmdSetPolygonMode::encode(MVKCommandEncoder* cmdEncoder) {
+	cmdEncoder->_renderingState.setPolygonMode(_polygonMode, true);
 }
 
 
diff --git a/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm b/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm
index 124859b..52dcb78 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCmdTransfer.mm
@@ -1506,8 +1506,10 @@
 
 	// Return to the previous rendering state on the next render activity
 	cmdEncoder->_graphicsPipelineState.markDirty();
+	cmdEncoder->_graphicsResourcesState.markDirty();
 	cmdEncoder->_depthStencilState.markDirty();
 	cmdEncoder->_renderingState.markDirty();
+	cmdEncoder->_occlusionQueryState.markDirty();
 }
 
 template <size_t N>
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
index 3958013..92d02e7 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.h
@@ -250,21 +250,23 @@
 						 const VkRect2D& renderArea,
 						 MVKArrayRef<VkClearValue> clearValues,
 						 MVKArrayRef<MVKImageView*> attachments,
-						 MVKArrayRef<MVKArrayRef<MTLSamplePosition>> subpassSamplePositions,
-						 MVKCommandUse cmdUse = kMVKCommandUseBeginRenderPass);
+						 MVKCommandUse cmdUse);
 
 	/** Begins the next render subpass. */
 	void beginNextSubpass(MVKCommand* subpassCmd, VkSubpassContents renderpassContents);
 
-	/** Sets the dynamic custom sample positions to use when rendering. */
-	void setDynamicSamplePositions(MVKArrayRef<MTLSamplePosition> dynamicSamplePositions);
-
 	/** Begins dynamic rendering. */
 	void beginRendering(MVKCommand* rendCmd, const VkRenderingInfo* pRenderingInfo);
 
 	/** Begins a Metal render pass for the current render subpass. */
 	void beginMetalRenderPass(MVKCommandUse cmdUse);
 
+	/** 
+	 * If a Metal render pass has started, and it needs to be restarted,
+	 * then end the existing Metal render pass, and start a new one.
+	 */
+	void restartMetalRenderPassIfNeeded();
+
 	/** If a render encoder is active, encodes store actions for all attachments to it. */
 	void encodeStoreActions(bool storeOverride = false);
 
@@ -435,13 +437,13 @@
 	id<MTLRenderCommandEncoder> _mtlRenderEncoder;
 
     /** Tracks the current graphics pipeline bound to the encoder. */
-    MVKGraphicsPipelineCommandEncoderState _graphicsPipelineState;
+	MVKPipelineCommandEncoderState _graphicsPipelineState;
 
 	/** Tracks the current graphics resources state of the encoder. */
 	MVKGraphicsResourcesCommandEncoderState _graphicsResourcesState;
 
     /** Tracks the current compute pipeline bound to the encoder. */
-    MVKComputePipelineCommandEncoderState _computePipelineState;
+	MVKPipelineCommandEncoderState _computePipelineState;
 
 	/** Tracks the current compute resources state of the encoder. */
 	MVKComputeResourcesCommandEncoderState _computeResourcesState;
@@ -452,6 +454,9 @@
 	/** Tracks the current rendering states of the encoder. */
 	MVKRenderingCommandEncoderState _renderingState;
 
+	/** Tracks the occlusion query state of the encoder. */
+	MVKOcclusionQueryCommandEncoderState _occlusionQueryState;
+
     /** The size of the threadgroup for the compute shader. */
     MTLSize _mtlThreadgroupSize;
 
@@ -479,7 +484,6 @@
 	void encodeGPUCounterSample(MVKGPUCounterQueryPool* mvkQryPool, uint32_t sampleIndex, MVKCounterSamplingFlags samplingPoints);
 	void encodeTimestampStageCounterSamples();
 	id<MTLFence> getStageCountersMTLFence();
-	MVKArrayRef<MTLSamplePosition> getCustomSamplePositions();
 	NSString* getMTLRenderCommandEncoderName(MVKCommandUse cmdUse);
 	template<typename T> void retainIfImmediatelyEncoding(T& mtlEnc);
 	template<typename T> void endMetalEncoding(T& mtlEnc);
@@ -495,8 +499,6 @@
 	MVKSmallVector<GPUCounterQuery, 16> _timestampStageCounterQueries;
 	MVKSmallVector<VkClearValue, kMVKDefaultAttachmentCount> _clearValues;
 	MVKSmallVector<MVKImageView*, kMVKDefaultAttachmentCount> _attachments;
-	MVKSmallVector<MTLSamplePosition> _dynamicSamplePositions;
-	MVKSmallVector<MVKSmallVector<MTLSamplePosition>> _subpassSamplePositions;
 	id<MTLComputeCommandEncoder> _mtlComputeEncoder;
 	id<MTLBlitCommandEncoder> _mtlBlitEncoder;
 	id<MTLFence> _stageCountersMTLFence;
@@ -505,7 +507,6 @@
 	MVKPushConstantsCommandEncoderState _tessEvalPushConstants;
 	MVKPushConstantsCommandEncoderState _fragmentPushConstants;
 	MVKPushConstantsCommandEncoderState _computePushConstants;
-    MVKOcclusionQueryCommandEncoderState _occlusionQueryState;
 	MVKPrefillMetalCommandBuffersStyle _prefillStyle;
 	VkSubpassContents _subpassContents;
 	uint32_t _renderSubpassIndex;
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
index 9575bb7..44f0204 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm
@@ -442,7 +442,6 @@
 					pRenderingInfo->renderArea,
 					MVKArrayRef(clearValues, attCnt),
 					MVKArrayRef(imageViews, attCnt),
-					MVKArrayRef<MVKArrayRef<MTLSamplePosition>>(),
 					kMVKCommandUseBeginRendering);
 
 	// If we've just created new transient objects, once retained by this encoder,
@@ -462,7 +461,6 @@
 										const VkRect2D& renderArea,
 										MVKArrayRef<VkClearValue> clearValues,
 										MVKArrayRef<MVKImageView*> attachments,
-										MVKArrayRef<MVKArrayRef<MTLSamplePosition>> subpassSamplePositions,
 										MVKCommandUse cmdUse) {
 	_pEncodingContext->setRenderingContext(renderPass, framebuffer);
 	_renderArea = renderArea;
@@ -471,13 +469,6 @@
 	_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, cmdUse);
 }
 
@@ -518,10 +509,6 @@
 	beginMetalRenderPass(kMVKCommandUseNextSubpass);
 }
 
-void MVKCommandEncoder::setDynamicSamplePositions(MVKArrayRef<MTLSamplePosition> dynamicSamplePositions) {
-	_dynamicSamplePositions.assign(dynamicSamplePositions.begin(), dynamicSamplePositions.end());
-}
-
 // Retain encoders when prefilling, because prefilling may span multiple autorelease pools.
 template<typename T>
 void MVKCommandEncoder::retainIfImmediatelyEncoding(T& mtlEnc) {
@@ -536,7 +523,6 @@
 	mtlEnc = nil;
 }
 
-
 // Creates _mtlRenderEncoder and marks cached render state as dirty so it will be set into the _mtlRenderEncoder.
 void MVKCommandEncoder::beginMetalRenderPass(MVKCommandUse cmdUse) {
 
@@ -592,8 +578,8 @@
 	// 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()];
+		auto sampPosns = _renderingState.getSamplePositions();
+		[mtlRPDesc setSamplePositions: sampPosns.data() count: sampPosns.size()];
 	}
 
     _mtlRenderEncoder = [_mtlCmdBuffer renderCommandEncoderWithDescriptor: mtlRPDesc];
@@ -616,16 +602,13 @@
     _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::restartMetalRenderPassIfNeeded() {
+	if ( !_mtlRenderEncoder ) { return; }
+
+	if (_renderingState.needsMetalRenderPassRestart()) {
+		encodeStoreActions(true);
+		beginMetalRenderPass(kMVKCommandUseRestartSubpass);
+	}
 }
 
 void MVKCommandEncoder::encodeStoreActions(bool storeOverride) {
@@ -1161,12 +1144,12 @@
 	_computeResourcesState(this),
 	_depthStencilState(this),
 	_renderingState(this),
+	_occlusionQueryState(this),
 	_vertexPushConstants(this, VK_SHADER_STAGE_VERTEX_BIT),
 	_tessCtlPushConstants(this, VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT),
 	_tessEvalPushConstants(this, VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT),
 	_fragmentPushConstants(this, VK_SHADER_STAGE_FRAGMENT_BIT),
 	_computePushConstants(this, VK_SHADER_STAGE_COMPUTE_BIT),
-	_occlusionQueryState(this),
 	_prefillStyle(prefillStyle){
 
 	_pDeviceFeatures = &_device->_enabledFeatures;
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
index 82ea4ea..4ac895d 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
@@ -106,8 +106,11 @@
 	virtual void encodeImpl(uint32_t stage) = 0;
 	MVKDevice* getDevice();
 	bool isDynamicState(MVKRenderStateType state);
+	template <typename T> T& getContent(T* iVarAry, bool isDynamic) {
+		return iVarAry[isDynamic ? StateScope::Dynamic : StateScope::Static];
+	}
 	template <typename T> T& getContent(T* iVarAry, MVKRenderStateType state) {
-		return iVarAry[isDynamicState(state) ? StateScope::Dynamic : StateScope::Static];
+		return getContent(iVarAry, isDynamicState(state));
 	}
 
     MVKCommandEncoder* _cmdEncoder;
@@ -123,9 +126,11 @@
 class MVKPipelineCommandEncoderState : public MVKCommandEncoderState {
 
 public:
-    virtual void bindPipeline(MVKPipeline* pipeline);
+	void bindPipeline(MVKPipeline* pipeline);
 
     MVKPipeline* getPipeline();
+	MVKGraphicsPipeline* getGraphicsPipeline() { return (MVKGraphicsPipeline*)getPipeline(); }
+	MVKComputePipeline* getComputePipeline() { return (MVKComputePipeline*)getPipeline(); }
 
     MVKPipelineCommandEncoderState(MVKCommandEncoder* cmdEncoder) : MVKCommandEncoderState(cmdEncoder) {}
 
@@ -137,42 +142,6 @@
 
 
 #pragma mark -
-#pragma mark MVKGraphicsPipelineCommandEncoderState
-
-/** Holds encoder state established by graphics pipeline commands. */
-class MVKGraphicsPipelineCommandEncoderState : public MVKPipelineCommandEncoderState {
-
-public:
-	void bindPipeline(MVKPipeline* pipeline) override;
-
-	MVKGraphicsPipeline* getGraphicsPipeline() { return (MVKGraphicsPipeline*)getPipeline(); }
-
-	void setPatchControlPoints(uint32_t patchControlPoints);
-	uint32_t getPatchControlPoints();
-
-	MVKGraphicsPipelineCommandEncoderState(MVKCommandEncoder* cmdEncoder) : MVKPipelineCommandEncoderState(cmdEncoder) {}
-
-protected:
-	uint32_t _patchControlPoints[StateScope::Count] = {};
-};
-
-
-#pragma mark -
-#pragma mark MVKComputePipelineCommandEncoderState
-
-/** Holds encoder state established by compute pipeline commands. */
-class MVKComputePipelineCommandEncoderState : public MVKPipelineCommandEncoderState {
-
-public:
-	MVKComputePipeline* getComputePipeline() { return (MVKComputePipeline*)getPipeline(); }
-
-	MVKComputePipelineCommandEncoderState(MVKCommandEncoder* cmdEncoder) : MVKPipelineCommandEncoderState(cmdEncoder) {}
-
-protected:
-};
-
-
-#pragma mark -
 #pragma mark MVKPushConstantsCommandEncoderState
 
 /** Holds encoder state established by push constant commands for a single shader stage. */
@@ -257,7 +226,7 @@
 					  VkStencilOp passOp, VkStencilOp depthFailOp, VkCompareOp compareOp);
 
 	MVKMTLDepthStencilDescriptorData _depthStencilData[StateScope::Count];
-	bool _depthTestEnabled[StateScope::Count];
+	bool _depthTestEnabled[StateScope::Count] = {};
 	bool _hasDepthAttachment = false;
 	bool _hasStencilAttachment = false;
 };
@@ -294,9 +263,6 @@
 
 	void setFrontFace(VkFrontFace frontFace, bool isDynamic);
 
-	void setPrimitiveTopology(VkPrimitiveTopology topology, bool isDynamic);
-	MTLPrimitiveType getPrimitiveType();
-
 	void setPolygonMode(VkPolygonMode polygonMode, bool isDynamic);
 
 	void setBlendConstants(float blendConstants[4], bool isDynamic);
@@ -316,13 +282,26 @@
 
 	void setRasterizerDiscardEnable(VkBool32 rasterizerDiscardEnable, bool isDynamic);
 
+	void setPrimitiveTopology(VkPrimitiveTopology topology, bool isDynamic);
+	MTLPrimitiveType getPrimitiveType();
+
+	void setPatchControlPoints(uint32_t patchControlPoints, bool isDynamic);
+	uint32_t getPatchControlPoints();
+
+	void setSampleLocationsEnable(VkBool32 sampleLocationsEnable, bool isDynamic);
+	void setSampleLocations(const MVKArrayRef<VkSampleLocationEXT> sampleLocations, bool isDynamic);
+	MVKArrayRef<MTLSamplePosition> getSamplePositions();
+
 	void beginMetalRenderPass() override;
+	bool needsMetalRenderPassRestart();
+
+	bool isDirty(MVKRenderStateType state);
+	void markDirty() override;
 
 	MVKRenderingCommandEncoderState(MVKCommandEncoder* cmdEncoder) : MVKCommandEncoderState(cmdEncoder) {}
 
 protected:
 	void encodeImpl(uint32_t stage) override;
-	bool isDirty(MVKRenderStateType state);
 	bool isDrawingTriangles();
 	template <typename T> void setContent(T* iVarAry, T* pVal, MVKRenderStateType state, bool isDynamic) {
 		auto* pIVar = &iVarAry[isDynamic ? StateScope::Dynamic : StateScope::Static];
@@ -330,10 +309,11 @@
 			*pIVar = *pVal;
 			_dirtyStates.enable(state);
 			_modifiedStates.enable(state);
-			markDirty();
+			MVKCommandEncoderState::markDirty();	// Avoid local markDirty() as it marks all states dirty.
 		}
 	}
 
+	MVKSmallVector<MTLSamplePosition, kMVKMaxSampleCount> _mtlSampleLocations[StateScope::Count] = {};
 	MVKMTLViewports _mtlViewports[StateScope::Count] = {};
 	MVKMTLScissors _mtlScissors[StateScope::Count] = {};
 	MVKColor32 _mtlBlendConstants[StateScope::Count] = {};
@@ -344,8 +324,10 @@
 	MTLPrimitiveType _mtlPrimitiveTopology[StateScope::Count] = { MTLPrimitiveTypePoint, MTLPrimitiveTypePoint };
 	MTLDepthClipMode _mtlDepthClipEnable[StateScope::Count] = { MTLDepthClipModeClip, MTLDepthClipModeClip };
 	MTLTriangleFillMode _mtlPolygonMode[StateScope::Count] = { MTLTriangleFillModeFill, MTLTriangleFillModeFill };
+	uint32_t _mtlPatchControlPoints[StateScope::Count] = {};
 	MVKRenderStateFlags _dirtyStates;
 	MVKRenderStateFlags _modifiedStates;
+	bool _mtlSampleLocationsEnable[StateScope::Count] = {};
 	bool _mtlDepthBiasEnable[StateScope::Count] = {};
 	bool _mtlPrimitiveRestartEnable[StateScope::Count] = {};
 	bool _mtlRasterizerDiscardEnable[StateScope::Count] = {};
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
index 9e17aa9..c7246a5 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
@@ -45,8 +45,11 @@
 #pragma mark MVKPipelineCommandEncoderState
 
 void MVKPipelineCommandEncoderState::bindPipeline(MVKPipeline* pipeline) {
-    if (pipeline != _pipeline) markDirty();
-    _pipeline = pipeline;
+	if (pipeline == _pipeline) { return; }
+
+	_pipeline = pipeline;
+	_pipeline->wasBound(_cmdEncoder);
+	markDirty();
 }
 
 MVKPipeline* MVKPipelineCommandEncoderState::getPipeline() { return _pipeline; }
@@ -60,23 +63,6 @@
 
 
 #pragma mark -
-#pragma mark MVKGraphicsPipelineCommandEncoderState
-
-void MVKGraphicsPipelineCommandEncoderState::bindPipeline(MVKPipeline* pipeline) {
-	MVKPipelineCommandEncoderState::bindPipeline(pipeline);
-	_patchControlPoints[StateScope::Static] = getGraphicsPipeline()->_tessInfo.patchControlPoints;
-}
-
-void MVKGraphicsPipelineCommandEncoderState::setPatchControlPoints(uint32_t patchControlPoints) {
-	_patchControlPoints[StateScope::Dynamic] = patchControlPoints;
-}
-
-uint32_t MVKGraphicsPipelineCommandEncoderState::getPatchControlPoints() {
-	return getContent(_patchControlPoints, PatchControlPoints);
-}
-
-
-#pragma mark -
 #pragma mark MVKPushConstantsCommandEncoderState
 
 void MVKPushConstantsCommandEncoderState:: setPushConstants(uint32_t offset, MVKArrayRef<char> pushConstants) {
@@ -310,60 +296,43 @@
 #pragma mark -
 #pragma mark MVKRenderingCommandEncoderState
 
-#define getContent(state)  getContent(_mtl##state, state)
-#define setContent(state)  setContent(_mtl##state, &mtl##state, state, isDynamic)
+#define getMTLContent(state)  getContent(_mtl##state, state)
+#define setMTLContent(state)  setContent(_mtl##state, &mtl##state, state, isDynamic)
 
 void MVKRenderingCommandEncoderState::setCullMode(VkCullModeFlags cullMode, bool isDynamic) {
 	auto mtlCullMode = mvkMTLCullModeFromVkCullModeFlags(cullMode);
-	setContent(CullMode);
+	setMTLContent(CullMode);
 	_cullBothFaces[isDynamic ? StateScope::Dynamic : StateScope::Static] = (cullMode == VK_CULL_MODE_FRONT_AND_BACK);
 }
 
 void MVKRenderingCommandEncoderState::setFrontFace(VkFrontFace frontFace, bool isDynamic) {
 	auto mtlFrontFace = mvkMTLWindingFromVkFrontFace(frontFace);
-	setContent(FrontFace);
-}
-
-void MVKRenderingCommandEncoderState::setPrimitiveTopology(VkPrimitiveTopology topology, bool isDynamic) {
-	auto mtlPrimitiveTopology = mvkMTLPrimitiveTypeFromVkPrimitiveTopology(topology);
-	setContent(PrimitiveTopology);
-}
-
-MTLPrimitiveType MVKRenderingCommandEncoderState::getPrimitiveType() {
-	return getContent(PrimitiveTopology);
-}
-
-bool MVKRenderingCommandEncoderState::isDrawingTriangles() {
-	switch (getPrimitiveType()) {
-		case MTLPrimitiveTypeTriangle:      return true;
-		case MTLPrimitiveTypeTriangleStrip: return true;
-		default:                            return false;
-	}
+	setMTLContent(FrontFace);
 }
 
 void MVKRenderingCommandEncoderState::setPolygonMode(VkPolygonMode polygonMode, bool isDynamic) {
 	auto mtlPolygonMode = mvkMTLTriangleFillModeFromVkPolygonMode(polygonMode);
-	setContent(PolygonMode);
+	setMTLContent(PolygonMode);
 }
 
 void MVKRenderingCommandEncoderState::setBlendConstants(float blendConstants[4], bool isDynamic) {
 	MVKColor32 mtlBlendConstants;
 	mvkCopy(mtlBlendConstants.float32, blendConstants, 4);
-	setContent(BlendConstants);
+	setMTLContent(BlendConstants);
 }
 
 void MVKRenderingCommandEncoderState::setDepthBias(const VkPipelineRasterizationStateCreateInfo& vkRasterInfo) {
 	bool isDynamic = false;
 
 	bool mtlDepthBiasEnable = static_cast<bool>(vkRasterInfo.depthBiasEnable);
-	setContent(DepthBiasEnable);
+	setMTLContent(DepthBiasEnable);
 
 	MVKDepthBias mtlDepthBias = {
 		.depthBiasConstantFactor = vkRasterInfo.depthBiasConstantFactor,
 		.depthBiasSlopeFactor = vkRasterInfo.depthBiasSlopeFactor,
 		.depthBiasClamp = vkRasterInfo.depthBiasClamp
 	};
-	setContent(DepthBias);
+	setMTLContent(DepthBias);
 }
 
 void MVKRenderingCommandEncoderState::setDepthBias(float depthBiasConstantFactor,
@@ -375,18 +344,18 @@
 		.depthBiasSlopeFactor = depthBiasSlopeFactor,
 		.depthBiasClamp = depthBiasClamp
 	};
-	setContent(DepthBias);
+	setMTLContent(DepthBias);
 }
 
 void MVKRenderingCommandEncoderState::setDepthBiasEnable(VkBool32 depthBiasEnable) {
 	bool isDynamic = true;
 	bool mtlDepthBiasEnable = static_cast<bool>(depthBiasEnable);
-	setContent(DepthBiasEnable);
+	setMTLContent(DepthBiasEnable);
 }
 
 void MVKRenderingCommandEncoderState::setDepthClipEnable(bool depthClip, bool isDynamic) {
 	auto mtlDepthClipEnable = depthClip ? MTLDepthClipModeClip : MTLDepthClipModeClamp;
-	setContent(DepthClipEnable);
+	setMTLContent(DepthClipEnable);
 }
 
 void MVKRenderingCommandEncoderState::setStencilReferenceValues(const VkPipelineDepthStencilStateCreateInfo& vkDepthStencilInfo) {
@@ -395,7 +364,7 @@
 		.frontFaceValue = vkDepthStencilInfo.front.reference,
 		.backFaceValue = vkDepthStencilInfo.back.reference
 	};
-	setContent(StencilReference);
+	setMTLContent(StencilReference);
 }
 
 void MVKRenderingCommandEncoderState::setStencilReferenceValues(VkStencilFaceFlags faceMask, uint32_t stencilReference) {
@@ -403,7 +372,7 @@
 	MVKStencilReference mtlStencilReference = _mtlStencilReference[StateScope::Dynamic];
 	if (shouldUpdateFace(FRONT)) { mtlStencilReference.frontFaceValue = stencilReference; }
 	if (shouldUpdateFace(BACK)) { mtlStencilReference.backFaceValue = stencilReference; }
-	setContent(StencilReference);
+	setMTLContent(StencilReference);
 }
 
 void MVKRenderingCommandEncoderState::setViewports(const MVKArrayRef<VkViewport> viewports,
@@ -418,7 +387,7 @@
 		mtlViewports.viewports[firstViewport + vpIdx] = mvkMTLViewportFromVkViewport(viewports[vpIdx]);
 		mtlViewports.viewportCount = max(mtlViewports.viewportCount, vpIdx + 1);
 	}
-	setContent(Viewports);
+	setMTLContent(Viewports);
 }
 
 void MVKRenderingCommandEncoderState::setScissors(const MVKArrayRef<VkRect2D> scissors,
@@ -433,17 +402,118 @@
 		mtlScissors.scissors[firstScissor + sIdx] = mvkMTLScissorRectFromVkRect2D(scissors[sIdx]);
 		mtlScissors.scissorCount = max(mtlScissors.scissorCount, sIdx + 1);
 	}
-	setContent(Scissors);
+	setMTLContent(Scissors);
 }
 
 void MVKRenderingCommandEncoderState::setPrimitiveRestartEnable(VkBool32 primitiveRestartEnable, bool isDynamic) {
 	bool mtlPrimitiveRestartEnable = static_cast<bool>(primitiveRestartEnable);
-	setContent(PrimitiveRestartEnable);
+	setMTLContent(PrimitiveRestartEnable);
 }
 
 void MVKRenderingCommandEncoderState::setRasterizerDiscardEnable(VkBool32 rasterizerDiscardEnable, bool isDynamic) {
 	bool mtlRasterizerDiscardEnable = static_cast<bool>(rasterizerDiscardEnable);
-	setContent(RasterizerDiscardEnable);
+	setMTLContent(RasterizerDiscardEnable);
+}
+
+// This value is retrieved, not encoded, so don't mark this encoder as dirty.
+void MVKRenderingCommandEncoderState::setPrimitiveTopology(VkPrimitiveTopology topology, bool isDynamic) {
+	getContent(_mtlPrimitiveTopology, isDynamic) = mvkMTLPrimitiveTypeFromVkPrimitiveTopology(topology);
+}
+
+MTLPrimitiveType MVKRenderingCommandEncoderState::getPrimitiveType() {
+	return getMTLContent(PrimitiveTopology);
+}
+
+bool MVKRenderingCommandEncoderState::isDrawingTriangles() {
+	switch (getPrimitiveType()) {
+		case MTLPrimitiveTypeTriangle:      return true;
+		case MTLPrimitiveTypeTriangleStrip: return true;
+		default:                            return false;
+	}
+}
+
+// This value is retrieved, not encoded, so don't mark this encoder as dirty.
+void MVKRenderingCommandEncoderState::setPatchControlPoints(uint32_t patchControlPoints, bool isDynamic) {
+	getContent(_mtlPatchControlPoints, isDynamic) = patchControlPoints;
+}
+
+uint32_t MVKRenderingCommandEncoderState::getPatchControlPoints() {
+	return getMTLContent(PatchControlPoints);
+}
+
+void MVKRenderingCommandEncoderState::setSampleLocationsEnable(VkBool32 sampleLocationsEnable, bool isDynamic) {
+	bool slEnbl = static_cast<bool>(sampleLocationsEnable);
+	auto& mtlSampLocEnbl = getContent(_mtlSampleLocationsEnable, isDynamic);
+
+	if (slEnbl == mtlSampLocEnbl) { return; }
+
+	mtlSampLocEnbl = slEnbl;
+
+	// This value is retrieved, not encoded, so don't mark this encoder as dirty.
+	_dirtyStates.enable(SampleLocationsEnable);
+}
+
+void MVKRenderingCommandEncoderState::setSampleLocations(MVKArrayRef<VkSampleLocationEXT> sampleLocations, bool isDynamic) {
+	auto& mtlSampPosns = getContent(_mtlSampleLocations, isDynamic);
+	size_t slCnt = sampleLocations.size();
+
+	// When comparing new vs current, make use of fact that MTLSamplePosition & VkSampleLocationEXT have same memory footprint.
+	if (slCnt == mtlSampPosns.size() &&
+		mvkAreEqual((MTLSamplePosition*)sampleLocations.data(),
+					mtlSampPosns.data(), slCnt)) {
+		return;
+	}
+
+	mtlSampPosns.clear();
+	for (uint32_t slIdx = 0; slIdx < slCnt; slIdx++) {
+		auto& sl = sampleLocations[slIdx];
+		mtlSampPosns.push_back(MTLSamplePositionMake(mvkClamp(sl.x, kMVKMinSampleLocationCoordinate, kMVKMaxSampleLocationCoordinate),
+													 mvkClamp(sl.y, kMVKMinSampleLocationCoordinate, kMVKMaxSampleLocationCoordinate)));
+	}
+
+	// This value is retrieved, not encoded, so don't mark this encoder as dirty.
+	_dirtyStates.enable(SampleLocations);
+}
+
+MVKArrayRef<MTLSamplePosition> MVKRenderingCommandEncoderState::getSamplePositions() {
+	return getMTLContent(SampleLocationsEnable) ? getMTLContent(SampleLocations).contents() : MVKArrayRef<MTLSamplePosition>();
+}
+
+// Return whether state is dirty, and mark it not dirty
+bool MVKRenderingCommandEncoderState::isDirty(MVKRenderStateType state) {
+	bool rslt = _dirtyStates.isEnabled(state);
+	_dirtyStates.disable(state);
+	return rslt;
+}
+
+// Don't force sample location & sample location enable to become dirty if they weren't already, because
+// this may cause needsMetalRenderPassRestart() to trigger an unnecessary Metal renderpass restart.
+void MVKRenderingCommandEncoderState::markDirty() {
+	MVKCommandEncoderState::markDirty();
+
+	bool wasSLDirty = _dirtyStates.isEnabled(SampleLocations);
+	bool wasSLEnblDirty = _dirtyStates.isEnabled(SampleLocationsEnable);
+	
+	_dirtyStates.enableAll();
+
+	_dirtyStates.set(SampleLocations, wasSLDirty);
+	_dirtyStates.set(SampleLocationsEnable, wasSLEnblDirty);
+}
+
+// Don't call parent beginMetalRenderPass() because it 
+// will call local markDirty() which is too aggressive.
+void MVKRenderingCommandEncoderState::beginMetalRenderPass() {
+	if (_isModified) {
+		_dirtyStates = _modifiedStates;
+		MVKCommandEncoderState::markDirty();
+	}
+}
+
+// Don't use || on isDirty calls, to ensure they both get called, so that the dirty flag of each will be cleared.
+bool MVKRenderingCommandEncoderState::needsMetalRenderPassRestart() {
+	bool isSLDirty = isDirty(SampleLocations);
+	bool isSLEnblDirty = isDirty(SampleLocationsEnable);
+	return isSLDirty || isSLEnblDirty;
 }
 
 #pragma mark Encoding
@@ -453,15 +523,16 @@
 
 	auto& rendEnc = _cmdEncoder->_mtlRenderEncoder;
 
-	if (isDirty(CullMode)) { [rendEnc setCullMode: getContent(CullMode)]; }
-	if (isDirty(FrontFace)) { [rendEnc setFrontFacingWinding: getContent(FrontFace)]; }
+	if (isDirty(PolygonMode)) { [rendEnc setTriangleFillMode: getMTLContent(PolygonMode)]; }
+	if (isDirty(CullMode)) { [rendEnc setCullMode: getMTLContent(CullMode)]; }
+	if (isDirty(FrontFace)) { [rendEnc setFrontFacingWinding: getMTLContent(FrontFace)]; }
 	if (isDirty(BlendConstants)) {
-		auto& bcFlt = getContent(BlendConstants).float32;
+		auto& bcFlt = getMTLContent(BlendConstants).float32;
 		[rendEnc setBlendColorRed: bcFlt[0] green: bcFlt[1] blue: bcFlt[2] alpha: bcFlt[3]];
 	}
 	if (isDirty(DepthBiasEnable) || isDirty(DepthBias)) {
-		if (getContent(DepthBiasEnable)) {
-			auto& db = getContent(DepthBias);
+		if (getMTLContent(DepthBiasEnable)) {
+			auto& db = getMTLContent(DepthBias);
 			[rendEnc setDepthBias: db.depthBiasConstantFactor
 					   slopeScale: db.depthBiasSlopeFactor
 							clamp: db.depthBiasClamp];
@@ -470,11 +541,11 @@
 		}
 	}
 	if (isDirty(DepthClipEnable) && _cmdEncoder->_pDeviceFeatures->depthClamp) {
-		[rendEnc setDepthClipMode: getContent(DepthClipEnable)];
+		[rendEnc setDepthClipMode: getMTLContent(DepthClipEnable)];
 	}
 
 	if (isDirty(StencilReference)) {
-		auto& sr = getContent(StencilReference);
+		auto& sr = getMTLContent(StencilReference);
 		[rendEnc setStencilFrontReferenceValue: sr.frontFaceValue backReferenceValue: sr.backFaceValue];
 	}
 
@@ -484,13 +555,13 @@
 	// to use primitive restart at all, and is just setting this as a "just-in-case",
 	// and forcing an error here would be unexpected to the app (including CTS).
 	auto mtlPrimType = getPrimitiveType();
-	if (isDirty(PrimitiveRestartEnable) && !getContent(PrimitiveRestartEnable) &&
+	if (isDirty(PrimitiveRestartEnable) && !getMTLContent(PrimitiveRestartEnable) &&
 		(mtlPrimType == MTLPrimitiveTypeTriangleStrip || mtlPrimType == MTLPrimitiveTypeLineStrip)) {
 		reportWarning(VK_ERROR_FEATURE_NOT_PRESENT, "Metal does not support disabling primitive restart.");
 	}
 
 	if (isDirty(Viewports)) {
-		auto& mtlViewports = getContent(Viewports);
+		auto& mtlViewports = getMTLContent(Viewports);
 		if (_cmdEncoder->_pDeviceFeatures->multiViewport) {
 #if MVK_MACOS_OR_IOS
 			[rendEnc setViewports: mtlViewports.viewports count: mtlViewports.viewportCount];
@@ -504,7 +575,7 @@
 	// set to front-and-back, emulate this by using zeroed scissor rectangles.
 	if (isDirty(Scissors)) {
 		static MTLScissorRect zeroRect = {};
-		auto mtlScissors = getContent(Scissors);
+		auto mtlScissors = getMTLContent(Scissors);
 		bool shouldDiscard = ((_mtlRasterizerDiscardEnable[StateScope::Dynamic] && isDynamicState(RasterizerDiscardEnable)) ||
 							  (isDrawingTriangles() && _cullBothFaces[StateScope::Dynamic] && isDynamicState(CullMode)));
 		for (uint32_t sIdx = 0; sIdx < mtlScissors.scissorCount; sIdx++) {
@@ -521,17 +592,8 @@
 	}
 }
 
-// Return whether state is dirty, and mark it not dirty
-bool MVKRenderingCommandEncoderState::isDirty(MVKRenderStateType state) {
-	bool rslt = _dirtyStates.isEnabled(state);
-	_dirtyStates.disable(state);
-	return rslt;
-}
-
-void MVKRenderingCommandEncoderState::beginMetalRenderPass() {
-	MVKCommandEncoderState::beginMetalRenderPass();
-	_dirtyStates = _modifiedStates;
-}
+#undef getMTLContent
+#undef setMTLContent
 
 
 #pragma mark -
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def b/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def
index 65683f8..6703a0b 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandTypePools.def
@@ -81,6 +81,7 @@
 MVK_CMD_TYPE_POOLS_FROM_3_THRESHOLDS(BeginRendering, 1, 2, 4)
 MVK_CMD_TYPE_POOL(EndRendering)
 MVK_CMD_TYPE_POOL(SetSampleLocations)
+MVK_CMD_TYPE_POOL(SetSampleLocationsEnable)
 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)
@@ -91,6 +92,7 @@
 MVK_CMD_TYPE_POOL(SetDepthBiasEnable)
 MVK_CMD_TYPE_POOL(SetDepthTestEnable)
 MVK_CMD_TYPE_POOL(SetDepthWriteEnable)
+MVK_CMD_TYPE_POOL(SetDepthClipEnable)
 MVK_CMD_TYPE_POOL(SetDepthCompareOp)
 MVK_CMD_TYPE_POOL(SetStencilTestEnable)
 MVK_CMD_TYPE_POOL(SetStencilOp)
@@ -100,8 +102,9 @@
 MVK_CMD_TYPE_POOL(SetCullMode)
 MVK_CMD_TYPE_POOL(SetFrontFace)
 MVK_CMD_TYPE_POOL(SetPrimitiveTopology)
-MVK_CMD_TYPE_POOL(SetPatchControlPoints)
 MVK_CMD_TYPE_POOL(SetPrimitiveRestartEnable)
+MVK_CMD_TYPE_POOL(SetPolygonMode)
+MVK_CMD_TYPE_POOL(SetPatchControlPoints)
 MVK_CMD_TYPE_POOL(SetRasterizerDiscardEnable)
 MVK_CMD_TYPE_POOLS_FROM_2_THRESHOLDS(BindVertexBuffers, 1, 2)
 MVK_CMD_TYPE_POOL(BindIndexBuffer)
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
index 125bf9a..be9c08f 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
@@ -73,16 +73,22 @@
 
 
 /** The buffer index to use for vertex content. */
-const static uint32_t kMVKVertexContentBufferIndex = 0;
+static constexpr uint32_t kMVKVertexContentBufferIndex = 0;
 
 // Parameters to define the sizing of inline collections
-const static uint32_t kMVKQueueFamilyCount = 4;
-const static uint32_t kMVKQueueCountPerQueueFamily = 1;		// Must be 1. See comments in MVKPhysicalDevice::getQueueFamilies()
-const static uint32_t kMVKMinSwapchainImageCount = 2;
-const static uint32_t kMVKMaxSwapchainImageCount = 3;
-const static uint32_t kMVKMaxColorAttachmentCount = 8;
-const static uint32_t kMVKMaxViewportScissorCount = 16;
-const static uint32_t kMVKMaxDescriptorSetCount = SPIRV_CROSS_NAMESPACE::kMaxArgumentBuffers;
+static constexpr uint32_t   kMVKQueueFamilyCount = 4;
+static constexpr uint32_t   kMVKQueueCountPerQueueFamily = 1;		// Must be 1. See comments in MVKPhysicalDevice::getQueueFamilies()
+static constexpr uint32_t   kMVKMinSwapchainImageCount = 2;
+static constexpr uint32_t   kMVKMaxSwapchainImageCount = 3;
+static constexpr uint32_t   kMVKMaxColorAttachmentCount = 8;
+static constexpr uint32_t   kMVKMaxViewportScissorCount = 16;
+static constexpr uint32_t   kMVKMaxDescriptorSetCount = SPIRV_CROSS_NAMESPACE::kMaxArgumentBuffers;
+static constexpr uint32_t   kMVKMaxSampleCount = 8;
+static constexpr uint32_t   kMVKSampleLocationCoordinateGridSize = 16;
+static constexpr float      kMVKMinSampleLocationCoordinate = 0.0;
+static constexpr float      kMVKMaxSampleLocationCoordinate = (float)(kMVKSampleLocationCoordinateGridSize - 1) / (float)kMVKSampleLocationCoordinateGridSize;
+static constexpr VkExtent2D kMVKSampleLocationPixelGridSize = { 1, 1 };
+static constexpr VkExtent2D kMVKSampleLocationPixelGridSizeNotSupported = { 0, 0 };
 
 #if !MVK_XCODE_12
 typedef NSUInteger MTLTimestamp;
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
index 99ac0ef..0dd865f 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
@@ -75,9 +75,6 @@
 static const uint32_t kAMDRadeonRX6800DeviceId = 0x73bf;
 static const uint32_t kAMDRadeonRX6700DeviceId = 0x73df;
 
-static const VkExtent2D kMetalSamplePositionGridSize = { 1, 1 };
-static const VkExtent2D kMetalSamplePositionGridSizeNotSupported = { 0, 0 };
-
 static const uint32_t kMaxTimeDomains = 2;
 
 #pragma clang diagnostic pop
@@ -399,6 +396,41 @@
 				extDynState2->extendedDynamicState2PatchControlPoints = true;
 				break;
 			}
+			case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_3_FEATURES_EXT: {
+				auto* extDynState3 = (VkPhysicalDeviceExtendedDynamicState3FeaturesEXT*)next;
+				extDynState3->extendedDynamicState3TessellationDomainOrigin = false;
+				extDynState3->extendedDynamicState3DepthClampEnable = true;
+				extDynState3->extendedDynamicState3PolygonMode = true;
+				extDynState3->extendedDynamicState3RasterizationSamples = false;
+				extDynState3->extendedDynamicState3SampleMask = false;
+				extDynState3->extendedDynamicState3AlphaToCoverageEnable = false;
+				extDynState3->extendedDynamicState3AlphaToOneEnable = false;
+				extDynState3->extendedDynamicState3LogicOpEnable = false;
+				extDynState3->extendedDynamicState3ColorBlendEnable = false;
+				extDynState3->extendedDynamicState3ColorBlendEquation = false;
+				extDynState3->extendedDynamicState3ColorWriteMask = false;
+				extDynState3->extendedDynamicState3RasterizationStream = false;
+				extDynState3->extendedDynamicState3ConservativeRasterizationMode = false;
+				extDynState3->extendedDynamicState3ExtraPrimitiveOverestimationSize = false;
+				extDynState3->extendedDynamicState3DepthClipEnable = true;
+				extDynState3->extendedDynamicState3SampleLocationsEnable = true;
+				extDynState3->extendedDynamicState3ColorBlendAdvanced = false;
+				extDynState3->extendedDynamicState3ProvokingVertexMode = false;
+				extDynState3->extendedDynamicState3LineRasterizationMode = false;
+				extDynState3->extendedDynamicState3LineStippleEnable = false;
+				extDynState3->extendedDynamicState3DepthClipNegativeOneToOne = false;
+				extDynState3->extendedDynamicState3ViewportWScalingEnable = false;
+				extDynState3->extendedDynamicState3ViewportSwizzle = false;
+				extDynState3->extendedDynamicState3CoverageToColorEnable = false;
+				extDynState3->extendedDynamicState3CoverageToColorLocation = false;
+				extDynState3->extendedDynamicState3CoverageModulationMode = false;
+				extDynState3->extendedDynamicState3CoverageModulationTableEnable = false;
+				extDynState3->extendedDynamicState3CoverageModulationTable = false;
+				extDynState3->extendedDynamicState3CoverageReductionMode = false;
+				extDynState3->extendedDynamicState3RepresentativeFragmentTestEnable = false;
+				extDynState3->extendedDynamicState3ShadingRateImageEnable = false;
+				break;
+			}
 			case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT: {
 				auto* interlockFeatures = (VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT*)next;
 				interlockFeatures->fragmentShaderSampleInterlock = _metalFeatures.rasterOrderGroups;
@@ -747,11 +779,11 @@
 			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;
+				sampLocnProps->maxSampleLocationGridSize = kMVKSampleLocationPixelGridSize;
+				sampLocnProps->sampleLocationCoordinateRange[0] = kMVKMinSampleLocationCoordinate;
+				sampLocnProps->sampleLocationCoordinateRange[1] = kMVKMaxSampleLocationCoordinate;
+				sampLocnProps->sampleLocationSubPixelBits = mvkPowerOfTwoExponent(kMVKSampleLocationCoordinateGridSize);
+				sampLocnProps->variableSampleLocations = true;
 				break;
 			}
 			case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VERTEX_ATTRIBUTE_DIVISOR_PROPERTIES_EXT: {
@@ -860,8 +892,8 @@
 												 VkMultisamplePropertiesEXT* pMultisampleProperties) {
 	if (pMultisampleProperties) {
 		pMultisampleProperties->maxSampleLocationGridSize = (mvkIsOnlyAnyFlagEnabled(samples, _metalFeatures.supportedSampleCounts)
-															 ? kMetalSamplePositionGridSize
-															 : kMetalSamplePositionGridSizeNotSupported);
+															 ? kMVKSampleLocationPixelGridSize
+															 : kMVKSampleLocationPixelGridSizeNotSupported);
 	}
 }
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def
index b979283..d3856e8 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceFeatureStructs.def
@@ -66,6 +66,7 @@
 MVK_DEVICE_FEATURE_EXTN(4444Formats,                  4444_FORMATS,                    EXT,   2)
 MVK_DEVICE_FEATURE_EXTN(ExtendedDynamicState,         EXTENDED_DYNAMIC_STATE,          EXT,   1)
 MVK_DEVICE_FEATURE_EXTN(ExtendedDynamicState2,        EXTENDED_DYNAMIC_STATE_2,        EXT,   3)
+MVK_DEVICE_FEATURE_EXTN(ExtendedDynamicState3,        EXTENDED_DYNAMIC_STATE_3,        EXT,  31)
 MVK_DEVICE_FEATURE_EXTN(FragmentShaderInterlock,      FRAGMENT_SHADER_INTERLOCK,       EXT,   3)
 MVK_DEVICE_FEATURE_EXTN(PipelineCreationCacheControl, PIPELINE_CREATION_CACHE_CONTROL, EXT,   1)
 MVK_DEVICE_FEATURE_EXTN(Robustness2,                  ROBUSTNESS_2,                    EXT,   3)
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
index 45068c1..ea81520 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
@@ -700,7 +700,27 @@
 	ADD_DVC_EXT_ENTRY_POINT(vkGetPastPresentationTimingGOOGLE, GOOGLE_DISPLAY_TIMING);
 	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetLogicOpEXT, EXT_EXTENDED_DYNAMIC_STATE_2);
 	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetPatchControlPointsEXT, EXT_EXTENDED_DYNAMIC_STATE_2);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetAlphaToCoverageEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetAlphaToOneEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetColorBlendAdvancedEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetColorBlendEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetColorBlendEquationEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetColorWriteMaskEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetConservativeRasterizationModeEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetDepthClampEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetDepthClipEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetDepthClipNegativeOneToOneEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetExtraPrimitiveOverestimationSizeEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetLineRasterizationModeEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetLineStippleEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
 	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetLogicOpEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetPolygonModeEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetProvokingVertexModeEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetRasterizationSamplesEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetRasterizationStreamEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetSampleLocationsEnableEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetSampleMaskEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
+	ADD_DVC_EXT_ENTRY_POINT(vkCmdSetTessellationDomainOriginEXT, EXT_EXTENDED_DYNAMIC_STATE_3);
 }
 
 void MVKInstance::logVersions() {
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h
index 062b646..6827b5b 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.h
@@ -132,7 +132,10 @@
 	/** Returns the debug report object type of this object. */
 	VkDebugReportObjectTypeEXT getVkDebugReportObjectType() override { return VK_DEBUG_REPORT_OBJECT_TYPE_PIPELINE_EXT; }
 
-	/** Binds this pipeline to the specified command encoder. */
+	/** Called when the pipeline has been bound to the command encoder. */
+	virtual void wasBound(MVKCommandEncoder* cmdEncoder) {}
+
+	/** Encodes this pipeline to the command encoder. */
 	virtual void encode(MVKCommandEncoder* cmdEncoder, uint32_t stage = 0) = 0;
 
 	/** Binds the push constants to a command encoder. */
@@ -241,6 +244,7 @@
 	PrimitiveTopology,
 	RasterizerDiscardEnable,
 	SampleLocations,
+	SampleLocationsEnable,
 	Scissors,
 	StencilCompareMask,
 	StencilOp,
@@ -249,17 +253,22 @@
 	StencilWriteMask,
 	VertexStride,
 	Viewports,
+	MVKRenderStateTypeCount
 };
 
 /** Boolean tracking of rendering state. */
 struct MVKRenderStateFlags {
 	void enable(MVKRenderStateType rs) { if (rs) { mvkEnableFlags(_stateFlags, getFlagMask(rs)); } }
 	void disable(MVKRenderStateType rs) { if (rs) { mvkDisableFlags(_stateFlags, getFlagMask(rs)); } }
+	void set(MVKRenderStateType rs, bool val) { val? enable(rs) : disable(rs); }
+	void enableAll() { mvkEnableAllFlags(_stateFlags); }
+	void disableAll() { mvkDisableAllFlags(_stateFlags); }
 	bool isEnabled(MVKRenderStateType rs) { return mvkIsAnyFlagEnabled(_stateFlags, getFlagMask(rs)); }
 protected:
 	uint32_t getFlagMask(MVKRenderStateType rs) { return rs ? (1u << (rs - 1u)) : 0; }	 // Ignore Unknown type
 	
 	uint32_t _stateFlags = 0;
+	static_assert(sizeof(_stateFlags) * 8 >= MVKRenderStateTypeCount - 1, "_stateFlags is too small to support the number of flags in MVKRenderStateType."); // Ignore Unknown type
 };
 
 /** Represents an Vulkan graphics pipeline. */
@@ -270,7 +279,8 @@
 	/** Returns the number and order of stages in this pipeline. Draws commands must encode this pipeline once per stage. */
 	void getStages(MVKPiplineStages& stages);
 
-	/** Binds this pipeline to the specified command encoder. */
+	virtual void wasBound(MVKCommandEncoder* cmdEncoder) override;
+
 	void encode(MVKCommandEncoder* cmdEncoder, uint32_t stage = 0) override;
 
     /** Returns whether this pipeline permits dynamic setting of the state. */
@@ -312,9 +322,6 @@
 	/** 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 Vulkan primitive topology. */
 	VkPrimitiveTopology getVkPrimitiveTopology() { return _vkPrimitiveTopology; }
 
@@ -327,9 +334,6 @@
 	 */
 	bool isValidVertexBufferIndex(MVKShaderStage stage, uint32_t mtlBufferIndex);
 
-	/** 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); }
 
@@ -354,8 +358,6 @@
 	~MVKGraphicsPipeline() override;
 
 protected:
-	friend class MVKGraphicsPipelineCommandEncoderState;
-
 	typedef MVKSmallVector<SPIRVShaderInterfaceVariable, 32> SPIRVShaderOutputs;
 	typedef MVKSmallVector<SPIRVShaderInterfaceVariable, 32> SPIRVShaderInputs;
 
@@ -364,7 +366,7 @@
 	bool compileTessVertexStageState(MTLComputePipelineDescriptor* vtxPLDesc, MVKMTLFunction* pVtxFunctions, VkPipelineCreationFeedback* pVertexFB);
 	bool compileTessControlStageState(MTLComputePipelineDescriptor* tcPLDesc, VkPipelineCreationFeedback* pTessCtlFB);
 	void initDynamicState(const VkGraphicsPipelineCreateInfo* pCreateInfo);
-	void initCustomSamplePositions(const VkGraphicsPipelineCreateInfo* pCreateInfo);
+	void initSampleLocations(const VkGraphicsPipelineCreateInfo* pCreateInfo);
     void initMTLRenderPipelineState(const VkGraphicsPipelineCreateInfo* pCreateInfo, const SPIRVTessReflectionData& reflectData, VkPipelineCreationFeedback* pPipelineFB, const VkPipelineShaderStageCreateInfo* pVertexSS, VkPipelineCreationFeedback* pVertexFB, const VkPipelineShaderStageCreateInfo* pTessCtlSS, VkPipelineCreationFeedback* pTessCtlFB, const VkPipelineShaderStageCreateInfo* pTessEvalSS, VkPipelineCreationFeedback* pTessEvalFB, const VkPipelineShaderStageCreateInfo* pFragmentSS, VkPipelineCreationFeedback* pFragmentFB);
     void initShaderConversionConfig(SPIRVToMSLConversionConfiguration& shaderConfig, const VkGraphicsPipelineCreateInfo* pCreateInfo, const SPIRVTessReflectionData& reflectData);
 	void initReservedVertexAttributeBufferCount(const VkGraphicsPipelineCreateInfo* pCreateInfo);
@@ -404,7 +406,7 @@
 
 	MVKSmallVector<VkViewport, kMVKMaxViewportScissorCount> _viewports;
 	MVKSmallVector<VkRect2D, kMVKMaxViewportScissorCount> _scissors;
-	MVKSmallVector<MTLSamplePosition> _customSamplePositions;
+	MVKSmallVector<VkSampleLocationEXT> _sampleLocations;
 	MVKSmallVector<MVKTranslatedVertexBinding> _translatedVertexBindings;
 	MVKSmallVector<MVKZeroDivisorVertexBinding> _zeroDivisorVertexBindings;
 	MVKSmallVector<MVKStagedMTLArgumentEncoders> _mtlArgumentEncoders;
@@ -449,7 +451,7 @@
 	bool _needsFragmentViewRangeBuffer = false;
 	bool _isRasterizing = false;
 	bool _isRasterizingColor = false;
-	bool _isUsingCustomSamplePositions = false;
+	bool _sampleLocationsEnable = false;
 };
 
 
@@ -461,7 +463,6 @@
 
 public:
 
-	/** Binds this pipeline to the specified command encoder. */
 	void encode(MVKCommandEncoder* cmdEncoder, uint32_t = 0) override;
 
 	/** Returns if this pipeline allows non-zero dispatch bases in vkCmdDispatchBase(). */
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm
index 7cb4a3c..0b26634 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKPipeline.mm
@@ -229,6 +229,13 @@
 #pragma mark -
 #pragma mark MVKGraphicsPipeline
 
+// Set retrieve-only rendering state when pipeline is bound, as it's too late at draw command.
+void MVKGraphicsPipeline::wasBound(MVKCommandEncoder* cmdEncoder) {
+	cmdEncoder->_renderingState.setPatchControlPoints(_tessInfo.patchControlPoints, false);
+	cmdEncoder->_renderingState.setSampleLocations(_sampleLocations.contents(), false);
+	cmdEncoder->_renderingState.setSampleLocationsEnable(_sampleLocationsEnable, false);
+}
+
 void MVKGraphicsPipeline::getStages(MVKPiplineStages& stages) {
     if (isTessellationPipeline()) {
         stages.push_back(kMVKGraphicsStageVertex);
@@ -514,7 +521,7 @@
 	_hasRasterInfo = mvkSetOrClear(&_rasterInfo, pCreateInfo->pRasterizationState);
 
 	// Must run after _isRasterizing and _dynamicState are populated
-	initCustomSamplePositions(pCreateInfo);
+	initSampleLocations(pCreateInfo);
 
 	// Depth stencil content - clearing will disable depth and stencil testing
 	// Must ignore allowed bad pDepthStencilState pointer if rasterization disabled or no depth or stencil attachment format
@@ -563,6 +570,8 @@
 		case VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE:    return PrimitiveRestartEnable;
 		case VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY:          return PrimitiveTopology;
 		case VK_DYNAMIC_STATE_RASTERIZER_DISCARD_ENABLE:   return RasterizerDiscardEnable;
+		case VK_DYNAMIC_STATE_SAMPLE_LOCATIONS_EXT:        return SampleLocations;
+		case VK_DYNAMIC_STATE_SAMPLE_LOCATIONS_ENABLE_EXT: return SampleLocationsEnable;
 		case VK_DYNAMIC_STATE_SCISSOR:                     return Scissors;
 		case VK_DYNAMIC_STATE_SCISSOR_WITH_COUNT:          return Scissors;
 		case VK_DYNAMIC_STATE_STENCIL_COMPARE_MASK:        return StencilCompareMask;
@@ -626,7 +635,7 @@
 }
 
 // Must run after _isRasterizing and _dynamicState are populated
-void MVKGraphicsPipeline::initCustomSamplePositions(const VkGraphicsPipelineCreateInfo* pCreateInfo) {
+void MVKGraphicsPipeline::initSampleLocations(const VkGraphicsPipelineCreateInfo* pCreateInfo) {
 
 	// Must ignore allowed bad pMultisampleState pointer if rasterization disabled
 	if ( !(_isRasterizing && pCreateInfo->pMultisampleState) ) { return; }
@@ -635,12 +644,9 @@
 		switch (next->sType) {
 			case VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT: {
 				auto* pSampLocnsCreateInfo = (VkPipelineSampleLocationsStateCreateInfoEXT*)next;
-				_isUsingCustomSamplePositions = pSampLocnsCreateInfo->sampleLocationsEnable;
-				if (_isUsingCustomSamplePositions && !isDynamicState(SampleLocations)) {
-					for (uint32_t slIdx = 0; slIdx < pSampLocnsCreateInfo->sampleLocationsInfo.sampleLocationsCount; slIdx++) {
-						auto& sl = pSampLocnsCreateInfo->sampleLocationsInfo.pSampleLocations[slIdx];
-						_customSamplePositions.push_back(MTLSamplePositionMake(sl.x, sl.y));
-					}
+				_sampleLocationsEnable = pSampLocnsCreateInfo->sampleLocationsEnable;
+				for (uint32_t slIdx = 0; slIdx < pSampLocnsCreateInfo->sampleLocationsInfo.sampleLocationsCount; slIdx++) {
+					_sampleLocations.push_back(pSampLocnsCreateInfo->sampleLocationsInfo.pSampleLocations[slIdx]);
 				}
 				break;
 			}
@@ -1635,7 +1641,7 @@
 
     // Multisampling - must ignore allowed bad pMultisampleState pointer if rasterization disabled
     if (_isRasterizing && pCreateInfo->pMultisampleState) {
-        plDesc.sampleCount = mvkSampleCountFromVkSampleCountFlagBits(pCreateInfo->pMultisampleState->rasterizationSamples);
+        plDesc.rasterSampleCount = mvkSampleCountFromVkSampleCountFlagBits(pCreateInfo->pMultisampleState->rasterizationSamples);
         plDesc.alphaToCoverageEnabled = pCreateInfo->pMultisampleState->alphaToCoverageEnable;
         plDesc.alphaToOneEnabled = pCreateInfo->pMultisampleState->alphaToOneEnable;
 
@@ -1926,10 +1932,14 @@
     }
 }
 
-// We render points if either the topology or polygon fill mode dictate it
+// We render points if either the static topology or static polygon fill mode dictate it
 bool MVKGraphicsPipeline::isRenderingPoints(const VkGraphicsPipelineCreateInfo* pCreateInfo) {
-	return ((pCreateInfo->pInputAssemblyState && (pCreateInfo->pInputAssemblyState->topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST)) ||
-			(pCreateInfo->pRasterizationState && (pCreateInfo->pRasterizationState->polygonMode == VK_POLYGON_MODE_POINT)));
+	return ((pCreateInfo->pInputAssemblyState && 
+			 (pCreateInfo->pInputAssemblyState->topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST) &&
+			 !isDynamicState(PrimitiveTopology)) ||
+			(pCreateInfo->pRasterizationState && 
+			 (pCreateInfo->pRasterizationState->polygonMode == VK_POLYGON_MODE_POINT) &&
+			 !isDynamicState(PolygonMode)));
 }
 
 // We disable rasterization if either static rasterizerDiscard is enabled or the static cull mode dictates it.
diff --git a/MoltenVK/MoltenVK/Layers/MVKExtensions.def b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
index f63ecf9..d8c222b 100644
--- a/MoltenVK/MoltenVK/Layers/MVKExtensions.def
+++ b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
@@ -105,6 +105,7 @@
 MVK_EXTENSION(EXT_descriptor_indexing,                EXT_DESCRIPTOR_INDEXING,                DEVICE,   10.11,  8.0,  1.0)
 MVK_EXTENSION(EXT_extended_dynamic_state,             EXT_EXTENDED_DYNAMIC_STATE,             DEVICE,   10.11,  8.0,  1.0)
 MVK_EXTENSION(EXT_extended_dynamic_state2,            EXT_EXTENDED_DYNAMIC_STATE_2,           DEVICE,   10.11,  8.0,  1.0)
+MVK_EXTENSION(EXT_extended_dynamic_state3,            EXT_EXTENDED_DYNAMIC_STATE_3,           DEVICE,   10.11,  8.0,  1.0)
 MVK_EXTENSION(EXT_external_memory_host,               EXT_EXTERNAL_MEMORY_HOST,               DEVICE,   10.11,  8.0,  1.0)
 MVK_EXTENSION(EXT_fragment_shader_interlock,          EXT_FRAGMENT_SHADER_INTERLOCK,          DEVICE,   10.13, 11.0,  1.0)
 MVK_EXTENSION(EXT_hdr_metadata,                       EXT_HDR_METADATA,                       DEVICE,   10.15, MVK_NA, MVK_NA)
diff --git a/MoltenVK/MoltenVK/Utility/MVKFoundation.cpp b/MoltenVK/MoltenVK/Utility/MVKFoundation.cpp
index 85ad7d5..d00fb39 100644
--- a/MoltenVK/MoltenVK/Utility/MVKFoundation.cpp
+++ b/MoltenVK/MoltenVK/Utility/MVKFoundation.cpp
@@ -33,7 +33,7 @@
 		case kMVKCommandUseBeginRendering:               return "vkCmdBeginRendering";
 		case kMVKCommandUseBeginRenderPass:              return "vkCmdBeginRenderPass";
 		case kMVKCommandUseNextSubpass:                  return "vkCmdNextSubpass";
-		case kMVKCommandUseRestartSubpass:               return "Metal renderpass restart on barrier";
+		case kMVKCommandUseRestartSubpass:               return "Metal renderpass restart";
 		case kMVKCommandUsePipelineBarrier:              return "vkCmdPipelineBarrier";
 		case kMVKCommandUseBlitImage:                    return "vkCmdBlitImage";
 		case kMVKCommandUseCopyImage:                    return "vkCmdCopyImage";
diff --git a/MoltenVK/MoltenVK/Utility/MVKFoundation.h b/MoltenVK/MoltenVK/Utility/MVKFoundation.h
index f582072..d3aa660 100644
--- a/MoltenVK/MoltenVK/Utility/MVKFoundation.h
+++ b/MoltenVK/MoltenVK/Utility/MVKFoundation.h
@@ -76,7 +76,7 @@
 	kMVKCommandUseBeginRendering,               /**< vkCmdBeginRendering. */
     kMVKCommandUseBeginRenderPass,              /**< vkCmdBeginRenderPass. */
     kMVKCommandUseNextSubpass,                  /**< vkCmdNextSubpass. */
-	kMVKCommandUseRestartSubpass,               /**< Restart a subpass because of explicit or implicit barrier. */
+	kMVKCommandUseRestartSubpass,               /**< Create a new Metal renderpass due to Metal requirements. */
     kMVKCommandUsePipelineBarrier,              /**< vkCmdPipelineBarrier. */
     kMVKCommandUseBlitImage,                    /**< vkCmdBlitImage. */
     kMVKCommandUseCopyImage,                    /**< vkCmdCopyImage. */
@@ -102,8 +102,8 @@
 
 /** Represents a given stage of a graphics pipeline. */
 enum MVKGraphicsStage {
-	kMVKGraphicsStageVertex = 0,	/**< The vertex shader stage. */
-	kMVKGraphicsStageTessControl,	/**< The tessellation control shader stage. */
+	kMVKGraphicsStageVertex = 0,	/**< The tessellation vertex compute shader stage. */
+	kMVKGraphicsStageTessControl,	/**< The tessellation control compute shader stage. */
 	kMVKGraphicsStageRasterization	/**< The rest of the pipeline. */
 };
 
diff --git a/MoltenVK/MoltenVK/Vulkan/vulkan.mm b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
index d1c6fbb..c08c5b3 100644
--- a/MoltenVK/MoltenVK/Vulkan/vulkan.mm
+++ b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
@@ -3652,6 +3652,120 @@
 #pragma mark -
 #pragma mark VK_EXT_extended_dynamic_state3
 
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetAlphaToCoverageEnableEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkBool32                                    alphaToCoverageEnable) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetAlphaToOneEnableEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkBool32                                    alphaToOneEnable) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetColorBlendAdvancedEXT(
+    VkCommandBuffer                             commandBuffer,
+    uint32_t                                    firstAttachment,
+    uint32_t                                    attachmentCount,
+    const VkColorBlendAdvancedEXT*              pColorBlendAdvanced) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetColorBlendEnableEXT(
+    VkCommandBuffer                             commandBuffer,
+    uint32_t                                    firstAttachment,
+    uint32_t                                    attachmentCount,
+    const VkBool32*                             pColorBlendEnables) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetColorBlendEquationEXT(
+    VkCommandBuffer                             commandBuffer,
+    uint32_t                                    firstAttachment,
+    uint32_t                                    attachmentCount,
+    const VkColorBlendEquationEXT*              pColorBlendEquations) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetColorWriteMaskEXT(
+    VkCommandBuffer                             commandBuffer,
+    uint32_t                                    firstAttachment,
+    uint32_t                                    attachmentCount,
+    const VkColorComponentFlags*                pColorWriteMasks) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetConservativeRasterizationModeEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkConservativeRasterizationModeEXT          conservativeRasterizationMode) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetDepthClampEnableEXT(
+    VkCommandBuffer                             commandBuffer,
+	VkBool32                                    depthClampEnable) {
+
+    MVKTraceVulkanCallStart();
+	MVKAddCmd(SetDepthClipEnable, commandBuffer, !depthClampEnable);
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetDepthClipEnableEXT(
+    VkCommandBuffer                             commandBuffer,
+	VkBool32                                    depthClipEnable) {
+
+    MVKTraceVulkanCallStart();
+	MVKAddCmd(SetDepthClipEnable, commandBuffer, depthClipEnable);
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetDepthClipNegativeOneToOneEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkBool32                                    negativeOneToOne) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetExtraPrimitiveOverestimationSizeEXT(
+    VkCommandBuffer                             commandBuffer,
+    float                                       extraPrimitiveOverestimationSize) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetLineRasterizationModeEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkLineRasterizationModeEXT                  lineRasterizationMode) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetLineStippleEnableEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkBool32                                    stippledLineEnable) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
 MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetLogicOpEnableEXT(
     VkCommandBuffer                             commandBuffer,
     VkBool32                                    logicOpEnable) {
@@ -3660,6 +3774,65 @@
     MVKTraceVulkanCallEnd();
 }
 
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetPolygonModeEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkPolygonMode                               polygonMode) {
+
+    MVKTraceVulkanCallStart();
+	MVKAddCmd(SetPolygonMode, commandBuffer, polygonMode);
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetProvokingVertexModeEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkProvokingVertexModeEXT                    provokingVertexMode) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetRasterizationSamplesEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkSampleCountFlagBits                       rasterizationSamples) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetRasterizationStreamEXT(
+    VkCommandBuffer                             commandBuffer,
+    uint32_t                                    rasterizationStream) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetSampleLocationsEnableEXT(
+    VkCommandBuffer                             commandBuffer,
+	VkBool32                                    sampleLocationsEnable) {
+
+    MVKTraceVulkanCallStart();
+	MVKAddCmd(SetSampleLocationsEnable, commandBuffer, sampleLocationsEnable);
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetSampleMaskEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkSampleCountFlagBits                       samples,
+    const VkSampleMask*                         pSampleMask) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkCmdSetTessellationDomainOriginEXT(
+    VkCommandBuffer                             commandBuffer,
+    VkTessellationDomainOrigin                  domainOrigin) {
+
+    MVKTraceVulkanCallStart();
+    MVKTraceVulkanCallEnd();
+}
+
 
 #pragma mark -
 #pragma mark VK_EXT_external_memory_host extension