Merge pull request #1619 from spnda/KHR_buffer_device_address

Add support for KHR_buffer_device_address
diff --git a/Docs/MoltenVK_Runtime_UserGuide.md b/Docs/MoltenVK_Runtime_UserGuide.md
index 23c4bf6..b7f572d 100644
--- a/Docs/MoltenVK_Runtime_UserGuide.md
+++ b/Docs/MoltenVK_Runtime_UserGuide.md
@@ -306,6 +306,7 @@
 - `VK_EXT_image_robustness`
 - `VK_EXT_inline_uniform_block`
 - `VK_EXT_memory_budget` *(requires Metal 2.0)*
+- `VK_EXT_metal_objects`
 - `VK_EXT_metal_surface`
 - `VK_EXT_post_depth_coverage` *(iOS and macOS, requires family 4 (A11) or better Apple GPU)*
 - `VK_EXT_private_data `
diff --git a/Docs/Whats_New.md b/Docs/Whats_New.md
index 9766904..dfdbc27 100644
--- a/Docs/Whats_New.md
+++ b/Docs/Whats_New.md
@@ -13,6 +13,17 @@
 
 
 
+MoltenVK 1.1.11
+--------------
+
+Released TBD
+
+- Add support for extensions:
+	- `VK_EXT_metal_objects`
+- Reducing redundant state changes to improve commend encoding performance.
+
+
+
 MoltenVK 1.1.10
 --------------
 
diff --git a/ExternalRevisions/Vulkan-Headers_repo_revision b/ExternalRevisions/Vulkan-Headers_repo_revision
index b907983..1245433 100644
--- a/ExternalRevisions/Vulkan-Headers_repo_revision
+++ b/ExternalRevisions/Vulkan-Headers_repo_revision
@@ -1 +1 @@
-3ef4c97fd6ea001d75a8e9da408ee473c180e456
+ec9b96aae53e152f6812e66fa43757c1256369a0
diff --git a/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h b/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
index 858f060..4a99c3d 100644
--- a/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
+++ b/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
@@ -50,7 +50,7 @@
  */
 #define MVK_VERSION_MAJOR   1
 #define MVK_VERSION_MINOR   1
-#define MVK_VERSION_PATCH   10
+#define MVK_VERSION_PATCH   11
 
 #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)
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
index c6e7280..0def2be 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.h
@@ -373,7 +373,7 @@
     // Template function that marks both the vector and all binding elements in the vector as dirty.
     template<class T>
     void markDirty(T& bindings, bool& bindingsDirtyFlag) {
-        for (auto& b : bindings) { b.isDirty = true; }
+        for (auto& b : bindings) { b.markDirty(); }
         bindingsDirtyFlag = true;
     }
 
@@ -384,18 +384,18 @@
 
         if ( !b.mtlResource ) { return; }
 
-        T db = b;   // Copy that can be marked dirty
         MVKCommandEncoderState::markDirty();
         bindingsDirtyFlag = true;
-        db.isDirty = true;
 
         for (auto iter = bindings.begin(), end = bindings.end(); iter != end; ++iter) {
-            if( iter->index == db.index ) {
-                *iter = db;
+            if (iter->index == b.index) {
+                iter->update(b);
                 return;
             }
         }
-        bindings.push_back(db);
+
+        bindings.push_back(b);
+        bindings.back().markDirty();
     }
 
 	// For texture bindings, we also keep track of whether any bindings need a texture swizzle
diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
index dfb8c1c..1a36927 100644
--- a/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
+++ b/MoltenVK/MoltenVK/Commands/MVKCommandEncoderState.mm
@@ -37,8 +37,8 @@
 #pragma mark MVKPipelineCommandEncoderState
 
 void MVKPipelineCommandEncoderState::bindPipeline(MVKPipeline* pipeline) {
+    if (pipeline != _pipeline) markDirty();
     _pipeline = pipeline;
-    markDirty();
 }
 
 MVKPipeline* MVKPipelineCommandEncoderState::getPipeline() { return _pipeline; }
@@ -71,14 +71,17 @@
 		usingViewports.resize(firstViewport + vpCnt);
 	}
 
+    bool dirty;
 	bool mustSetDynamically = _cmdEncoder->supportsDynamicState(VK_DYNAMIC_STATE_VIEWPORT);
 	if (isSettingDynamically || (!mustSetDynamically && vpCnt > 0)) {
+        dirty = memcmp(&usingViewports[firstViewport], &viewports[0], vpCnt * sizeof(VkViewport)) != 0;
 		std::copy(viewports.begin(), viewports.end(), usingViewports.begin() + firstViewport);
 	} else {
+        dirty = !usingViewports.empty();
 		usingViewports.clear();
 	}
 
-	markDirty();
+	if (dirty) markDirty();
 }
 
 void MVKViewportCommandEncoderState::encodeImpl(uint32_t stage) {
@@ -121,14 +124,17 @@
 		usingScissors.resize(firstScissor + sCnt);
 	}
 
+    bool dirty;
 	bool mustSetDynamically = _cmdEncoder->supportsDynamicState(VK_DYNAMIC_STATE_SCISSOR);
 	if (isSettingDynamically || (!mustSetDynamically && sCnt > 0)) {
+        dirty = memcmp(&usingScissors[firstScissor], &scissors[0], sCnt * sizeof(VkRect2D)) != 0;
 		std::copy(scissors.begin(), scissors.end(), usingScissors.begin() + firstScissor);
 	} else {
+        dirty = !usingScissors.empty();
 		usingScissors.clear();
 	}
 
