Add support for extension VK_EXT_headless_surface.

- Consolidate info about CAMetalLayer and headless in MVKSurface.
- MVKSwapchainImage remove getCAMetalDrawable()
  and focus on abstracting getMTLTexture().
- MVKPresentableSwapchainImage::getCAMetalDrawable() return nil if headless.
- Add MVKPresentableSwapchainImage::_mtlTextureHeadless to support
  a fixed MTLTexture that is not retrieved from a CAMetalDrawable.
- MVKPresentableSwapchainImage refactor signalling semaphores and fences.
- MVKPresentableSwapchainImage don't lock when signalling semaphores and fences.
- If no present occurs, actualPresentTime will be zero. Set it to current
  time, instead of to desiredPresentTime, since it's more accurate.
- Rework timestamps:
  - Remove _mvkTimestampBase so mvkGetTimestamp() is equal to
    mach_absolute_time(), which is used in presentation timing.
  - Add mvkGetRuntimeNanoseconds().
  - Rename mvkGetAbsoluteTime() to mvkGetContinuousNanoseconds().
  - Remove mvkGetTimestampPeriod() as unused.
- MVKSemaphoreMTLEvent::encodeDeferredSignal remove redundant nil test (unrelated).
- Fix swapchain and surface bugs when windowing system
  is accessed from off the main thread (unrelated).
- Log warning when deprecated functions vkCreateMacOSSurfaceMVK()
  or vkCreateIOSSurfaceMVK() are used (unrelated).
- Remove documentation for visionos, as support is not ready (unrelated).
diff --git a/Common/MVKOSExtensions.h b/Common/MVKOSExtensions.h
index f9faba9..2c40602 100644
--- a/Common/MVKOSExtensions.h
+++ b/Common/MVKOSExtensions.h
@@ -24,6 +24,9 @@
 #include <limits>
 
 
+#pragma mark -
+#pragma mark Operating System versions
+
 typedef float MVKOSVersion;
 
 /*** Constant indicating unsupported functionality in an OS. */
@@ -66,20 +69,31 @@
 #endif
 }
 
+
+#pragma mark -
+#pragma mark Timestamps
+
 /**
- * Returns a monotonic timestamp value for use in Vulkan and performance timestamping.
+ * Returns a monotonic tick value for use in Vulkan and performance timestamping.
  *
- * The returned value corresponds to the number of CPU "ticks" since the app was initialized.
- *
- * Calling this value twice, subtracting the first value from the second, and then multiplying
- * the result by the value returned by mvkGetTimestampPeriod() will provide an indication of the
- * number of nanoseconds between the two calls. The convenience function mvkGetElapsedMilliseconds()
- * can be used to perform this calculation.
+ * The returned value corresponds to the number of CPU ticks since an arbitrary 
+ * point in the past, and does not increment while the system is asleep.
  */
 uint64_t mvkGetTimestamp();
 
-/** Returns the number of nanoseconds between each increment of the value returned by mvkGetTimestamp(). */
-double mvkGetTimestampPeriod();
+/** 
+ * Returns the number of runtime nanoseconds since an arbitrary point in the past,
+ * excluding any time spent while the system is asleep.
+ *
+ * This value corresponds to the timestamps returned by Metal presentation timings.
+ */
+uint64_t mvkGetRuntimeNanoseconds();
+
+/**
+ * Returns the number of nanoseconds since an arbitrary point in the past,
+ * including any time spent while the system is asleep.
+ */
+uint64_t mvkGetContinuousNanoseconds();
 
 /**
  * Returns the number of nanoseconds elapsed between startTimestamp and endTimestamp,
@@ -97,12 +111,6 @@
  */
 double mvkGetElapsedMilliseconds(uint64_t startTimestamp = 0, uint64_t endTimestamp = 0);
 
-/** Returns the current absolute time in nanoseconds. */
-uint64_t mvkGetAbsoluteTime();
-
-/** Ensures the block is executed on the main thread. */
-void mvkDispatchToMainAndWait(dispatch_block_t block);
-
 
 #pragma mark -
 #pragma mark Process environment
@@ -141,8 +149,12 @@
 /** Returns the size of a page of host memory on this platform. */
 uint64_t mvkGetHostMemoryPageSize();
 
+
 #pragma mark -
 #pragma mark Threading
 
 /** Returns the amount of avaliable CPU cores. */
 uint32_t mvkGetAvaliableCPUCores();
+
+/** Ensures the block is executed on the main thread. */
+void mvkDispatchToMainAndWait(dispatch_block_t block);
diff --git a/Common/MVKOSExtensions.mm b/Common/MVKOSExtensions.mm
index 93025f2..8d33f3d 100644
--- a/Common/MVKOSExtensions.mm
+++ b/Common/MVKOSExtensions.mm
@@ -29,6 +29,10 @@
 
 using namespace std;
 
+
+#pragma mark -
+#pragma mark Operating System versions
+
 MVKOSVersion mvkOSVersion() {
 	static MVKOSVersion _mvkOSVersion = 0;
 	if ( !_mvkOSVersion ) {
@@ -38,43 +42,35 @@
 	return _mvkOSVersion;
 }
 
-static uint64_t _mvkTimestampBase;
-static double _mvkTimestampPeriod;
+
+#pragma mark -
+#pragma mark Timestamps
+
 static mach_timebase_info_data_t _mvkMachTimebase;
 
-uint64_t mvkGetTimestamp() { return mach_absolute_time() - _mvkTimestampBase; }
+uint64_t mvkGetTimestamp() { return mach_absolute_time(); }
 
-double mvkGetTimestampPeriod() { return _mvkTimestampPeriod; }
+uint64_t mvkGetRuntimeNanoseconds() { return mach_absolute_time() * _mvkMachTimebase.numer / _mvkMachTimebase.denom; }
+
+uint64_t mvkGetContinuousNanoseconds() { return mach_continuous_time() * _mvkMachTimebase.numer / _mvkMachTimebase.denom; }
 
 uint64_t mvkGetElapsedNanoseconds(uint64_t startTimestamp, uint64_t endTimestamp) {
 	if (endTimestamp == 0) { endTimestamp = mvkGetTimestamp(); }
-	return (endTimestamp - startTimestamp) * _mvkTimestampPeriod;
+	return (endTimestamp - startTimestamp) * _mvkMachTimebase.numer / _mvkMachTimebase.denom;
 }
 
 double mvkGetElapsedMilliseconds(uint64_t startTimestamp, uint64_t endTimestamp) {
 	return mvkGetElapsedNanoseconds(startTimestamp, endTimestamp) / 1e6;
 }
 
-uint64_t mvkGetAbsoluteTime() { return mach_continuous_time() * _mvkMachTimebase.numer / _mvkMachTimebase.denom; }
-
-// Initialize timestamping capabilities on app startup.
-//Called automatically when the framework is loaded and initialized.
+// Initialize timestamp capabilities on app startup.
+// Called automatically when the framework is loaded and initialized.
 static bool _mvkTimestampsInitialized = false;
 __attribute__((constructor)) static void MVKInitTimestamps() {
 	if (_mvkTimestampsInitialized ) { return; }
 	_mvkTimestampsInitialized = true;
 
-	_mvkTimestampBase = mach_absolute_time();
 	mach_timebase_info(&_mvkMachTimebase);
-	_mvkTimestampPeriod = (double)_mvkMachTimebase.numer / (double)_mvkMachTimebase.denom;
-}
-
-void mvkDispatchToMainAndWait(dispatch_block_t block) {
-	if (NSThread.isMainThread) {
-		block();
-	} else {
-		dispatch_sync(dispatch_get_main_queue(), block);
-	}
 }
 
 
@@ -145,6 +141,7 @@
 
 uint64_t mvkGetHostMemoryPageSize() { return sysconf(_SC_PAGESIZE); }
 
+
 #pragma mark -
 #pragma mark Threading
 
@@ -152,3 +149,11 @@
 uint32_t mvkGetAvaliableCPUCores() {
     return (uint32_t)[[NSProcessInfo processInfo] activeProcessorCount];
 }
+
+void mvkDispatchToMainAndWait(dispatch_block_t block) {
+	if (NSThread.isMainThread) {
+		block();
+	} else {
+		dispatch_sync(dispatch_get_main_queue(), block);
+	}
+}
diff --git a/Docs/MoltenVK_Runtime_UserGuide.md b/Docs/MoltenVK_Runtime_UserGuide.md
index 9b00360..ee93c80 100644
--- a/Docs/MoltenVK_Runtime_UserGuide.md
+++ b/Docs/MoltenVK_Runtime_UserGuide.md
@@ -380,6 +380,9 @@
 - `VK_EXT_external_memory_host`
 - `VK_EXT_fragment_shader_interlock`
   - *Requires Metal 2.0 and Raster Order Groups.*
+- `VK_EXT_hdr_metadata`
+  - *macOS only.*
+- `VK_EXT_headless_surface`
 - `VK_EXT_host_query_reset`
 - `VK_EXT_image_robustness`
 - `VK_EXT_inline_uniform_block`
diff --git a/Docs/Whats_New.md b/Docs/Whats_New.md
index e0abbf7..ebafdde 100644
--- a/Docs/Whats_New.md
+++ b/Docs/Whats_New.md
@@ -20,11 +20,13 @@
 
 - Add support for extensions:
 	- `VK_EXT_extended_dynamic_state3` *(Metal does not support `VK_POLYGON_MODE_POINT`)*
+	- `VK_EXT_headless_surface`
 - 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.
 - Fix rare deadlock during launch via `dlopen()`.
 - Fix initial value of `VkPhysicalDeviceLimits::timestampPeriod` on non-Apple Silicon GPUs.
+- Fix swapchain and surface bugs when windowing system is accessed from off the main thread.
 - Update to latest SPIRV-Cross:
   - MSL: Fix regression error in argument buffer runtime arrays.
 
diff --git a/MoltenVK/MoltenVK.xcodeproj/project.pbxproj b/MoltenVK/MoltenVK.xcodeproj/project.pbxproj
index 46ff50f..be7ca32 100644
--- a/MoltenVK/MoltenVK.xcodeproj/project.pbxproj
+++ b/MoltenVK/MoltenVK.xcodeproj/project.pbxproj
@@ -117,7 +117,7 @@
 		2FEA0AAF24902F9F00EEF3AD /* MVKLayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = A94FB7A11C7DFB4800632CA3 /* MVKLayers.mm */; };
 		2FEA0AB024902F9F00EEF3AD /* MVKFramebuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = A94FB7881C7DFB4800632CA3 /* MVKFramebuffer.mm */; };
 		2FEA0AB124902F9F00EEF3AD /* MVKMTLBufferAllocation.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9C96DCF1DDC20C20053187F /* MVKMTLBufferAllocation.mm */; };