-	markDirty();
+	if (dirty) markDirty();
 }
 
 void MVKScissorCommandEncoderState::encodeImpl(uint32_t stage) {
@@ -248,6 +254,7 @@
 #pragma mark MVKDepthStencilCommandEncoderState
 
 void MVKDepthStencilCommandEncoderState:: setDepthStencilState(const VkPipelineDepthStencilStateCreateInfo& vkDepthStencilInfo) {
+    auto oldData = _depthStencilData;
 
     if (vkDepthStencilInfo.depthTestEnable) {
         _depthStencilData.depthCompareFunction = mvkMTLCompareFunctionFromVkCompareOp(vkDepthStencilInfo.depthCompareOp);
@@ -260,7 +267,7 @@
     setStencilState(_depthStencilData.frontFaceStencilData, vkDepthStencilInfo.front, vkDepthStencilInfo.stencilTestEnable);
     setStencilState(_depthStencilData.backFaceStencilData, vkDepthStencilInfo.back, vkDepthStencilInfo.stencilTestEnable);
 
-    markDirty();
+    if (!(oldData == _depthStencilData)) markDirty();
 }
 
 void MVKDepthStencilCommandEncoderState::setStencilState(MVKMTLStencilDescriptorData& stencilInfo,
@@ -289,6 +296,8 @@
 // it may not be accurate, and if not dynamic, pipeline will override when it is encoded anyway.
 void MVKDepthStencilCommandEncoderState::setStencilCompareMask(VkStencilFaceFlags faceMask,
                                                                uint32_t stencilCompareMask) {
+    auto oldData = _depthStencilData;
+
     if (mvkAreAllFlagsEnabled(faceMask, VK_STENCIL_FACE_FRONT_BIT)) {
         _depthStencilData.frontFaceStencilData.readMask = stencilCompareMask;
     }
@@ -296,13 +305,15 @@
         _depthStencilData.backFaceStencilData.readMask = stencilCompareMask;
     }
 
-    markDirty();
+    if (!(oldData == _depthStencilData)) markDirty();
 }
 
 // We don't check for dynamic state here, because if this is called before pipeline is set,
 // it may not be accurate, and if not dynamic, pipeline will override when it is encoded anyway.
 void MVKDepthStencilCommandEncoderState::setStencilWriteMask(VkStencilFaceFlags faceMask,
                                                              uint32_t stencilWriteMask) {
+    auto oldData = _depthStencilData;
+
     if (mvkAreAllFlagsEnabled(faceMask, VK_STENCIL_FACE_FRONT_BIT)) {
         _depthStencilData.frontFaceStencilData.writeMask = stencilWriteMask;
     }
@@ -310,10 +321,12 @@
         _depthStencilData.backFaceStencilData.writeMask = stencilWriteMask;
     }
 
-    markDirty();
+    if (!(oldData == _depthStencilData)) markDirty();
 }
 
 void MVKDepthStencilCommandEncoderState::beginMetalRenderPass() {
+    MVKCommandEncoderState::beginMetalRenderPass();
+
 	MVKRenderSubpass* mvkSubpass = _cmdEncoder->getSubpass();
 	MVKPixelFormats* pixFmts = _cmdEncoder->getPixelFormats();
 	MTLPixelFormat mtlDSFormat = pixFmts->getMTLPixelFormat(mvkSubpass->getDepthStencilFormat());
@@ -351,22 +364,27 @@
     // If ref values are to be set dynamically, don't set them here.
     if (_cmdEncoder->supportsDynamicState(VK_DYNAMIC_STATE_STENCIL_REFERENCE)) { return; }
 
+    if (_frontFaceValue != vkDepthStencilInfo.front.reference || _backFaceValue != vkDepthStencilInfo.back.reference)
+        markDirty();
+
     _frontFaceValue = vkDepthStencilInfo.front.reference;
     _backFaceValue = vkDepthStencilInfo.back.reference;
-    markDirty();
 }
 
 // We don't check for dynamic state here, because if this is called before pipeline is set,
 // it may not be accurate, and if not dynamic, pipeline will override when it is encoded anyway.
 void MVKStencilReferenceValueCommandEncoderState::setReferenceValues(VkStencilFaceFlags faceMask,
                                                                      uint32_t stencilReference) {
+    bool dirty = false;
     if (mvkAreAllFlagsEnabled(faceMask, VK_STENCIL_FACE_FRONT_BIT)) {
+        dirty |= (_frontFaceValue != stencilReference);
         _frontFaceValue = stencilReference;
     }
     if (mvkAreAllFlagsEnabled(faceMask, VK_STENCIL_FACE_BACK_BIT)) {
+        dirty |= (_backFaceValue != stencilReference);
         _backFaceValue = stencilReference;
     }
-    markDirty();
+    if (dirty) markDirty();
 }
 
 void MVKStencilReferenceValueCommandEncoderState::encodeImpl(uint32_t stage) {
@@ -381,16 +399,20 @@
 
 void MVKDepthBiasCommandEncoderState::setDepthBias(const VkPipelineRasterizationStateCreateInfo& vkRasterInfo) {
 
+    auto wasEnabled = _isEnabled;
     _isEnabled = vkRasterInfo.depthBiasEnable;
 
     // If ref values are to be set dynamically, don't set them here.
     if (_cmdEncoder->supportsDynamicState(VK_DYNAMIC_STATE_DEPTH_BIAS)) { return; }
 
-    _depthBiasConstantFactor = vkRasterInfo.depthBiasConstantFactor;
-    _depthBiasSlopeFactor = vkRasterInfo.depthBiasSlopeFactor;
-    _depthBiasClamp = vkRasterInfo.depthBiasClamp;
+    if (_isEnabled != wasEnabled || _depthBiasConstantFactor != vkRasterInfo.depthBiasConstantFactor
+        || _depthBiasSlopeFactor != vkRasterInfo.depthBiasSlopeFactor || _depthBiasClamp != vkRasterInfo.depthBiasClamp) {
 
-    markDirty();
+        markDirty();
+        _depthBiasConstantFactor = vkRasterInfo.depthBiasConstantFactor;
+        _depthBiasSlopeFactor = vkRasterInfo.depthBiasSlopeFactor;
+        _depthBiasClamp = vkRasterInfo.depthBiasClamp;
+    }
 }
 
 // We don't check for dynamic state here, because if this is called before pipeline is set,
@@ -398,11 +420,15 @@
 void MVKDepthBiasCommandEncoderState::setDepthBias(float depthBiasConstantFactor,
                                                    float depthBiasSlopeFactor,
                                                    float depthBiasClamp) {
-    _depthBiasConstantFactor = depthBiasConstantFactor;
-    _depthBiasSlopeFactor = depthBiasSlopeFactor;
-    _depthBiasClamp = depthBiasClamp;
 
-    markDirty();
+    if (_depthBiasConstantFactor != depthBiasConstantFactor || _depthBiasSlopeFactor != depthBiasSlopeFactor
+        || _depthBiasClamp != depthBiasClamp) {
+
+        markDirty();
+        _depthBiasConstantFactor = depthBiasConstantFactor;
+        _depthBiasSlopeFactor = depthBiasSlopeFactor;
+        _depthBiasClamp = depthBiasClamp;
+    }
 }
 
 void MVKDepthBiasCommandEncoderState::encodeImpl(uint32_t stage) {
@@ -426,12 +452,13 @@
     // Abort if we are using dynamic, but call is not dynamic.
 	if ( !isDynamic && _cmdEncoder->supportsDynamicState(VK_DYNAMIC_STATE_BLEND_CONSTANTS) ) { return; }
 
-    _red = red;
-    _green = green;
-    _blue = blue;
-    _alpha = alpha;
-
-    markDirty();
+    if (_red != red || _green != green || _blue != blue || _alpha != alpha) {
+        markDirty();
+        _red = red;
+        _green = green;
+        _blue = blue;
+        _alpha = alpha;
+    }
 }
 
 void MVKBlendColorCommandEncoderState::encodeImpl(uint32_t stage) {
@@ -752,6 +779,10 @@
                                                            b.mtlBytes,
                                                            b.size,
                                                            b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl)
+                                            setBufferOffset: b.offset
+                                            atIndex: b.index];
                            else
                                [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBuffer: b.mtlBuffer
                                                                                                              offset: b.offset
@@ -785,9 +816,14 @@
                                                               b.size,
                                                               b.index);
                                } else {
-                                   [cmdEncoder->_mtlRenderEncoder setVertexBuffer: b.mtlBuffer
-                                                                           offset: b.offset
-                                                                          atIndex: b.index];
+                                   if (b.justOffset) {
+                                       [cmdEncoder->_mtlRenderEncoder setVertexBufferOffset: b.offset
+                                                                                    atIndex: b.index];
+                                   } else {
+                                       [cmdEncoder->_mtlRenderEncoder setVertexBuffer: b.mtlBuffer
+                                                                               offset: b.offset
+                                                                              atIndex: b.index];
+                                   }
 
                                    // Add any translated vertex bindings for this binding
                                    auto xltdVtxBindings = pipeline->getTranslatedVertexBindings();
@@ -828,6 +864,9 @@
                                                            b.mtlBytes,
                                                            b.size,
                                                            b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBufferOffset: b.offset
+                                                                                                                  atIndex: b.index];
                            else
                                [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseTessellationVertexTessCtl) setBuffer: b.mtlBuffer
                                                                                                              offset: b.offset
@@ -858,6 +897,9 @@
                                                           b.mtlBytes,
                                                           b.size,
                                                           b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->_mtlRenderEncoder setVertexBufferOffset: b.offset
+                                                                            atIndex: b.index];
                            else
                                [cmdEncoder->_mtlRenderEncoder setVertexBuffer: b.mtlBuffer
                                                                        offset: b.offset
@@ -888,6 +930,9 @@
                                                             b.mtlBytes,
                                                             b.size,
                                                             b.index);
+                           else if (b.justOffset)
+                               [cmdEncoder->_mtlRenderEncoder setFragmentBufferOffset: b.offset
+                                                                              atIndex: b.index];
                            else
                                [cmdEncoder->_mtlRenderEncoder setFragmentBuffer: b.mtlBuffer
                                                                          offset: b.offset
@@ -1027,7 +1072,12 @@
 										b.mtlBytes,
 										b.size,
 										b.index);
-		} else {
+        } else if (b.justOffset) {
+            [cmdEncoder->getMTLComputeEncoder(kMVKCommandUseDispatch)
+                        setBufferOffset: b.offset
+                                atIndex: b.index];
+
+        } else {
 			[cmdEncoder->getMTLComputeEncoder(kMVKCommandUseDispatch) setBuffer: b.mtlBuffer
 																		 offset: b.offset
 																		atIndex: b.index];
diff --git a/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h b/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h
index 012600a..a0f71d0 100644
--- a/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h
+++ b/MoltenVK/MoltenVK/Commands/MVKMTLResourceBindings.h
@@ -34,6 +34,16 @@
     uint32_t swizzle = 0;
 	uint16_t index = 0;
     bool isDirty = true;
+
+    inline void markDirty() { isDirty = true; }
+
+    inline void update(const MVKMTLTextureBinding &other) {
+        if (mtlTexture != other.mtlTexture || swizzle != other.swizzle) {
+            mtlTexture = other.mtlTexture;
+            swizzle = other.swizzle;
+            markDirty();
+        }
+    }
 } MVKMTLTextureBinding;
 
 /** Describes a MTLSamplerState resource binding. */
@@ -41,6 +51,15 @@
     union { id<MTLSamplerState> mtlSamplerState = nil; id<MTLSamplerState> mtlResource; }; // aliases
     uint16_t index = 0;
     bool isDirty = true;