-		2FEA0AB224902F9F00EEF3AD /* CAMetalLayer+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.m */; };
+		2FEA0AB224902F9F00EEF3AD /* CAMetalLayer+MoltenVK.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.mm */; };
 		2FEA0AB324902F9F00EEF3AD /* MVKCmdDispatch.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9096E5D1F81E16300DFBEA6 /* MVKCmdDispatch.mm */; };
 		2FEA0AB424902F9F00EEF3AD /* MVKCmdDebug.mm in Sources */ = {isa = PBXBuildFile; fileRef = A99C90ED229455B300A061DA /* MVKCmdDebug.mm */; };
 		45003E73214AD4E500E989CB /* MVKExtensions.def in Headers */ = {isa = PBXBuildFile; fileRef = 45003E6F214AD4C900E989CB /* MVKExtensions.def */; };
@@ -360,8 +360,8 @@
 		A9E53DE62100B197002781DD /* NSString+MoltenVK.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD42100B197002781DD /* NSString+MoltenVK.mm */; };
 		A9E53DE72100B197002781DD /* MTLTextureDescriptor+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD52100B197002781DD /* MTLTextureDescriptor+MoltenVK.m */; };
 		A9E53DE82100B197002781DD /* MTLTextureDescriptor+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD52100B197002781DD /* MTLTextureDescriptor+MoltenVK.m */; };
-		A9E53DE92100B197002781DD /* CAMetalLayer+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.m */; };
-		A9E53DEA2100B197002781DD /* CAMetalLayer+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.m */; };
+		A9E53DE92100B197002781DD /* CAMetalLayer+MoltenVK.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.mm */; };
+		A9E53DEA2100B197002781DD /* CAMetalLayer+MoltenVK.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.mm */; };
 		A9E53DF32100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.h in Headers */ = {isa = PBXBuildFile; fileRef = A9E53DEE2100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.h */; };
 		A9E53DF42100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.h in Headers */ = {isa = PBXBuildFile; fileRef = A9E53DEE2100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.h */; };
 		A9E53DF52100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DF22100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.m */; };
@@ -495,7 +495,7 @@
 		DCFD7F572A45BC6E007BBBF7 /* MVKFramebuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = A94FB7881C7DFB4800632CA3 /* MVKFramebuffer.mm */; };
 		DCFD7F582A45BC6E007BBBF7 /* MTLRenderPassStencilAttachmentDescriptor+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = 453638302508A4C6000EFFD3 /* MTLRenderPassStencilAttachmentDescriptor+MoltenVK.m */; };
 		DCFD7F592A45BC6E007BBBF7 /* MVKMTLBufferAllocation.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9C96DCF1DDC20C20053187F /* MVKMTLBufferAllocation.mm */; };
-		DCFD7F5A2A45BC6E007BBBF7 /* CAMetalLayer+MoltenVK.m in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.m */; };
+		DCFD7F5A2A45BC6E007BBBF7 /* CAMetalLayer+MoltenVK.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.mm */; };
 		DCFD7F5B2A45BC6E007BBBF7 /* MVKCmdDispatch.mm in Sources */ = {isa = PBXBuildFile; fileRef = A9096E5D1F81E16300DFBEA6 /* MVKCmdDispatch.mm */; };
 		DCFD7F5C2A45BC6E007BBBF7 /* MVKCmdDebug.mm in Sources */ = {isa = PBXBuildFile; fileRef = A99C90ED229455B300A061DA /* MVKCmdDebug.mm */; };
 /* End PBXBuildFile section */
@@ -691,7 +691,7 @@
 		A9E53DD32100B197002781DD /* MTLSamplerDescriptor+MoltenVK.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MTLSamplerDescriptor+MoltenVK.h"; sourceTree = "<group>"; };
 		A9E53DD42100B197002781DD /* NSString+MoltenVK.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSString+MoltenVK.mm"; sourceTree = "<group>"; };
 		A9E53DD52100B197002781DD /* MTLTextureDescriptor+MoltenVK.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MTLTextureDescriptor+MoltenVK.m"; sourceTree = "<group>"; };
-		A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CAMetalLayer+MoltenVK.m"; sourceTree = "<group>"; };
+		A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "CAMetalLayer+MoltenVK.mm"; sourceTree = "<group>"; };
 		A9E53DEE2100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MTLRenderPassDescriptor+MoltenVK.h"; sourceTree = "<group>"; };
 		A9E53DF22100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MTLRenderPassDescriptor+MoltenVK.m"; sourceTree = "<group>"; };
 		A9E53DFA21064F84002781DD /* MTLRenderPipelineDescriptor+MoltenVK.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MTLRenderPipelineDescriptor+MoltenVK.m"; sourceTree = "<group>"; };
@@ -889,7 +889,7 @@
 			isa = PBXGroup;
 			children = (
 				A9E53DD12100B197002781DD /* CAMetalLayer+MoltenVK.h */,
-				A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.m */,
+				A9E53DD62100B197002781DD /* CAMetalLayer+MoltenVK.mm */,
 				453638312508A4C7000EFFD3 /* MTLRenderPassDepthAttachmentDescriptor+MoltenVK.h */,
 				4536382F2508A4C6000EFFD3 /* MTLRenderPassDepthAttachmentDescriptor+MoltenVK.m */,
 				A9E53DEE2100B302002781DD /* MTLRenderPassDescriptor+MoltenVK.h */,
@@ -1703,7 +1703,7 @@
 				2FEA0AAF24902F9F00EEF3AD /* MVKLayers.mm in Sources */,
 				2FEA0AB024902F9F00EEF3AD /* MVKFramebuffer.mm in Sources */,
 				2FEA0AB124902F9F00EEF3AD /* MVKMTLBufferAllocation.mm in Sources */,
-				2FEA0AB224902F9F00EEF3AD /* CAMetalLayer+MoltenVK.m in Sources */,
+				2FEA0AB224902F9F00EEF3AD /* CAMetalLayer+MoltenVK.mm in Sources */,
 				2FEA0AB324902F9F00EEF3AD /* MVKCmdDispatch.mm in Sources */,
 				2FEA0AB424902F9F00EEF3AD /* MVKCmdDebug.mm in Sources */,
 			);
@@ -1763,7 +1763,7 @@
 				A94FB7EE1C7DFB4800632CA3 /* MVKFramebuffer.mm in Sources */,
 				453638382508A4C7000EFFD3 /* MTLRenderPassStencilAttachmentDescriptor+MoltenVK.m in Sources */,
 				A9C96DD21DDC20C20053187F /* MVKMTLBufferAllocation.mm in Sources */,
-				A9E53DE92100B197002781DD /* CAMetalLayer+MoltenVK.m in Sources */,
+				A9E53DE92100B197002781DD /* CAMetalLayer+MoltenVK.mm in Sources */,
 				A9096E5E1F81E16300DFBEA6 /* MVKCmdDispatch.mm in Sources */,
 				A99C90F0229455B300A061DA /* MVKCmdDebug.mm in Sources */,
 			);