+
+    inline void markDirty() { isDirty = true; }
+
+    inline void update(const MVKMTLSamplerStateBinding &other) {
+        if (mtlSamplerState != other.mtlSamplerState) {
+            mtlSamplerState = other.mtlSamplerState;
+            markDirty();
+        }
+    }
 } MVKMTLSamplerStateBinding;
 
 /** Describes a MTLBuffer resource binding. */
@@ -49,8 +68,27 @@
     VkDeviceSize offset = 0;
     uint32_t size = 0;
 	uint16_t index = 0;
+    bool justOffset = false;
     bool isDirty = true;
     bool isInline = false;
+
+    inline void markDirty() { justOffset = false; isDirty = true; }
+
+    inline void update(const MVKMTLBufferBinding &other) {
+        if (mtlBuffer != other.mtlBuffer || size != other.size || isInline != other.isInline) {
+            mtlBuffer = other.mtlBuffer;
+            size = other.size;
+            isInline = other.isInline;
+            offset = other.offset;
+
+            justOffset = false;
+            isDirty = true;
+        } else if (offset != other.offset) {
+            offset = other.offset;
+            justOffset = !isDirty || justOffset;
+            isDirty = true;
+        }
+    }
 } MVKMTLBufferBinding;
 
 /** Describes a MTLBuffer resource binding as used for an index buffer. */
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
index 63b6e18..9989a69 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.h
@@ -765,6 +765,9 @@
 	/** Returns whether this instance is currently automatically capturing a GPU trace. */
 	inline bool isCurrentlyAutoGPUCapturing() { return _isCurrentlyAutoGPUCapturing; }
 
+	/** Returns the Metal objects underpinning the Vulkan objects indicated in the pNext chain of pMetalObjectsInfo. */
+	void getMetalObjects(VkExportMetalObjectsInfoEXT* pMetalObjectsInfo);
+
 
 #pragma mark Properties directly accessible
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
index 2097bd2..2926b2a 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
@@ -3339,20 +3339,31 @@
 MVKSemaphore* MVKDevice::createSemaphore(const VkSemaphoreCreateInfo* pCreateInfo,
 										 const VkAllocationCallbacks* pAllocator) {
 	const VkSemaphoreTypeCreateInfo* pTypeCreateInfo = nullptr;
+	const VkExportMetalObjectCreateInfoEXT* pExportInfo = nullptr;
+	const VkImportMetalSharedEventInfoEXT* pImportInfo = nullptr;
 	for (auto* next = (const VkBaseInStructure*)pCreateInfo->pNext; next; next = next->pNext) {
 		switch (next->sType) {
 			case VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO:
 				pTypeCreateInfo = (VkSemaphoreTypeCreateInfo*)next;
 				break;
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECT_CREATE_INFO_EXT:
+				pExportInfo = (VkExportMetalObjectCreateInfoEXT*)next;
+				break;
+			case VK_STRUCTURE_TYPE_IMPORT_METAL_SHARED_EVENT_INFO_EXT:
+				pImportInfo = (VkImportMetalSharedEventInfoEXT*)next;
+				break;
 			default:
 				break;
 		}
 	}
-	if (pTypeCreateInfo && pTypeCreateInfo->semaphoreType == VK_SEMAPHORE_TYPE_TIMELINE) {
+
+	if ((pTypeCreateInfo && pTypeCreateInfo->semaphoreType == VK_SEMAPHORE_TYPE_TIMELINE) ||
+		(pExportInfo && pExportInfo->exportObjectType == VK_EXPORT_METAL_OBJECT_TYPE_METAL_SHARED_EVENT_BIT_EXT) ||
+		 pImportInfo) {
 		if (_pMetalFeatures->events) {
-			return new MVKTimelineSemaphoreMTLEvent(this, pCreateInfo, pTypeCreateInfo);
+			return new MVKTimelineSemaphoreMTLEvent(this, pCreateInfo, pTypeCreateInfo, pExportInfo, pImportInfo);
 		} else {
-			return new MVKTimelineSemaphoreEmulated(this, pCreateInfo, pTypeCreateInfo);
+			return new MVKTimelineSemaphoreEmulated(this, pCreateInfo, pTypeCreateInfo, pExportInfo, pImportInfo);
 		}
 	} else {
 		switch (_vkSemaphoreStyle) {
@@ -3370,10 +3381,25 @@
 
 MVKEvent* MVKDevice::createEvent(const VkEventCreateInfo* pCreateInfo,
 								 const VkAllocationCallbacks* pAllocator) {
+	const VkExportMetalObjectCreateInfoEXT* pExportInfo = nullptr;
+	const VkImportMetalSharedEventInfoEXT* pImportInfo = nullptr;
+	for (auto* next = (const VkBaseInStructure*)pCreateInfo->pNext; next; next = next->pNext) {
+		switch (next->sType) {
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECT_CREATE_INFO_EXT:
+				pExportInfo = (VkExportMetalObjectCreateInfoEXT*)next;
+				break;
+			case VK_STRUCTURE_TYPE_IMPORT_METAL_SHARED_EVENT_INFO_EXT:
+				pImportInfo = (VkImportMetalSharedEventInfoEXT*)next;
+				break;
+			default:
+				break;
+		}
+	}
+
 	if (_pMetalFeatures->events) {
-		return new MVKEventNative(this, pCreateInfo);
+		return new MVKEventNative(this, pCreateInfo, pExportInfo, pImportInfo);
 	} else {
-		return new MVKEventEmulated(this, pCreateInfo);
+		return new MVKEventEmulated(this, pCreateInfo, pExportInfo, pImportInfo);
 	}
 }
 
@@ -3988,6 +4014,64 @@
 	}
 }
 
+void MVKDevice::getMetalObjects(VkExportMetalObjectsInfoEXT* pMetalObjectsInfo) {
+	for (auto* next = (VkBaseOutStructure*)pMetalObjectsInfo->pNext; next; next = next->pNext) {
+		switch (next->sType) {
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_DEVICE_INFO_EXT: {
+				auto* pDvcInfo = (VkExportMetalDeviceInfoEXT*)next;
+				pDvcInfo->mtlDevice = getMTLDevice();
+				break;
+			}
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_COMMAND_QUEUE_INFO_EXT: {
+				auto* pQInfo = (VkExportMetalCommandQueueInfoEXT*)next;
+				MVKQueue* mvkQ = MVKQueue::getMVKQueue(pQInfo->queue);
+				pQInfo->mtlCommandQueue = mvkQ->getMTLCommandQueue();
+				break;
+			}
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_BUFFER_INFO_EXT: {
+				auto* pBuffInfo = (VkExportMetalBufferInfoEXT*)next;
+				auto* mvkDevMem = (MVKDeviceMemory*)pBuffInfo->memory;
+				pBuffInfo->mtlBuffer = mvkDevMem->getMTLBuffer();
+				break;
+			}
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_TEXTURE_INFO_EXT: {
+				auto* pImgInfo = (VkExportMetalTextureInfoEXT*)next;
+				uint8_t planeIndex = MVKImage::getPlaneFromVkImageAspectFlags(pImgInfo->plane);
+				auto* mvkImg = (MVKImage*)pImgInfo->image;
+				auto* mvkImgView = (MVKImageView*)pImgInfo->imageView;
+				auto* mvkBuffView = (MVKBufferView*)pImgInfo->bufferView;
+				if (mvkImg) {
+					pImgInfo->mtlTexture = mvkImg->getMTLTexture(planeIndex);
+				} else if (mvkImgView) {
+					pImgInfo->mtlTexture = mvkImgView->getMTLTexture(planeIndex);
+				} else {
+					pImgInfo->mtlTexture = mvkBuffView->getMTLTexture();
+				}
+				break;
+			}
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_IO_SURFACE_INFO_EXT: {
+				auto* pIOSurfInfo = (VkExportMetalIOSurfaceInfoEXT*)next;
+				auto* mvkImg = (MVKImage*)pIOSurfInfo->image;
+				pIOSurfInfo->ioSurface = mvkImg->getIOSurface();
+				break;
+			}
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_SHARED_EVENT_INFO_EXT: {
+				auto* pShEvtInfo = (VkExportMetalSharedEventInfoEXT*)next;
+				auto* mvkSem4 = (MVKSemaphore*)pShEvtInfo->semaphore;
+				auto* mvkEvt = (MVKEvent*)pShEvtInfo->event;
+				if (mvkSem4) {
+					pShEvtInfo->mtlSharedEvent = mvkSem4->getMTLSharedEvent();
+				} else if (mvkEvt) {
+					pShEvtInfo->mtlSharedEvent = mvkEvt->getMTLSharedEvent();
+				}
+				break;
+			}
+			default:
+				break;
+		}
+	}
+}
+
 
 #pragma mark Construction
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm
index 229b0ab..a397e3c 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDeviceMemory.mm
@@ -283,6 +283,7 @@
 
 	_allocationSize = pAllocateInfo->allocationSize;
 
+	bool willExportMTLBuffer = false;
 	VkImage dedicatedImage = VK_NULL_HANDLE;
 	VkBuffer dedicatedBuffer = VK_NULL_HANDLE;
 	VkExternalMemoryHandleTypeFlags handleTypes = 0;
@@ -300,6 +301,22 @@
 				handleTypes = pExpMemInfo->handleTypes;
 				break;
 			}
+			case VK_STRUCTURE_TYPE_IMPORT_METAL_BUFFER_INFO_EXT: {
+				// Setting Metal objects directly will override Vulkan settings.
+				// It is responsibility of app to ensure these are consistent. Not doing so results in undefined behavior.
+				const auto* pMTLBuffInfo = (VkImportMetalBufferInfoEXT*)next;
+				[_mtlBuffer release];							// guard against dups
+				_mtlBuffer = [pMTLBuffInfo->mtlBuffer retain];	// retained
+				_mtlStorageMode = _mtlBuffer.storageMode;
+				_mtlCPUCacheMode = _mtlBuffer.cpuCacheMode;
+				_allocationSize = _mtlBuffer.length;
+				break;
+			}
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECT_CREATE_INFO_EXT: {
+				const auto* pExportInfo = (VkExportMetalObjectCreateInfoEXT*)next;
+				willExportMTLBuffer = pExportInfo->exportObjectType == VK_EXPORT_METAL_OBJECT_TYPE_METAL_BUFFER_BIT_EXT;
+				break;
+			}
 			default:
 				break;
 		}
@@ -344,8 +361,9 @@
 	}
 
 	// If memory needs to be coherent it must reside in an MTLBuffer, since an open-ended map() must work.