@@ -1823,7 +1823,7 @@
 				A94FB7EF1C7DFB4800632CA3 /* MVKFramebuffer.mm in Sources */,
 				4536383A2508A4C7000EFFD3 /* MTLRenderPassStencilAttachmentDescriptor+MoltenVK.m in Sources */,
 				A9C96DD31DDC20C20053187F /* MVKMTLBufferAllocation.mm in Sources */,
-				A9E53DEA2100B197002781DD /* CAMetalLayer+MoltenVK.m in Sources */,
+				A9E53DEA2100B197002781DD /* CAMetalLayer+MoltenVK.mm in Sources */,
 				A9096E5F1F81E16300DFBEA6 /* MVKCmdDispatch.mm in Sources */,
 				A99C90F1229455B300A061DA /* MVKCmdDebug.mm in Sources */,
 			);
@@ -1883,7 +1883,7 @@
 				DCFD7F572A45BC6E007BBBF7 /* MVKFramebuffer.mm in Sources */,
 				DCFD7F582A45BC6E007BBBF7 /* MTLRenderPassStencilAttachmentDescriptor+MoltenVK.m in Sources */,
 				DCFD7F592A45BC6E007BBBF7 /* MVKMTLBufferAllocation.mm in Sources */,
-				DCFD7F5A2A45BC6E007BBBF7 /* CAMetalLayer+MoltenVK.m in Sources */,
+				DCFD7F5A2A45BC6E007BBBF7 /* CAMetalLayer+MoltenVK.mm in Sources */,
 				DCFD7F5B2A45BC6E007BBBF7 /* MVKCmdDispatch.mm in Sources */,
 				DCFD7F5C2A45BC6E007BBBF7 /* MVKCmdDebug.mm in Sources */,
 			);
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
index cbbbfab..401c880 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
@@ -1204,8 +1204,8 @@
     isHeadless = getMTLDevice().isHeadless;
 #endif
     
-	// If this device is headless or the surface does not have a CAMetalLayer, it is not supported.
-    *pSupported = !(isHeadless || (surface->getCAMetalLayer() == nil));
+	// If this device is headless, the surface must be headless.
+	*pSupported = isHeadless ? surface->isHeadless() : wasConfigurationSuccessful();
 	return *pSupported ? VK_SUCCESS : surface->getConfigurationResult();
 }
 
@@ -1264,13 +1264,12 @@
 
 	// The CAlayer underlying the surface must be a CAMetalLayer.
 	MVKSurface* surface = (MVKSurface*)pSurfaceInfo->surface;
-	CAMetalLayer* mtlLayer = surface->getCAMetalLayer();
-	if ( !mtlLayer ) { return surface->getConfigurationResult(); }
+	if ( !surface->wasConfigurationSuccessful() ) { return surface->getConfigurationResult(); }
 
 	VkSurfaceCapabilitiesKHR& surfCaps = pSurfaceCapabilities->surfaceCapabilities;
 	surfCaps.minImageCount = _metalFeatures.minSwapchainImageCount;
 	surfCaps.maxImageCount = _metalFeatures.maxSwapchainImageCount;
-	surfCaps.currentExtent = mvkGetNaturalExtent(mtlLayer);
+	surfCaps.currentExtent = surface->getNaturalExtent();
 	surfCaps.minImageExtent = { 1, 1 };
 	surfCaps.maxImageExtent = { _properties.limits.maxImageDimension2D, _properties.limits.maxImageDimension2D };
 	surfCaps.maxImageArrayLayers = 1;