-	if (isMemoryHostCoherent() && !ensureMTLBuffer() ) {
-		setConfigurationResult(reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Could not allocate a host-coherent VkDeviceMemory of size %llu bytes. The maximum memory-aligned size of a host-coherent VkDeviceMemory is %llu bytes.", _allocationSize, _device->_pMetalFeatures->maxMTLBufferSize));
+	// Or if a MTLBuffer will be exported, ensure it exists.
+	if ((isMemoryHostCoherent() || willExportMTLBuffer) && !ensureMTLBuffer() ) {
+		setConfigurationResult(reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Could not allocate a host-coherent or exportable VkDeviceMemory of size %llu bytes. The maximum memory-aligned size of a host-coherent VkDeviceMemory is %llu bytes.", _allocationSize, _device->_pMetalFeatures->maxMTLBufferSize));
 	}
 }
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
index 379ed9d..8340095 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
@@ -712,6 +712,10 @@
 VkResult MVKImage::setMTLTexture(uint8_t planeIndex, id<MTLTexture> mtlTexture) {
 	lock_guard<mutex> lock(_lock);
 
+	if (planeIndex >= _planes.size()) { return reportError(VK_ERROR_INITIALIZATION_FAILED, "Plane index is out of bounds. Attempted to set MTLTexture at plane index %d in VkImage that has %zu planes.", planeIndex, _planes.size()); }
+
+	if (_planes[planeIndex]->_mtlTexture == mtlTexture) { return VK_SUCCESS; }
+
 	releaseIOSurface();
     _planes[planeIndex]->releaseMTLTexture();
 	_planes[planeIndex]->_mtlTexture = [mtlTexture retain];		// retained
@@ -747,6 +751,9 @@
 VkResult MVKImage::useIOSurface(IOSurfaceRef ioSurface) {
 	lock_guard<mutex> lock(_lock);
 
+	// Don't recreate existing. But special case of incoming nil if already nil means create a new IOSurface.
+	if (ioSurface && _ioSurface == ioSurface) { return VK_SUCCESS; }
+
     if (!_device->_pMetalFeatures->ioSurfaces) { return reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkUseIOSurfaceMVK() : IOSurfaces are not supported on this platform."); }
 
 #if MVK_SUPPORT_IOSURFACE_BOOL
@@ -986,6 +993,35 @@
 
 	if (pExtMemInfo) { initExternalMemory(pExtMemInfo->handleTypes); }
 
+	// Setting Metal objects directly will override Vulkan settings.
+	// It is responsibility of app to ensure these are consistent. Not doing so results in undefined behavior.
+	const VkExportMetalObjectCreateInfoEXT* pExportInfo = nullptr;
+	for (const auto* next = (const VkBaseInStructure*)pCreateInfo->pNext; next; next = next->pNext) {
+		switch (next->sType) {
+			case VK_STRUCTURE_TYPE_IMPORT_METAL_TEXTURE_INFO_EXT: {
+				const auto* pMTLTexInfo = (VkImportMetalTextureInfoEXT*)next;
+				uint8_t planeIndex = MVKImage::getPlaneFromVkImageAspectFlags(pMTLTexInfo->plane);
+				setConfigurationResult(setMTLTexture(planeIndex, pMTLTexInfo->mtlTexture));
+				break;
+			}
+			case VK_STRUCTURE_TYPE_IMPORT_METAL_IO_SURFACE_INFO_EXT: {
+				const auto* pIOSurfInfo = (VkImportMetalIOSurfaceInfoEXT*)next;
+				setConfigurationResult(useIOSurface(pIOSurfInfo->ioSurface));
+				break;
+			}
+			case VK_STRUCTURE_TYPE_EXPORT_METAL_OBJECT_CREATE_INFO_EXT:
+				pExportInfo = (VkExportMetalObjectCreateInfoEXT*)next;
+				break;
+			default:
+				break;
+		}
+	}
+
+	// If we're expecting to export an IOSurface, and weren't give one,
+	// base this image on a new IOSurface that matches its configuration.
+	if (pExportInfo && pExportInfo->exportObjectType == VK_EXPORT_METAL_OBJECT_TYPE_METAL_IOSURFACE_BIT_EXT && !_ioSurface) {
+		setConfigurationResult(useIOSurface(nil));
+	}
 }
 
 VkSampleCountFlagBits MVKImage::validateSamples(const VkImageCreateInfo* pCreateInfo, bool isAttachment) {
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
index a512542..4718594 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
@@ -656,6 +656,7 @@
 	ADD_DVC_EXT_ENTRY_POINT(vkCmdDebugMarkerInsertEXT, EXT_DEBUG_MARKER);
 	ADD_DVC_EXT_ENTRY_POINT(vkSetHdrMetadataEXT, EXT_HDR_METADATA);
 	ADD_DVC_EXT_ENTRY_POINT(vkResetQueryPoolEXT, EXT_HOST_QUERY_RESET);
+	ADD_DVC_EXT_ENTRY_POINT(vkExportMetalObjectsEXT, EXT_METAL_OBJECTS);
 	ADD_DVC_EXT_ENTRY_POINT(vkCreatePrivateDataSlotEXT, EXT_PRIVATE_DATA);
 	ADD_DVC_EXT_ENTRY_POINT(vkDestroyPrivateDataSlotEXT, EXT_PRIVATE_DATA);
 	ADD_DVC_EXT_ENTRY_POINT(vkGetPrivateDataEXT, EXT_PRIVATE_DATA);
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSync.h b/MoltenVK/MoltenVK/GPUObjects/MVKSync.h
index 60091a4..fdbaf4a 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKSync.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKSync.h
@@ -183,6 +183,12 @@
 	/** Returns whether this semaphore uses command encoding. */
 	virtual bool isUsingCommandEncoding() = 0;
 
+	/**
+	 * Returns the MTLSharedEvent underlying this Vulkan semaphore,
+	 * or nil if this semaphore is not underpinned by a MTLSharedEvent.
+	 */
+	virtual id<MTLSharedEvent> getMTLSharedEvent() { return nil; };
+
 
 #pragma mark Construction
 
@@ -290,7 +296,11 @@
 
 #pragma mark Construction
 
-    MVKTimelineSemaphore(MVKDevice* device, const VkSemaphoreCreateInfo* pCreateInfo, const VkSemaphoreTypeCreateInfo* pTypeCreateInfo)
+	MVKTimelineSemaphore(MVKDevice* device,
+						 const VkSemaphoreCreateInfo* pCreateInfo,
+						 const VkSemaphoreTypeCreateInfo* pTypeCreateInfo,
+						 const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+						 const VkImportMetalSharedEventInfoEXT* pImportInfo)
         : MVKSemaphore(device, pCreateInfo) {}
 
 };
@@ -306,18 +316,23 @@
 	void encodeWait(id<MTLCommandBuffer> mtlCmdBuff, uint64_t value) override;
 	void encodeSignal(id<MTLCommandBuffer> mtlCmdBuff, uint64_t value) override;
 	bool isUsingCommandEncoding() override { return true; }
+	id<MTLSharedEvent> getMTLSharedEvent() override { return _mtlEvent; };
 
 	uint64_t getCounterValue() override { return _mtlEvent.signaledValue; }
 	void signal(const VkSemaphoreSignalInfo* pSignalInfo) override;
 	bool registerWait(MVKFenceSitter* sitter, const VkSemaphoreWaitInfo* pWaitInfo, uint32_t index) override;
 	void unregisterWait(MVKFenceSitter* sitter) override;
 
-	MVKTimelineSemaphoreMTLEvent(MVKDevice* device, const VkSemaphoreCreateInfo* pCreateInfo, const VkSemaphoreTypeCreateInfo* pTypeCreateInfo);
+	MVKTimelineSemaphoreMTLEvent(MVKDevice* device,
+								 const VkSemaphoreCreateInfo* pCreateInfo,
+								 const VkSemaphoreTypeCreateInfo* pTypeCreateInfo,
+								 const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+								 const VkImportMetalSharedEventInfoEXT* pImportInfo);
 
 	~MVKTimelineSemaphoreMTLEvent() override;
 
 protected:
-	id<MTLSharedEvent> _mtlEvent;
+	id<MTLSharedEvent> _mtlEvent = nil;
 	std::mutex _lock;
 	std::unordered_set<MVKFenceSitter*> _sitters;
 };
@@ -339,7 +354,11 @@
 	bool registerWait(MVKFenceSitter* sitter, const VkSemaphoreWaitInfo* pWaitInfo, uint32_t index) override;
 	void unregisterWait(MVKFenceSitter* sitter) override;
 
-	MVKTimelineSemaphoreEmulated(MVKDevice* device, const VkSemaphoreCreateInfo* pCreateInfo, const VkSemaphoreTypeCreateInfo* pTypeCreateInfo);
+	MVKTimelineSemaphoreEmulated(MVKDevice* device,
+								 const VkSemaphoreCreateInfo* pCreateInfo,
+								 const VkSemaphoreTypeCreateInfo* pTypeCreateInfo,
+								 const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+								 const VkImportMetalSharedEventInfoEXT* pImportInfo);
 
 protected:
 	void signalImpl(uint64_t value);
@@ -475,10 +494,16 @@
 	/** Encodes an operation to block command buffer operation until this event is signaled. */
 	virtual void encodeWait(id<MTLCommandBuffer> mtlCmdBuff) = 0;
 
+	/** Returns the MTLSharedEvent underlying this Vulkan event. */
+	virtual id<MTLSharedEvent> getMTLSharedEvent() = 0;
+
 
 #pragma mark Construction
 
-	MVKEvent(MVKDevice* device, const VkEventCreateInfo* pCreateInfo) : MVKVulkanAPIDeviceObject(device) {}
+	MVKEvent(MVKDevice* device,
+			 const VkEventCreateInfo* pCreateInfo,
+			 const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+			 const VkImportMetalSharedEventInfoEXT* pImportInfo) : MVKVulkanAPIDeviceObject(device) {}
 
 protected:
 	void propagateDebugName() override {}
@@ -497,8 +522,12 @@
 	void signal(bool status) override;
 	void encodeSignal(id<MTLCommandBuffer> mtlCmdBuff, bool status) override;
 	void encodeWait(id<MTLCommandBuffer> mtlCmdBuff) override;
+	id<MTLSharedEvent> getMTLSharedEvent() override { return _mtlEvent; };
 
-	MVKEventNative(MVKDevice* device, const VkEventCreateInfo* pCreateInfo);
+	MVKEventNative(MVKDevice* device,
+				   const VkEventCreateInfo* pCreateInfo,
+				   const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+				   const VkImportMetalSharedEventInfoEXT* pImportInfo);
 
 	~MVKEventNative() override;
 
@@ -518,8 +547,12 @@
 	void signal(bool status) override;
 	void encodeSignal(id<MTLCommandBuffer> mtlCmdBuff, bool status) override;
 	void encodeWait(id<MTLCommandBuffer> mtlCmdBuff) override;
+	id<MTLSharedEvent> getMTLSharedEvent() override { return nil; };
 
-	MVKEventEmulated(MVKDevice* device, const VkEventCreateInfo* pCreateInfo);
+	MVKEventEmulated(MVKDevice* device,
+					 const VkEventCreateInfo* pCreateInfo,
+					 const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+					 const VkImportMetalSharedEventInfoEXT* pImportInfo);
 
 protected:
 	MVKSemaphoreImpl _blocker;
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm b/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm
index d711342..60af66b 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm
@@ -211,11 +211,21 @@
 	_sitters.erase(sitter);
 }
 
-MVKTimelineSemaphoreMTLEvent::MVKTimelineSemaphoreMTLEvent(MVKDevice* device, const VkSemaphoreCreateInfo* pCreateInfo, const VkSemaphoreTypeCreateInfo* pTypeCreateInfo) :
-	MVKTimelineSemaphore(device, pCreateInfo, pTypeCreateInfo),
-	_mtlEvent([device->getMTLDevice() newSharedEvent]) { 	//retained
+MVKTimelineSemaphoreMTLEvent::MVKTimelineSemaphoreMTLEvent(MVKDevice* device,
+														   const VkSemaphoreCreateInfo* pCreateInfo,
+														   const VkSemaphoreTypeCreateInfo* pTypeCreateInfo,
+														   const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+														   const VkImportMetalSharedEventInfoEXT* pImportInfo) :
+	MVKTimelineSemaphore(device, pCreateInfo, pTypeCreateInfo, pExportInfo, pImportInfo) {
 
-	_mtlEvent.signaledValue = pTypeCreateInfo->initialValue;
+	// Import or create a Metal event
+	_mtlEvent = (pImportInfo
+				 ? [pImportInfo->mtlSharedEvent retain]
+				 : [device->getMTLDevice() newSharedEvent]);	//retained
+
+	if (pTypeCreateInfo) {
+		_mtlEvent.signaledValue = pTypeCreateInfo->initialValue;
+	}
 }
 
 MVKTimelineSemaphoreMTLEvent::~MVKTimelineSemaphoreMTLEvent() {
@@ -284,9 +294,18 @@
 	for (auto value : emptySets) { _sitters.erase(value); }
 }
 
-MVKTimelineSemaphoreEmulated::MVKTimelineSemaphoreEmulated(MVKDevice* device, const VkSemaphoreCreateInfo* pCreateInfo, const VkSemaphoreTypeCreateInfo* pTypeCreateInfo) :
-	MVKTimelineSemaphore(device, pCreateInfo, pTypeCreateInfo),
-	_value(pTypeCreateInfo->initialValue) {}
+MVKTimelineSemaphoreEmulated::MVKTimelineSemaphoreEmulated(MVKDevice* device,
+														   const VkSemaphoreCreateInfo* pCreateInfo,
+														   const VkSemaphoreTypeCreateInfo* pTypeCreateInfo,
+														   const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+														   const VkImportMetalSharedEventInfoEXT* pImportInfo) :
+	MVKTimelineSemaphore(device, pCreateInfo, pTypeCreateInfo, pExportInfo, pImportInfo),
+	_value(pTypeCreateInfo ? pTypeCreateInfo->initialValue : 0) {
+
+	if (pExportInfo && pExportInfo->exportObjectType == VK_EXPORT_METAL_OBJECT_TYPE_METAL_SHARED_EVENT_BIT_EXT) {
+		setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "vkCreateEvent(): MTLSharedEvent is not available on this platform."));
+	}
+}
 
 
 #pragma mark -
@@ -375,8 +394,16 @@
 	}
 }
 
-MVKEventNative::MVKEventNative(MVKDevice* device, const VkEventCreateInfo* pCreateInfo) : MVKEvent(device, pCreateInfo) {
-	_mtlEvent = [_device->getMTLDevice() newSharedEvent];	// retained
+MVKEventNative::MVKEventNative(MVKDevice* device,
+							   const VkEventCreateInfo* pCreateInfo,
+							   const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+							   const VkImportMetalSharedEventInfoEXT* pImportInfo) :
+	MVKEvent(device, pCreateInfo, pExportInfo, pImportInfo) {
+
+	// Import or create a Metal event
+	_mtlEvent = (pImportInfo
+				 ? [pImportInfo->mtlSharedEvent retain]
+				 : [device->getMTLDevice() newSharedEvent]);	//retained
 }
 
 MVKEventNative::~MVKEventNative() {
@@ -421,8 +448,16 @@
 	}
 }
 
-MVKEventEmulated::MVKEventEmulated(MVKDevice* device, const VkEventCreateInfo* pCreateInfo) :
-	MVKEvent(device, pCreateInfo), _blocker(false, 1), _inlineSignalStatus(false) {}
+MVKEventEmulated::MVKEventEmulated(MVKDevice* device,
+								   const VkEventCreateInfo* pCreateInfo,
+								   const VkExportMetalObjectCreateInfoEXT* pExportInfo,
+								   const VkImportMetalSharedEventInfoEXT* pImportInfo) :
+	MVKEvent(device, pCreateInfo, pExportInfo, pImportInfo), _blocker(false, 1), _inlineSignalStatus(false) {
+
+	if (pExportInfo && pExportInfo->exportObjectType == VK_EXPORT_METAL_OBJECT_TYPE_METAL_SHARED_EVENT_BIT_EXT) {
+		setConfigurationResult(reportError(VK_ERROR_INITIALIZATION_FAILED, "vkCreateEvent(): MTLSharedEvent is not available on this platform."));
+	}
+}
 
 
 #pragma mark -