@@ -1349,9 +1348,7 @@
 											  uint32_t* pCount,
 											  VkSurfaceFormatKHR* pSurfaceFormats) {
 
-	// The layer underlying the surface view must be a CAMetalLayer.
-	CAMetalLayer* mtlLayer = surface->getCAMetalLayer();
-	if ( !mtlLayer ) { return surface->getConfigurationResult(); }
+	if ( !surface->wasConfigurationSuccessful() ) { return surface->getConfigurationResult(); }
 
 #define addSurfFmt(MTL_FMT) \
 	do { \
@@ -1474,9 +1471,7 @@
 												   uint32_t* pCount,
 												   VkPresentModeKHR* pPresentModes) {
 
-	// The layer underlying the surface view must be a CAMetalLayer.
-	CAMetalLayer* mtlLayer = surface->getCAMetalLayer();
-	if ( !mtlLayer ) { return surface->getConfigurationResult(); }
+	if ( !surface->wasConfigurationSuccessful() ) { return surface->getConfigurationResult(); }
 
 #define ADD_VK_PRESENT_MODE(VK_PM)																	\
 	do {																							\
@@ -1504,9 +1499,7 @@
 												 uint32_t* pRectCount,
 												 VkRect2D* pRects) {
 
-	// The layer underlying the surface view must be a CAMetalLayer.
-	CAMetalLayer* mtlLayer = surface->getCAMetalLayer();
-	if ( !mtlLayer ) { return surface->getConfigurationResult(); }
+	if ( !surface->wasConfigurationSuccessful() ) { return surface->getConfigurationResult(); }
 
 	if ( !pRects ) {
 		*pRectCount = 1;
@@ -1518,7 +1511,7 @@
 	*pRectCount = 1;
 
 	pRects[0].offset = { 0, 0 };
-	pRects[0].extent = mvkGetNaturalExtent(mtlLayer);
+	pRects[0].extent = surface->getNaturalExtent();
 
 	return VK_SUCCESS;
 }
@@ -3666,14 +3659,14 @@
 	MTLTimestamp cpuStamp, gpuStamp;
 	uint64_t cpuStart, cpuEnd;
 
-	cpuStart = mvkGetAbsoluteTime();
+	cpuStart = mvkGetContinuousNanoseconds();
 	[getMTLDevice() sampleTimestamps: &cpuStamp gpuTimestamp: &gpuStamp];
 	// Sample again to calculate the maximum deviation. Note that the
 	// -[MTLDevice sampleTimestamps:gpuTimestamp:] method guarantees that CPU
 	// timestamps are in nanoseconds. We don't want to call the method again,
 	// because that could result in an expensive syscall to query the GPU time-
 	// stamp.
-	cpuEnd = mvkGetAbsoluteTime();
+	cpuEnd = mvkGetContinuousNanoseconds();
 	for (uint32_t tsIdx = 0; tsIdx < timestampCount; ++tsIdx) {
 		switch (pTimestampInfos[tsIdx].timeDomain) {
 			case VK_TIME_DOMAIN_DEVICE_EXT:
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h
index 900b10f..058876d 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h
@@ -378,14 +378,8 @@
 
 public:
 
-	/** Binds this resource to the specified offset within the specified memory allocation. */
 	VkResult bindDeviceMemory(MVKDeviceMemory* mvkMem, VkDeviceSize memOffset, uint8_t planeIndex) override;
 
-#pragma mark Metal
-
-	/** Returns the Metal texture used by the CAMetalDrawable underlying this image. */
-	id<MTLTexture> getMTLTexture(uint8_t planeIndex) override;
-
 
 #pragma mark Construction
 
@@ -399,7 +393,6 @@
 protected:
 	friend class MVKPeerSwapchainImage;
 
-	virtual id<CAMetalDrawable> getCAMetalDrawable() = 0;
 	void detachSwapchain();
 
 	std::mutex _detachmentLock;
@@ -445,6 +438,8 @@
 
 #pragma mark Metal
 
+	id<MTLTexture> getMTLTexture(uint8_t planeIndex) override;
+
 	/** Presents the contained drawable to the OS. */
 	VkResult presentCAMetalDrawable(id<MTLCommandBuffer> mtlCmdBuff, MVKImagePresentInfo presentInfo);
 
@@ -468,16 +463,16 @@
 protected:
 	friend MVKSwapchain;
 
-	id<CAMetalDrawable> getCAMetalDrawable() override;
+	id<CAMetalDrawable> getCAMetalDrawable();
 	void addPresentedHandler(id<CAMetalDrawable> mtlDrawable, MVKImagePresentInfo presentInfo, MVKSwapchainSignaler signaler);
 	void releaseMetalDrawable();
 	MVKSwapchainImageAvailability getAvailability();
-	void makeAvailable(const MVKSwapchainSignaler& signaler);
 	void makeAvailable();
 	VkResult acquireAndSignalWhenAvailable(MVKSemaphore* semaphore, MVKFence* fence);
 	MVKSwapchainSignaler getPresentationSignaler();
 
 	id<CAMetalDrawable> _mtlDrawable = nil;
+	id<MTLTexture> _mtlTextureHeadless = nil;
 	MVKSwapchainImageAvailability _availability;
 	MVKSmallVector<MVKSwapchainSignaler, 1> _availabilitySignalers;
 	MVKSwapchainSignaler _preSignaler = {};
@@ -494,7 +489,8 @@
 
 public:
 
-	/** Binds this resource according to the specified bind information. */
+	id<MTLTexture> getMTLTexture(uint8_t planeIndex) override;
+
 	VkResult bindDeviceMemory2(const VkBindImageMemoryInfo* pBindInfo) override;
 
 
@@ -504,10 +500,6 @@
 						  const VkImageCreateInfo* pCreateInfo,
 						  MVKSwapchain* swapchain,
 						  uint32_t swapchainIndex);
-
-protected:
-	id<CAMetalDrawable> getCAMetalDrawable() override;
-
 };
 
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
index 323918f..1c132a4 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
@@ -25,8 +25,10 @@
 #include "MVKFoundation.h"
 #include "MVKOSExtensions.h"
 #include "MVKCodec.h"
+
 #import "MTLTextureDescriptor+MoltenVK.h"
 #import "MTLSamplerDescriptor+MoltenVK.h"
+#import "CAMetalLayer+MoltenVK.h"
 
 using namespace std;
 using namespace SPIRV_CROSS_NAMESPACE;
@@ -1169,12 +1171,6 @@
 }
 
 
-#pragma mark Metal
-
-// Overridden to always retrieve the MTLTexture directly from the CAMetalDrawable.
-id<MTLTexture> MVKSwapchainImage::getMTLTexture(uint8_t planeIndex) { return [getCAMetalDrawable() texture]; }
-
-
 #pragma mark Construction
 
 MVKSwapchainImage::MVKSwapchainImage(MVKDevice* device,
@@ -1212,39 +1208,33 @@
 	return _availability;
 }
 
-// If present, signal the semaphore for the first waiter for the given image.
-static void signalPresentationSemaphore(const MVKSwapchainSignaler& signaler, id<MTLCommandBuffer> mtlCmdBuff) {
-	if (signaler.semaphore) { signaler.semaphore->encodeDeferredSignal(mtlCmdBuff, signaler.semaphoreSignalToken); }
-}
-
-// Signal either or both of the semaphore and fence in the specified tracker pair.
-static void signal(const MVKSwapchainSignaler& signaler, id<MTLCommandBuffer> mtlCmdBuff) {
-	if (signaler.semaphore) { signaler.semaphore->encodeDeferredSignal(mtlCmdBuff, signaler.semaphoreSignalToken); }
-	if (signaler.fence) { signaler.fence->signal(); }
-}
-
 // Tell the semaphore and fence that they are being tracked for future signaling.
-static void markAsTracked(const MVKSwapchainSignaler& signaler) {
+static void track(const MVKSwapchainSignaler& signaler) {
 	if (signaler.semaphore) { signaler.semaphore->retain(); }
 	if (signaler.fence) { signaler.fence->retain(); }
 }
 
-// Tell the semaphore and fence that they are no longer being tracked for future signaling.
-static void unmarkAsTracked(const MVKSwapchainSignaler& signaler) {
-	if (signaler.semaphore) { signaler.semaphore->release(); }
-	if (signaler.fence) { signaler.fence->release(); }
+static void signal(MVKSemaphore* semaphore, uint64_t semaphoreSignalToken, id<MTLCommandBuffer> mtlCmdBuff) {
+	if (semaphore) { semaphore->encodeDeferredSignal(mtlCmdBuff, semaphoreSignalToken); }
 }
 
-static void signalAndUnmarkAsTracked(const MVKSwapchainSignaler& signaler) {
-	signal(signaler, nil);
-	unmarkAsTracked(signaler);
+static void signal(MVKFence* fence) {
+	if (fence) { fence->signal(); }
+}
+
+// Signal the semaphore and fence and tell them that they are no longer being tracked for future signaling.
+static void signalAndUntrack(const MVKSwapchainSignaler& signaler) {
+	signal(signaler.semaphore, signaler.semaphoreSignalToken, nil);
+	if (signaler.semaphore) { signaler.semaphore->release(); }
+
+	signal(signaler.fence);
+	if (signaler.fence) { signaler.fence->release(); }
 }
 
 VkResult MVKPresentableSwapchainImage::acquireAndSignalWhenAvailable(MVKSemaphore* semaphore, MVKFence* fence) {
 
 	// Now that this image is being acquired, release the existing drawable and its texture.
 	// This is not done earlier so the texture is retained for any post-processing such as screen captures, etc.
-	// This may trigger a delayed presentation callback, which uses the _availabilityLock, also used below.
 	releaseMetalDrawable();
 
 	lock_guard<mutex> lock(_availabilityLock);
@@ -1267,7 +1257,8 @@
 				mtlCmdBuff = _device->getAnyQueue()->getMTLCommandBuffer(kMVKCommandUseAcquireNextImage);
 				if ( !mtlCmdBuff ) { setConfigurationResult(VK_ERROR_OUT_OF_POOL_MEMORY); }
 			}
-			signal(signaler, mtlCmdBuff);
+			signal(signaler.semaphore, signaler.semaphoreSignalToken, mtlCmdBuff);
+			signal(signaler.fence);
 			[mtlCmdBuff commit];
 		}
 
@@ -1275,7 +1266,7 @@
 	} else {
 		_availabilitySignalers.push_back(signaler);
 	}
-	markAsTracked(signaler);
+	track(signaler);
 
 	return getConfigurationResult();
 }
@@ -1284,6 +1275,9 @@
 // Attempt several times to retrieve a good drawable, and set an error to trigger the
 // swapchain to be re-established if one cannot be retrieved.
 id<CAMetalDrawable> MVKPresentableSwapchainImage::getCAMetalDrawable() {
+
+	if (_mtlTextureHeadless) { return nil; }	// If headless, there is no drawable.
+
 	if ( !_mtlDrawable ) {
 		@autoreleasepool {
 			bool hasInvalidFormat = false;
@@ -1305,6 +1299,11 @@
 	return _mtlDrawable;
 }
 
+// If not headless, retrieve the MTLTexture directly from the CAMetalDrawable.
+id<MTLTexture> MVKPresentableSwapchainImage::getMTLTexture(uint8_t planeIndex) {
+	return _mtlTextureHeadless ? _mtlTextureHeadless : getCAMetalDrawable().texture;
+}
+
 // Present the drawable and make myself available only once the command buffer has completed.
 // Pass MVKImagePresentInfo by value because it may not exist when the callback runs.
 VkResult MVKPresentableSwapchainImage::presentCAMetalDrawable(id<MTLCommandBuffer> mtlCmdBuff,
@@ -1343,15 +1342,13 @@
 	auto* fence = presentInfo.fence;
 	if (fence) { fence->retain(); }
 	[mtlCmdBuff addCompletedHandler: ^(id<MTLCommandBuffer> mcb) {
-		if (fence) {
-			fence->signal();
-			fence->release();
-		}
+		signal(fence);
+		if (fence) { fence->release(); }
 		[mtlDrwbl release];
 		release();
 	}];
 
-	signalPresentationSemaphore(signaler, mtlCmdBuff);
+	signal(signaler.semaphore, signaler.semaphoreSignalToken, mtlCmdBuff);
 
 	return getConfigurationResult();
 }
@@ -1408,6 +1405,13 @@
 void MVKPresentableSwapchainImage::endPresentation(const MVKImagePresentInfo& presentInfo,
 												   const MVKSwapchainSignaler& signaler,
 												   uint64_t actualPresentTime) {
+
+	// If the presentation time is not available, use the current nanosecond runtime clock,
+	// which should be reasonably accurate (sub-ms) to the presentation time. The presentation
+	// time will not be available if the presentation did not actually happen, such as when
+	// running headless, or on a test harness that is not attached to the windowing system.
+	if (actualPresentTime == 0) { actualPresentTime = mvkGetRuntimeNanoseconds(); }
+
 	{	// Scope to avoid deadlock if release() is run within detachment lock
 		// If I have become detached from the swapchain, it means the swapchain, and possibly the
 		// VkDevice, have been destroyed by the time of this callback, so do not reference them.
@@ -1415,7 +1419,11 @@
 		if (_device) { _device->addPerformanceInterval(_device->_performanceStatistics.queue.presentSwapchains, _presentationStartTime); }
 		if (_swapchain) { _swapchain->endPresentation(presentInfo, actualPresentTime); }
 	}
-	makeAvailable(signaler);
+
+	// Makes an image available for acquisition by the app.
+	// If any semaphores are waiting to be signaled when this image becomes available, the
+	// earliest semaphore is signaled, and this image remains unavailable for other uses.
+	signalAndUntrack(signaler);
 	release();
 }
 
@@ -1425,15 +1433,6 @@
 	_mtlDrawable = nil;
 }
 
-// Makes an image available for acquisition by the app.
-// If any semaphores are waiting to be signaled when this image becomes available, the
-// earliest semaphore is signaled, and this image remains unavailable for other uses.
-void MVKPresentableSwapchainImage::makeAvailable(const MVKSwapchainSignaler& signaler) {
-	lock_guard<mutex> lock(_availabilityLock);
-
-	signalAndUnmarkAsTracked(signaler);
-}
-
 // Signal, untrack, and release any signalers that are tracking.
 // Release the drawable before the lock, as it may trigger completion callback.
 void MVKPresentableSwapchainImage::makeAvailable() {
@@ -1441,9 +1440,9 @@
 	lock_guard<mutex> lock(_availabilityLock);
 
 	if ( !_availability.isAvailable ) {
-		signalAndUnmarkAsTracked(_preSignaler);
+		signalAndUntrack(_preSignaler);
 		for (auto& sig : _availabilitySignalers) {
-			signalAndUnmarkAsTracked(sig);
+			signalAndUntrack(sig);
 		}
 		_availabilitySignalers.clear();
 		_availability.isAvailable = true;
@@ -1460,11 +1459,26 @@
 
 	_availability.acquisitionID = _swapchain->getNextAcquisitionID();
 	_availability.isAvailable = true;
+
+	if (swapchain->isHeadless()) {
+		@autoreleasepool {
+			MTLTextureDescriptor* mtlTexDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: getMTLPixelFormat()
+																								  width: pCreateInfo->extent.width
+																								 height: pCreateInfo->extent.height
+																							  mipmapped: NO];
+			mtlTexDesc.usageMVK = MTLTextureUsageRenderTarget;
+			mtlTexDesc.storageModeMVK = MTLStorageModePrivate;
+
+			_mtlTextureHeadless = [[getMTLDevice() newTextureWithDescriptor: mtlTexDesc] retain];	// retained
+		}
+	}
 }
 
 
 void MVKPresentableSwapchainImage::destroy() {
 	releaseMetalDrawable();
+	[_mtlTextureHeadless release];
+	_mtlTextureHeadless = nil;
 	MVKSwapchainImage::destroy();
 }
 
@@ -1498,8 +1512,8 @@
 
 #pragma mark Metal
 
-id<CAMetalDrawable> MVKPeerSwapchainImage::getCAMetalDrawable() {
-	return ((MVKSwapchainImage*)_swapchain->getPresentableImage(_swapchainIndex))->getCAMetalDrawable();
+id<MTLTexture> MVKPeerSwapchainImage::getMTLTexture(uint8_t planeIndex) { 
+	return ((MVKSwapchainImage*)_swapchain->getPresentableImage(_swapchainIndex))->getMTLTexture(planeIndex);
 }
 
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.h b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.h
index f6ca8e7..6a2fa92 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.h
@@ -117,6 +117,9 @@
 	MVKSurface* createSurface(const VkMetalSurfaceCreateInfoEXT* pCreateInfo,
 							  const VkAllocationCallbacks* pAllocator);
 
+	MVKSurface* createSurface(const VkHeadlessSurfaceCreateInfoEXT* pCreateInfo,
+							  const VkAllocationCallbacks* pAllocator);
+
 	MVKSurface* createSurface(const Vk_PLATFORM_SurfaceCreateInfoMVK* pCreateInfo,
 							  const VkAllocationCallbacks* pAllocator);
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
index 543a1fe..de9ad02 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKInstance.mm
@@ -102,6 +102,11 @@
 	return new MVKSurface(this, pCreateInfo, pAllocator);
 }
 
+MVKSurface* MVKInstance::createSurface(const VkHeadlessSurfaceCreateInfoEXT* pCreateInfo,
+									   const VkAllocationCallbacks* pAllocator) {
+	return new MVKSurface(this, pCreateInfo, pAllocator);
+}
+
 MVKSurface* MVKInstance::createSurface(const Vk_PLATFORM_SurfaceCreateInfoMVK* pCreateInfo,
 									   const VkAllocationCallbacks* pAllocator) {
 	return new MVKSurface(this, pCreateInfo, pAllocator);
@@ -426,6 +431,8 @@
 	ADD_INST_EXT_ENTRY_POINT(vkGetPhysicalDeviceSurfacePresentModesKHR, KHR_SURFACE);
 	ADD_INST_EXT_ENTRY_POINT(vkGetPhysicalDeviceSurfaceCapabilities2KHR, KHR_GET_SURFACE_CAPABILITIES_2);
 	ADD_INST_EXT_ENTRY_POINT(vkGetPhysicalDeviceSurfaceFormats2KHR, KHR_GET_SURFACE_CAPABILITIES_2);
+	ADD_INST_EXT_ENTRY_POINT(vkCreateHeadlessSurfaceEXT, EXT_HEADLESS_SURFACE);
+	ADD_INST_EXT_ENTRY_POINT(vkCreateMetalSurfaceEXT, EXT_METAL_SURFACE);
 	ADD_INST_EXT_ENTRY_POINT(vkCreateDebugReportCallbackEXT, EXT_DEBUG_REPORT);
 	ADD_INST_EXT_ENTRY_POINT(vkDestroyDebugReportCallbackEXT, EXT_DEBUG_REPORT);
 	ADD_INST_EXT_ENTRY_POINT(vkDebugReportMessageEXT, EXT_DEBUG_REPORT);
@@ -441,7 +448,6 @@
 	ADD_INST_EXT_ENTRY_POINT(vkCreateDebugUtilsMessengerEXT, EXT_DEBUG_UTILS);
 	ADD_INST_EXT_ENTRY_POINT(vkDestroyDebugUtilsMessengerEXT, EXT_DEBUG_UTILS);
 	ADD_INST_EXT_ENTRY_POINT(vkSubmitDebugUtilsMessageEXT, EXT_DEBUG_UTILS);
-	ADD_INST_EXT_ENTRY_POINT(vkCreateMetalSurfaceEXT, EXT_METAL_SURFACE);
 
 #ifdef VK_USE_PLATFORM_IOS_MVK
 	ADD_INST_EXT_ENTRY_POINT(vkCreateIOSSurfaceMVK, MVK_IOS_SURFACE);
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h b/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h
index 479965b..5d23225 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h
@@ -20,7 +20,7 @@
 
 #include "MVKBaseObject.h"
 #include "MVKOSExtensions.h"
-#include "mvk_datatypes.h"
+#include "mvk_datatypes.hpp"
 #include <spirv_msl.hpp>
 #include <unordered_map>
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h
index 5746bfb..453eac6 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.h
@@ -24,16 +24,6 @@
 #import <Metal/Metal.h>
 #import <QuartzCore/CAMetalLayer.h>
 
-#ifdef VK_USE_PLATFORM_IOS_MVK
-#	define PLATFORM_VIEW_CLASS	UIView
-#	import <UIKit/UIView.h>
-#endif
-
-#ifdef VK_USE_PLATFORM_MACOS_MVK
-#	define PLATFORM_VIEW_CLASS	NSView
-#	import <AppKit/NSView.h>
-#endif
-
 class MVKInstance;
 class MVKSwapchain;
 
@@ -59,6 +49,14 @@
     /** Returns the CAMetalLayer underlying this surface. */
 	CAMetalLayer* getCAMetalLayer();
 
+	/** Returns the extent of this surface. */
+	VkExtent2D getExtent();
+
+	/** Returns the extent for which the underlying CAMetalLayer will not need to be scaled when composited. */
+	VkExtent2D getNaturalExtent();
+
+	/** Returns whether this surface is headless. */
+	bool isHeadless() { return !_mtlCAMetalLayer && wasConfigurationSuccessful(); }
 
 #pragma mark Construction
 
@@ -67,6 +65,10 @@
 			   const VkAllocationCallbacks* pAllocator);
 
 	MVKSurface(MVKInstance* mvkInstance,
+			   const VkHeadlessSurfaceCreateInfoEXT* pCreateInfo,
+			   const VkAllocationCallbacks* pAllocator);
+
+	MVKSurface(MVKInstance* mvkInstance,
 			   const Vk_PLATFORM_SurfaceCreateInfoMVK* pCreateInfo,
 			   const VkAllocationCallbacks* pAllocator);
 
@@ -76,7 +78,8 @@
 	friend class MVKSwapchain;
 
 	void propagateDebugName() override {}
-	void initLayer(CAMetalLayer* mtlLayer, const char* vkFuncName);
+	void setActiveSwapchain(MVKSwapchain* swapchain);
+	void initLayer(CAMetalLayer* mtlLayer, const char* vkFuncName, bool isHeadless);
 	void releaseLayer();
 
 	std::mutex _layerLock;
@@ -84,5 +87,6 @@
 	CAMetalLayer* _mtlCAMetalLayer = nil;
 	MVKBlockObserver* _layerObserver = nil;
 	MVKSwapchain* _activeSwapchain = nullptr;
+	VkExtent2D _headlessExtent = {0xFFFFFFFF, 0xFFFFFFFF};
 };
 
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm
index 3899ab6..0485571 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKSurface.mm
@@ -17,11 +17,26 @@
  */
 
 #include "MVKSurface.h"
+#include "MVKSwapchain.h"
 #include "MVKInstance.h"
 #include "MVKFoundation.h"
 #include "MVKOSExtensions.h"
+#include "mvk_datatypes.hpp"
+
+#import "CAMetalLayer+MoltenVK.h"
 #import "MVKBlockObserver.h"
 
+#ifdef VK_USE_PLATFORM_IOS_MVK
+#	define PLATFORM_VIEW_CLASS	UIView
+#	import <UIKit/UIView.h>
+#endif
+
+#ifdef VK_USE_PLATFORM_MACOS_MVK
+#	define PLATFORM_VIEW_CLASS	NSView
+#	import <AppKit/NSView.h>
+#endif
+
+
 // We need to double-dereference the name to first convert to the platform symbol, then to a string.
 #define STR_PLATFORM(NAME) #NAME
 #define STR(NAME) STR_PLATFORM(NAME)
@@ -34,38 +49,55 @@
 	return _mtlCAMetalLayer;
 }
 
+VkExtent2D MVKSurface::getExtent() {
+	return _mtlCAMetalLayer ? mvkVkExtent2DFromCGSize(_mtlCAMetalLayer.drawableSize) : _headlessExtent;
+}
+
+VkExtent2D MVKSurface::getNaturalExtent() {
+	return _mtlCAMetalLayer ? mvkVkExtent2DFromCGSize(_mtlCAMetalLayer.naturalDrawableSizeMVK) : _headlessExtent;
+}
+
+// Per spec, headless surface extent is set from the swapchain.
+void MVKSurface::setActiveSwapchain(MVKSwapchain* swapchain) {
+	_activeSwapchain = swapchain;
+	_headlessExtent = swapchain->getImageExtent();
+}
+
 MVKSurface::MVKSurface(MVKInstance* mvkInstance,
 					   const VkMetalSurfaceCreateInfoEXT* pCreateInfo,
 					   const VkAllocationCallbacks* pAllocator) : _mvkInstance(mvkInstance) {
-	initLayer((CAMetalLayer*)pCreateInfo->pLayer, "vkCreateMetalSurfaceEXT");
+	initLayer((CAMetalLayer*)pCreateInfo->pLayer, "vkCreateMetalSurfaceEXT", false);
+}
+
+MVKSurface::MVKSurface(MVKInstance* mvkInstance,
+					   const VkHeadlessSurfaceCreateInfoEXT* pCreateInfo,
+					   const VkAllocationCallbacks* pAllocator) : _mvkInstance(mvkInstance) {
+	initLayer(nil, "vkCreateHeadlessSurfaceEXT", true);
 }
 
 // pCreateInfo->pView can be either a CAMetalLayer or a view (NSView/UIView).
 MVKSurface::MVKSurface(MVKInstance* mvkInstance,
 					   const Vk_PLATFORM_SurfaceCreateInfoMVK* pCreateInfo,
 					   const VkAllocationCallbacks* pAllocator) : _mvkInstance(mvkInstance) {
+	MVKLogWarn("%s() is deprecated. Use vkCreateMetalSurfaceEXT() from the VK_EXT_metal_surface extension.", STR(vkCreate_PLATFORM_SurfaceMVK));
 
 	// Get the platform object contained in pView
-	id<NSObject> obj = (id<NSObject>)pCreateInfo->pView;
-
 	// If it's a view (NSView/UIView), extract the layer, otherwise assume it's already a CAMetalLayer.
+	id<NSObject> obj = (id<NSObject>)pCreateInfo->pView;
 	if ([obj isKindOfClass: [PLATFORM_VIEW_CLASS class]]) {
-		obj = ((PLATFORM_VIEW_CLASS*)obj).layer;
-		if ( !NSThread.isMainThread ) {
-			MVKLogWarn("%s(): You are not calling this function from the main thread. %s should only be accessed from the main thread. When using this function outside the main thread, consider passing the CAMetalLayer itself in %s::pView, instead of the %s.",
-					   STR(vkCreate_PLATFORM_SurfaceMVK), STR(PLATFORM_VIEW_CLASS), STR(Vk_PLATFORM_SurfaceCreateInfoMVK), STR(PLATFORM_VIEW_CLASS));
-		}
+		__block id<NSObject> layer;
+		mvkDispatchToMainAndWait(^{ layer = ((PLATFORM_VIEW_CLASS*)obj).layer; });
+		obj = layer;
 	}
 
 	// Confirm that we were provided with a CAMetalLayer
-	initLayer([obj isKindOfClass: CAMetalLayer.class] ? (CAMetalLayer*)obj : nil,
-			  STR(vkCreate_PLATFORM_SurfaceMVK));
+	initLayer([obj isKindOfClass: CAMetalLayer.class] ? (CAMetalLayer*)obj : nil, STR(vkCreate_PLATFORM_SurfaceMVK), false);
 }
 
-void MVKSurface::initLayer(CAMetalLayer* mtlLayer, const char* vkFuncName) {
+void MVKSurface::initLayer(CAMetalLayer* mtlLayer, const char* vkFuncName, bool isHeadless) {
 
 	_mtlCAMetalLayer = [mtlLayer retain];	// retained
-	if ( !_mtlCAMetalLayer ) { setConfigurationResult(reportError(VK_ERROR_SURFACE_LOST_KHR, "%s(): On-screen rendering requires a layer of type CAMetalLayer.", vkFuncName)); }
+	if ( !_mtlCAMetalLayer && !isHeadless ) { setConfigurationResult(reportError(VK_ERROR_SURFACE_LOST_KHR, "%s(): On-screen rendering requires a layer of type CAMetalLayer.", vkFuncName)); }
 
 	// Sometimes, the owning view can replace its CAMetalLayer.
 	// When that happens, the app needs to recreate the surface.
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h
index cd418bd..d8eb535 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h
@@ -23,7 +23,6 @@
 #include "MVKSmallVector.h"
 #include <mutex>
 
-#import "CAMetalLayer+MoltenVK.h"
 #import <Metal/Metal.h>
 
 class MVKWatermark;
@@ -46,9 +45,15 @@
 	/** Returns the CAMetalLayer underlying the surface used by this swapchain. */
 	CAMetalLayer* getCAMetalLayer();
 
+	/** Returns whether the surface is headless. */
+	bool isHeadless();
+
 	/** Returns the number of images in this swapchain. */
 	uint32_t getImageCount() { return (uint32_t)_presentableImages.size(); }
 
+	/** Returns the size of the images in this swapchain. */
+	VkExtent2D getImageExtent() { return _imageExtent; }
+
 	/** Returns the image at the specified index. */
 	MVKPresentableSwapchainImage* getPresentableImage(uint32_t index) { return _presentableImages[index]; }
 
@@ -126,7 +131,7 @@
 	std::atomic<uint64_t> _currentAcquisitionID = 0;
 	std::mutex _presentHistoryLock;
 	uint64_t _lastFrameTime = 0;
-	VkExtent2D _mtlLayerDrawableExtent = {0, 0};
+	VkExtent2D _imageExtent = {0, 0};
 	std::atomic<uint32_t> _unpresentedImageCount = 0;
 	uint32_t _currentPerfLogFrameCount = 0;
 	uint32_t _presentHistoryCount = 0;
@@ -134,18 +139,3 @@
 	uint32_t _presentHistoryHeadIndex = 0;
 	bool _isDeliberatelyScaled = false;
 };
-
-
-#pragma mark -
-#pragma mark Support functions
-
-/**
- * Returns the natural extent of the CAMetalLayer.
- *
- * The natural extent is the size of the bounds property of the layer,
- * multiplied by the contentsScale property of the layer, rounded
- * to nearest integer using half-to-even rounding.
- */
-static inline VkExtent2D mvkGetNaturalExtent(CAMetalLayer* mtlLayer) {
-	return mvkVkExtent2DFromCGSize(mtlLayer.naturalDrawableSizeMVK);
-}
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm
index 63c3ac7..5beeee0 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm
@@ -26,9 +26,11 @@
 #include "MVKWatermarkTextureContent.h"
 #include "MVKWatermarkShaderSource.h"
 #include "mvk_datatypes.hpp"
+#include <libkern/OSByteOrder.h>
+
+#import "CAMetalLayer+MoltenVK.h"
 #import "MVKBlockObserver.h"
 
-#include <libkern/OSByteOrder.h>
 
 using namespace std;
 
@@ -49,6 +51,8 @@
 
 CAMetalLayer* MVKSwapchain::getCAMetalLayer() { return _surface->getCAMetalLayer(); }
 
+bool MVKSwapchain::isHeadless() { return _surface->isHeadless(); }
+
 VkResult MVKSwapchain::getImages(uint32_t* pCount, VkImage* pSwapchainImages) {
 
 	// Get the number of surface images
@@ -124,16 +128,15 @@
 	return VK_SUCCESS;
 }
 
-// This swapchain is optimally sized for the surface if the app has specified deliberate
-// swapchain scaling, or the CAMetalLayer drawableSize has not changed since the swapchain
-// was created, and the CAMetalLayer will not need to be scaled when composited.
+// This swapchain is optimally sized for the surface if the app has specified 
+// deliberate swapchain scaling, or the surface extent has not changed since the
+// swapchain was created, and the surface will not need to be scaled when composited.
 bool MVKSwapchain::hasOptimalSurface() {
 	if (_isDeliberatelyScaled) { return true; }
 
-	auto* mtlLayer = getCAMetalLayer();
-	VkExtent2D drawExtent = mvkVkExtent2DFromCGSize(mtlLayer.drawableSize);
-	return (mvkVkExtent2DsAreEqual(drawExtent, _mtlLayerDrawableExtent) &&
-			mvkVkExtent2DsAreEqual(drawExtent, mvkGetNaturalExtent(mtlLayer)));
+	VkExtent2D surfExtent = _surface->getExtent();
+	return (mvkVkExtent2DsAreEqual(surfExtent, _imageExtent) &&
+			mvkVkExtent2DsAreEqual(surfExtent, _surface->getNaturalExtent()));
 }
 
 
@@ -187,30 +190,29 @@
 VkResult MVKSwapchain::getRefreshCycleDuration(VkRefreshCycleDurationGOOGLE *pRefreshCycleDuration) {
 	if (_device->getConfigurationResult() != VK_SUCCESS) { return _device->getConfigurationResult(); }
 
-	auto* mtlLayer = getCAMetalLayer();
-#if MVK_VISIONOS
-	// TODO: See if this can be obtained from OS instead
-	NSInteger framesPerSecond = 90;
+	auto* screen = getCAMetalLayer().screenMVK;		// Will be nil if headless
+#if MVK_MACOS && !MVK_MACCAT
+	double framesPerSecond = 60;
+	if (screen) {
+		CGDirectDisplayID displayId = [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
+		CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayId);
+		framesPerSecond = CGDisplayModeGetRefreshRate(mode);
+		CGDisplayModeRelease(mode);
+#if MVK_XCODE_13
+		if (framesPerSecond == 0 && [screen respondsToSelector: @selector(maximumFramesPerSecond)])
+			framesPerSecond = [screen maximumFramesPerSecond];
+#endif
+		// Builtin panels, e.g., on MacBook, report a zero refresh rate.
+		if (framesPerSecond == 0)
+			framesPerSecond = 60.0;
+	}
 #elif MVK_IOS_OR_TVOS || MVK_MACCAT
 	NSInteger framesPerSecond = 60;
-	UIScreen* screen = mtlLayer.screenMVK;
 	if ([screen respondsToSelector: @selector(maximumFramesPerSecond)]) {
 		framesPerSecond = screen.maximumFramesPerSecond;
 	}
-#elif MVK_MACOS && !MVK_MACCAT
-	NSScreen* screen = mtlLayer.screenMVK;
-	CGDirectDisplayID displayId = [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
-	CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayId);
-	double framesPerSecond = CGDisplayModeGetRefreshRate(mode);
-	CGDisplayModeRelease(mode);
-#if MVK_XCODE_13
-	if (framesPerSecond == 0 && [screen respondsToSelector: @selector(maximumFramesPerSecond)])
-		framesPerSecond = [screen maximumFramesPerSecond];
-#endif
-
-	// Builtin panels, e.g., on MacBook, report a zero refresh rate.
-	if (framesPerSecond == 0)
-		framesPerSecond = 60.0;
+#elif MVK_VISIONOS
+	NSInteger framesPerSecond = 90;		// TODO: See if this can be obtained from OS instead
 #endif
 
 	pRefreshCycleDuration->refreshDuration = (uint64_t)1e9 / framesPerSecond;
@@ -260,12 +262,6 @@
 		_presentHistoryHeadIndex = (_presentHistoryHeadIndex + 1) % kMaxPresentationHistory;
 	}
 
-	// If actual present time is not available, use desired time instead, and if that
-	// hasn't been set, use the current time, which should be reasonably accurate (sub-ms),
-	// since we are here as part of the addPresentedHandler: callback.
-	if (actualPresentTime == 0) { actualPresentTime = presentInfo.desiredPresentTime; }
-	if (actualPresentTime == 0) { actualPresentTime = CACurrentMediaTime() * 1.0e9; }
-
 	_presentTimingHistory[_presentHistoryIndex].presentID = presentInfo.presentID;
 	_presentTimingHistory[_presentHistoryIndex].desiredPresentTime = presentInfo.desiredPresentTime;
 	_presentTimingHistory[_presentHistoryIndex].actualPresentTime = actualPresentTime;
@@ -380,12 +376,13 @@
 
 MVKSwapchain::MVKSwapchain(MVKDevice* device, const VkSwapchainCreateInfoKHR* pCreateInfo)
 	: MVKVulkanAPIDeviceObject(device),
-	_surface((MVKSurface*)pCreateInfo->surface) {
+	_surface((MVKSurface*)pCreateInfo->surface),
+	_imageExtent(pCreateInfo->imageExtent) {
 
 	// Check if oldSwapchain is properly set
 	auto* oldSwapchain = (MVKSwapchain*)pCreateInfo->oldSwapchain;
 	if (oldSwapchain == _surface->_activeSwapchain) {
-		_surface->_activeSwapchain = this;
+		_surface->setActiveSwapchain(this);
 	} else {
 		setConfigurationResult(reportError(VK_ERROR_NATIVE_WINDOW_IN_USE_KHR, "vkCreateSwapchainKHR(): pCreateInfo->oldSwapchain does not match the VkSwapchain that is in use by the surface"));
 		return;
@@ -470,10 +467,11 @@
 									VkSwapchainPresentScalingCreateInfoEXT* pScalingInfo,
 									uint32_t imgCnt) {
 
-	if ( getIsSurfaceLost() ) { return; }
-
 	auto* mtlLayer = getCAMetalLayer();
+	if ( !mtlLayer || getIsSurfaceLost() ) { return; }
+
 	auto minMagFilter = mvkConfig().swapchainMinMagFilterUseNearest ? kCAFilterNearest : kCAFilterLinear;
+	mtlLayer.drawableSize = mvkCGSizeFromVkExtent2D(_imageExtent);
 	mtlLayer.device = getMTLDevice();
 	mtlLayer.pixelFormat = getPixelFormats()->getMTLPixelFormat(pCreateInfo->imageFormat);
 	mtlLayer.maximumDrawableCountMVK = imgCnt;
@@ -491,15 +489,10 @@
 	// presentations on the oldSwapchain to complete and call back, but if the drawableSize
 	// is not changing from the previous, we force those completions first.
 	auto* oldSwapchain = (MVKSwapchain*)pCreateInfo->oldSwapchain;
-	if (oldSwapchain && mvkVkExtent2DsAreEqual(pCreateInfo->imageExtent, mvkVkExtent2DFromCGSize(mtlLayer.drawableSize))) {
+	if (oldSwapchain && mvkVkExtent2DsAreEqual(pCreateInfo->imageExtent, _surface->getExtent())) {
 		oldSwapchain->forceUnpresentedImageCompletion();
 	}
 
-	// Remember the extent to later detect if it has changed under the covers,
-	// and set the drawable size of the CAMetalLayer from the extent.
-	_mtlLayerDrawableExtent = pCreateInfo->imageExtent;
-	mtlLayer.drawableSize = mvkCGSizeFromVkExtent2D(_mtlLayerDrawableExtent);
-
 	if (pCreateInfo->compositeAlpha != VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) {
 		mtlLayer.opaque = pCreateInfo->compositeAlpha == VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
 	}
@@ -585,14 +578,13 @@
 		}
 	}
 
-	auto* mtlLayer = getCAMetalLayer();
     VkExtent2D imgExtent = pCreateInfo->imageExtent;
     VkImageCreateInfo imgInfo = {
         .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
         .pNext = VK_NULL_HANDLE,
         .imageType = VK_IMAGE_TYPE_2D,
-        .format = getPixelFormats()->getVkFormat(mtlLayer.pixelFormat),
-        .extent = { imgExtent.width, imgExtent.height, 1 },
+        .format = pCreateInfo->imageFormat,
+        .extent = mvkVkExtent3DFromVkExtent2D(imgExtent),
         .mipLevels = 1,
         .arrayLayers = 1,
         .samples = VK_SAMPLE_COUNT_1_BIT,
@@ -618,14 +610,20 @@
 		_presentableImages.push_back(_device->createPresentableSwapchainImage(&imgInfo, this, imgIdx, nullptr));
 	}
 
-	NSString* screenName = @"Main Screen";
+	auto* mtlLayer = getCAMetalLayer();
+	if (mtlLayer) {
+		NSString* screenName = @"Main Screen";
 #if MVK_MACOS && !MVK_MACCAT
-	if ([mtlLayer.screenMVK respondsToSelector:@selector(localizedName)]) {
-		screenName = mtlLayer.screenMVK.localizedName;
-	}
+		auto* screen = mtlLayer.screenMVK;
+		if ([screen respondsToSelector:@selector(localizedName)]) {
+			screenName = screen.localizedName;
+		}
 #endif
-	MVKLogInfo("Created %d swapchain images with size (%d, %d) and contents scale %.1f in layer %s (%p) on screen %s.",
-			   imgCnt, imgExtent.width, imgExtent.height, mtlLayer.contentsScale, mtlLayer.name.UTF8String, mtlLayer, screenName.UTF8String);
+		MVKLogInfo("Created %d swapchain images with size (%d, %d) and contents scale %.1f in layer %s (%p) on screen %s.",
+				   imgCnt, imgExtent.width, imgExtent.height, mtlLayer.contentsScale, mtlLayer.name.UTF8String, mtlLayer, screenName.UTF8String);
+	} else {
+		MVKLogInfo("Created %d swapchain images with size (%d, %d) on headless surface.", imgCnt, imgExtent.width, imgExtent.height);
+	}
 }
 
 void MVKSwapchain::destroy() {
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm b/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm
index b7a4a64..dfb536b 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKSync.mm
@@ -128,7 +128,7 @@
 }
 
 void MVKSemaphoreMTLEvent::encodeDeferredSignal(id<MTLCommandBuffer> mtlCmdBuff, uint64_t deferToken) {
-	if (mtlCmdBuff) { [mtlCmdBuff encodeSignalEvent: _mtlEvent value: deferToken]; }
+	[mtlCmdBuff encodeSignalEvent: _mtlEvent value: deferToken];
 }
 
 MVKSemaphoreMTLEvent::MVKSemaphoreMTLEvent(MVKDevice* device,
diff --git a/MoltenVK/MoltenVK/Layers/MVKExtensions.def b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
index d8c222b..777d725 100644
--- a/MoltenVK/MoltenVK/Layers/MVKExtensions.def
+++ b/MoltenVK/MoltenVK/Layers/MVKExtensions.def
@@ -109,6 +109,7 @@
 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)
+MVK_EXTENSION(EXT_headless_surface,                   EXT_HEADLESS_SURFACE,                   INSTANCE, 10.11,  8.0,  1.0)
 MVK_EXTENSION(EXT_host_query_reset,                   EXT_HOST_QUERY_RESET,                   DEVICE,   10.11,  8.0,  1.0)
 MVK_EXTENSION(EXT_image_robustness,                   EXT_IMAGE_ROBUSTNESS,                   DEVICE,   10.11,  8.0,  1.0)
 MVK_EXTENSION(EXT_inline_uniform_block,               EXT_INLINE_UNIFORM_BLOCK,               DEVICE,   10.11,  8.0,  1.0)
diff --git a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h
index 61a5c43..c78128b 100644
--- a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h
+++ b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h
@@ -23,12 +23,10 @@
 #import <QuartzCore/QuartzCore.h>
 
 #if MVK_IOS_OR_TVOS || MVK_MACCAT
-#	define PLATFORM_SCREEN_CLASS	UIScreen
 #	include <UIKit/UIScreen.h>
 #endif
 
 #if MVK_MACOS && !MVK_MACCAT
-#	define PLATFORM_SCREEN_CLASS	NSScreen
 #	include <AppKit/NSScreen.h>
 #endif
 
@@ -76,9 +74,16 @@
  */
 @property(nonatomic, readwrite) CFStringRef colorspaceNameMVK;
 
-#if !MVK_VISIONOS
+#if MVK_IOS_OR_TVOS || MVK_MACCAT
 /** Returns the screen on which this layer is rendering. */
-@property(nonatomic, readonly) PLATFORM_SCREEN_CLASS* screenMVK;
+@property(nonatomic, readonly) UIScreen* screenMVK;
+#endif
+
+#if MVK_MACOS && !MVK_MACCAT
+/** Returns the screen on which this layer is rendering. */
+@property(nonatomic, readonly) NSScreen* screenMVK;
+
+@property(nonatomic, readonly) NSScreen* privateScreenMVKImpl;
 #endif
 
 @end
diff --git a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.m b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.mm
similarity index 91%
rename from MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.m
rename to MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.mm
index 9a8b10d..380a915 100644
--- a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.m
+++ b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.mm
@@ -18,6 +18,7 @@
 
 
 #include "CAMetalLayer+MoltenVK.h"
+#include "MVKOSExtensions.h"
 
 #if MVK_MACOS && !MVK_MACCAT
 #	include <AppKit/NSApplication.h>
@@ -88,6 +89,13 @@
 
 #if MVK_MACOS && !MVK_MACCAT
 -(NSScreen*) screenMVK {
+	__block NSScreen* screen;
+	mvkDispatchToMainAndWait(^{ screen = self.privateScreenMVKImpl; });
+	return screen;
+}
+
+// Search for the screen currently displaying the layer, and default to the main screen if it can't be found.
+-(NSScreen*) privateScreenMVKImpl {
 	// If this layer has a delegate that is an NSView, and the view is in a window, retrieve the screen from the window.
 	if ([self.delegate isKindOfClass: NSView.class]) {
 		NSWindow* window = ((NSView*)self.delegate).window;
diff --git a/MoltenVK/MoltenVK/Vulkan/vulkan.mm b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
index c08c5b3..293826a 100644
--- a/MoltenVK/MoltenVK/Vulkan/vulkan.mm
+++ b/MoltenVK/MoltenVK/Vulkan/vulkan.mm
@@ -3870,6 +3870,26 @@
 
 
 #pragma mark -
+#pragma mark VK_EXT_headless_surface extension
+
+MVK_PUBLIC_VULKAN_SYMBOL VkResult vkCreateHeadlessSurfaceEXT(
+    VkInstance                                  instance,
+    const VkHeadlessSurfaceCreateInfoEXT*       pCreateInfo,
+    const VkAllocationCallbacks*                pAllocator,
+    VkSurfaceKHR*                               pSurface) {
+
+	MVKTraceVulkanCallStart();
+	MVKInstance* mvkInst = MVKInstance::getMVKInstance(instance);
+	MVKSurface* mvkSrfc = mvkInst->createSurface(pCreateInfo, pAllocator);
+	*pSurface = (VkSurfaceKHR)mvkSrfc;
+	VkResult rslt = mvkSrfc->getConfigurationResult();
+	if (rslt < 0) { *pSurface = VK_NULL_HANDLE; mvkInst->destroySurface(mvkSrfc, pAllocator); }
+	MVKTraceVulkanCallEnd();
+	return rslt;
+}
+
+
+#pragma mark -
 #pragma mark VK_EXT_host_query_reset extension
 
 MVK_PUBLIC_VULKAN_CORE_ALIAS(vkResetQueryPool, EXT);
diff --git a/README.md b/README.md
index 5c205b0..2fa3652 100644
--- a/README.md
+++ b/README.md
@@ -149,21 +149,14 @@
 	--maccat 
 	--tvos 
 	--tvossim
-	--visionos 
-	--visionossim
-
-The `visionos` and `visionossim` selections require Xcode 15+.
 
 You can specify multiple of these selections. The result is a single `XCFramework` 
 for each external dependency library, with each `XCFramework` containing binaries for 
 each of the requested platforms. 
 
-The `--all` selection is the same as entering all of the other platform choices, except 
-`--visionos` and `--visionossim`, and will result in a single `XCFramework` for each 
-external dependency library, with each `XCFramework` containing binaries for all supported 
-platforms and simulators. The `--visionos` and `--visionossim` selections must be invoked
-with a separate invocation of `fetchDependencies`, because those selections require 
-Xcode 15+, and will cause a multi-platform build on older versions of Xcode to abort.
+The `--all` selection is the same as entering all of the other platform choices, 
+and will result in a single `XCFramework` for each external dependency library, 
+with each `XCFramework` containing binaries for all supported platforms and simulators. 
 
 Running `fetchDependencies` repeatedly with different platforms will accumulate targets 
 in the `XCFramework`, if the `--keep-cache` option is used on each invocation.
@@ -263,8 +256,6 @@
 	make maccat
 	make tvos
 	make tvossim
-	make visionos
-	make visionossim
 	
 	make all-debug
 	make macos-debug
@@ -273,15 +264,12 @@
 	make maccat-debug
 	make tvos-debug
 	make tvossim-debug
-	make visionos-debug
-	make visionossim-debug
 	
 	make clean
 	make install
 
 - Running `make` repeatedly with different targets will accumulate binaries for these different targets.
-- The `all` target executes all platform targets, except `visionos` and `visionossim`, as these require
-  Xcode 15+, and will abort a multi-platform build on older versions of Xcode.
+- The `all` target executes all platform targets.
 - The `all` target is the default target. Running `make` with no arguments is the same as running `make all`.
 - The `*-debug` targets build the binaries using the **_Debug_** configuration.
 - The `install` target will copy the most recently built `MoltenVK.xcframework` into the