diff --git a/MoltenVK/MoltenVK/Layers/MVKExtensions.def b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
index 8f5587d..51b08b1 100644
--- a/MoltenVK/MoltenVK/Layers/MVKExtensions.def
+++ b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
@@ -95,6 +95,7 @@
 MVK_EXTENSION(EXT_image_robustness,                EXT_IMAGE_ROBUSTNESS,                 DEVICE,   10.11,  8.0)
 MVK_EXTENSION(EXT_inline_uniform_block,            EXT_INLINE_UNIFORM_BLOCK,             DEVICE,   10.11,  8.0)
 MVK_EXTENSION(EXT_memory_budget,                   EXT_MEMORY_BUDGET,                    DEVICE,   10.13, 11.0)
+MVK_EXTENSION(EXT_metal_objects,                   EXT_METAL_OBJECTS,                    DEVICE,   10.11,  8.0)
 MVK_EXTENSION(EXT_metal_surface,                   EXT_METAL_SURFACE,                    INSTANCE, 10.11,  8.0)
 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)
diff --git a/MoltenVK/MoltenVK/Vulkan/vulkan.mm b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
index 4909577..3d6964f 100644
--- a/MoltenVK/MoltenVK/Vulkan/vulkan.mm
+++ b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
@@ -3065,6 +3065,21 @@
 	return rslt;
 }
 
+
+#pragma mark -
+#pragma mark VK_EXT_metal_objects extension
+
+MVK_PUBLIC_VULKAN_SYMBOL void vkExportMetalObjectsEXT(
+	VkDevice                                    device,
+	VkExportMetalObjectsInfoEXT*                pMetalObjectsInfo) {
+
+	MVKTraceVulkanCallStart();
+	MVKDevice* mvkDvc = MVKDevice::getMVKDevice(device);
+	mvkDvc->getMetalObjects(pMetalObjectsInfo);
+	MVKTraceVulkanCallEnd();
+}
+
+
 #pragma mark -
 #pragma mark VK_EXT_private_data extension