Merge branch 'master' of https://github.com/KhronosGroup/MoltenVK
diff --git a/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h b/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
index d6025c4..80f1e10 100644
--- a/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
+++ b/MoltenVK/MoltenVK/API/vk_mvk_moltenvk.h
@@ -620,6 +620,7 @@
 	VkBool32 sharedLinearTextures;				/**< If true, linear textures and texture buffers can be created from buffers in Shared storage. */
 	VkBool32 depthResolve;						/**< If true, resolving depth textures with filters other than Sample0 is supported. */
 	VkBool32 stencilResolve;					/**< If true, resolving stencil textures with filters other than Sample0 is supported. */
+	uint32_t maxPerStageDynamicMTLBufferCount;	/**< The maximum number of inline buffers that can be set on a command buffer. */
 } MVKPhysicalDeviceMetalFeatures;
 
 /** MoltenVK performance of a particular type of activity. */
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
index 4add282..8c094d1 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKDevice.mm
@@ -298,10 +298,10 @@
             case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_INLINE_UNIFORM_BLOCK_PROPERTIES_EXT: {
 				auto* inlineUniformBlockProps = (VkPhysicalDeviceInlineUniformBlockPropertiesEXT*)next;
 				inlineUniformBlockProps->maxInlineUniformBlockSize = _metalFeatures.dynamicMTLBufferSize;
-                inlineUniformBlockProps->maxPerStageDescriptorInlineUniformBlocks = _properties.limits.maxPerStageDescriptorUniformBuffers;
-                inlineUniformBlockProps->maxPerStageDescriptorUpdateAfterBindInlineUniformBlocks = _properties.limits.maxPerStageDescriptorUniformBuffers;
-                inlineUniformBlockProps->maxDescriptorSetInlineUniformBlocks = _properties.limits.maxDescriptorSetUniformBuffers;
-                inlineUniformBlockProps->maxDescriptorSetUpdateAfterBindInlineUniformBlocks = _properties.limits.maxDescriptorSetUniformBuffers;
+                inlineUniformBlockProps->maxPerStageDescriptorInlineUniformBlocks = _metalFeatures.dynamicMTLBufferSize ? _metalFeatures.maxPerStageDynamicMTLBufferCount - 1 : 0;    // Less one for push constants
+                inlineUniformBlockProps->maxPerStageDescriptorUpdateAfterBindInlineUniformBlocks = inlineUniformBlockProps->maxPerStageDescriptorInlineUniformBlocks;
+                inlineUniformBlockProps->maxDescriptorSetInlineUniformBlocks = (inlineUniformBlockProps->maxPerStageDescriptorInlineUniformBlocks * 4);
+                inlineUniformBlockProps->maxDescriptorSetUpdateAfterBindInlineUniformBlocks = (inlineUniformBlockProps->maxPerStageDescriptorUpdateAfterBindInlineUniformBlocks * 4);
 				break;
 			}
 			case VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_PROPERTIES_EXT: {
@@ -412,6 +412,8 @@
 
 	if ( !pImageFormatProperties ) { return VK_SUCCESS; }
 
+	mvkClear(pImageFormatProperties);
+
 	// Metal does not support creating uncompressed views of compressed formats.
 	// Metal does not support split-instance images.
 	if (mvkIsAnyFlagEnabled(flags, VK_IMAGE_CREATE_BLOCK_TEXEL_VIEW_COMPATIBLE_BIT | VK_IMAGE_CREATE_SPLIT_INSTANCE_BIND_REGIONS_BIT)) {
@@ -419,11 +421,19 @@
 	}
 
 	MVKFormatType mvkFmt = _pixelFormats.getFormatType(format);
+	bool isChromaSubsampled = _pixelFormats.getChromaSubsamplingPlaneCount(format) > 0;
+	bool isMultiPlanar = _pixelFormats.getChromaSubsamplingPlaneCount(format) > 1;
+	bool isBGRG = isChromaSubsampled && !isMultiPlanar && _pixelFormats.getBlockTexelSize(format).width > 1;
 	bool hasAttachmentUsage = mvkIsAnyFlagEnabled(usage, (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
 														  VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT |
 														  VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT |
 														  VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT));
 
+	// Disjoint memory requires a multiplanar format.
+	if (!isMultiPlanar && mvkIsAnyFlagEnabled(flags, VK_IMAGE_CREATE_DISJOINT_BIT)) {
+		return VK_ERROR_FORMAT_NOT_SUPPORTED;
+	}
+
 	VkPhysicalDeviceLimits* pLimits = &_properties.limits;
 	VkExtent3D maxExt = { 1, 1, 1};
 	uint32_t maxLevels = 1;
@@ -436,10 +446,7 @@
 		case VK_IMAGE_TYPE_1D:
 			maxExt.height = 1;
 			maxExt.depth = 1;
-			if (mvkTreatTexture1DAs2D()) {
-				maxExt.width = pLimits->maxImageDimension2D;
-				maxLevels = mvkMipmapLevels3D(maxExt);
-			} else {
+			if (!mvkTreatTexture1DAs2D()) {
 				maxExt.width = pLimits->maxImageDimension1D;
 				maxLevels = 1;
 				sampleCounts = VK_SAMPLE_COUNT_1_BIT;
@@ -453,29 +460,42 @@
 				// Metal does not allow compressed or depth/stencil formats on native 1D textures
 				if (mvkFmt == kMVKFormatDepthStencil) { return VK_ERROR_FORMAT_NOT_SUPPORTED; }
 				if (mvkFmt == kMVKFormatCompressed) { return VK_ERROR_FORMAT_NOT_SUPPORTED; }
+				if (isChromaSubsampled) { return VK_ERROR_FORMAT_NOT_SUPPORTED; }
+				break;
 			}
-			break;
 
+			// A 420 1D image doesn't make much sense.
+			if (isChromaSubsampled && _pixelFormats.getBlockTexelSize(format).height > 1) {
+				return VK_ERROR_FORMAT_NOT_SUPPORTED;
+			}
+			// Vulkan doesn't allow 1D multisampled images.
+			sampleCounts = VK_SAMPLE_COUNT_1_BIT;
+			/* fallthrough */
 		case VK_IMAGE_TYPE_2D:
 			if (mvkIsAnyFlagEnabled(flags, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) ) {
+				// Chroma-subsampled cube images aren't supported.
+				if (isChromaSubsampled) { return VK_ERROR_FORMAT_NOT_SUPPORTED; }
+				// 1D cube images aren't supported.
+				if (type == VK_IMAGE_TYPE_1D) { return VK_ERROR_FORMAT_NOT_SUPPORTED; }
 				maxExt.width = pLimits->maxImageDimensionCube;
 				maxExt.height = pLimits->maxImageDimensionCube;
 			} else {
 				maxExt.width = pLimits->maxImageDimension2D;
-				maxExt.height = pLimits->maxImageDimension2D;
+				maxExt.height = (type == VK_IMAGE_TYPE_1D ? 1 : pLimits->maxImageDimension2D);
 			}
 			maxExt.depth = 1;
 			if (tiling == VK_IMAGE_TILING_LINEAR) {
 				// Linear textures have additional restrictions under Metal:
-				// - They may not be depth/stencil or compressed textures.
-				if (mvkFmt == kMVKFormatDepthStencil || mvkFmt == kMVKFormatCompressed) {
+				// - They may not be depth/stencil, compressed, or chroma subsampled textures.
+				//   We allow multi-planar formats because those internally use non-subsampled formats.
+				if (mvkFmt == kMVKFormatDepthStencil || mvkFmt == kMVKFormatCompressed || isBGRG) {
 					return VK_ERROR_FORMAT_NOT_SUPPORTED;
 				}
 #if MVK_MACOS
 				// - On macOS, Linear textures may not be used as framebuffer attachments.
 				if (hasAttachmentUsage) { return VK_ERROR_FORMAT_NOT_SUPPORTED; }
 #endif
-				// Linear textures may only have one mip level. layer & sample
+				// Linear textures may only have one mip level, layer & sample.
 				maxLevels = 1;
 				maxLayers = 1;
 				sampleCounts = VK_SAMPLE_COUNT_1_BIT;
@@ -483,14 +503,22 @@
 				VkFormatProperties fmtProps;
 				getFormatProperties(format, &fmtProps);
 				// Compressed multisampled textures aren't supported.
+				// Chroma-subsampled multisampled textures aren't supported.
 				// Multisampled cube textures aren't supported.
 				// Non-renderable multisampled textures aren't supported.
-				if (mvkFmt == kMVKFormatCompressed ||
+				if (mvkFmt == kMVKFormatCompressed || isChromaSubsampled ||
 					mvkIsAnyFlagEnabled(flags, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT) ||
 					!mvkIsAnyFlagEnabled(fmtProps.optimalTilingFeatures, VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT|VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) ) {
 					sampleCounts = VK_SAMPLE_COUNT_1_BIT;
 				}
-				maxLevels = mvkMipmapLevels3D(maxExt);
+				// BGRG and GBGR images may only have one mip level and one layer.
+				// Other chroma subsampled formats may have multiple mip levels, but still only one layer.
+				if (isChromaSubsampled) {
+					maxLevels = isBGRG ? 1 : mvkMipmapLevels3D(maxExt);
+					maxLayers = 1;
+				} else {
+					maxLevels = mvkMipmapLevels3D(maxExt);
+				}
 			}
 			break;
 
@@ -500,7 +528,8 @@
 				return VK_ERROR_FORMAT_NOT_SUPPORTED;
 			}
 			// Metal does not allow compressed or depth/stencil formats on 3D textures
-			if (mvkFmt == kMVKFormatDepthStencil
+			if (mvkFmt == kMVKFormatDepthStencil ||
+				isChromaSubsampled
 #if MVK_IOS_OR_TVOS
 				|| mvkFmt == kMVKFormatCompressed
 #endif
@@ -560,7 +589,7 @@
         switch (nextProps->sType) {
             case VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_IMAGE_FORMAT_PROPERTIES: {
                 auto* samplerYcbcrConvProps = (VkSamplerYcbcrConversionImageFormatProperties*)nextProps;
-                samplerYcbcrConvProps->combinedImageSamplerDescriptorCount = _pixelFormats.getChromaSubsamplingPlaneCount(pImageFormatInfo->format);
+                samplerYcbcrConvProps->combinedImageSamplerDescriptorCount = std::max(_pixelFormats.getChromaSubsamplingPlaneCount(pImageFormatInfo->format), (uint8_t)1u);
                 break;
             }
             default:
@@ -990,6 +1019,7 @@
 	_metalFeatures.maxPerStageBufferCount = 31;
     _metalFeatures.maxMTLBufferSize = (256 * MEBI);
     _metalFeatures.dynamicMTLBufferSize = 0;
+    _metalFeatures.maxPerStageDynamicMTLBufferCount = 0;
 
     _metalFeatures.maxPerStageSamplerCount = 16;
     _metalFeatures.maxQueryBufferSize = (64 * KIBI);
@@ -1015,6 +1045,7 @@
 	_metalFeatures.maxTextureDimension = (8 * KIBI);
     _metalFeatures.dynamicMTLBufferSize = (4 * KIBI);
     _metalFeatures.sharedLinearTextures = true;
+    _metalFeatures.maxPerStageDynamicMTLBufferCount = _metalFeatures.maxPerStageBufferCount;
 
     if (supportsMTLFeatureSet(tvOS_GPUFamily1_v2)) {
 		_metalFeatures.mslVersionEnum = MTLLanguageVersion1_2;
@@ -1069,6 +1100,7 @@
 		_metalFeatures.mslVersionEnum = MTLLanguageVersion1_1;
         _metalFeatures.dynamicMTLBufferSize = (4 * KIBI);
 		_metalFeatures.maxTextureDimension = (8 * KIBI);
+		_metalFeatures.maxPerStageDynamicMTLBufferCount = _metalFeatures.maxPerStageBufferCount;
     }
 
     if (supportsMTLFeatureSet(iOS_GPUFamily1_v3)) {
@@ -1150,6 +1182,7 @@
         _metalFeatures.combinedStoreResolveAction = true;
 		_metalFeatures.deferredStoreActions = true;
         _metalFeatures.maxMTLBufferSize = (1 * GIBI);
+        _metalFeatures.maxPerStageDynamicMTLBufferCount = 14;
     }
 
     if (supportsMTLFeatureSet(macOS_GPUFamily1_v3)) {
@@ -1521,7 +1554,6 @@
         uint32_t maxStorage = 0, maxUniform = 0;
         bool singleTexelStorage = true, singleTexelUniform = true;
         _pixelFormats.enumerateSupportedFormats({0, 0, VK_FORMAT_FEATURE_UNIFORM_TEXEL_BUFFER_BIT | VK_FORMAT_FEATURE_STORAGE_TEXEL_BUFFER_BIT}, true, [&](VkFormat vk) {
-            if ( _pixelFormats.getChromaSubsamplingComponentBits(vk) > 0 ) { return false; }    // Skip chroma subsampling formats
 			MTLPixelFormat mtlFmt = _pixelFormats.getMTLPixelFormat(vk);
 			if ( !mtlFmt ) { return false; }	// If format is invalid, avoid validation errors on MTLDevice format alignment calls
 
@@ -2965,8 +2997,15 @@
 VkDeviceSize MVKDevice::getVkFormatTexelBufferAlignment(VkFormat format, MVKBaseObject* mvkObj) {
 	VkDeviceSize deviceAlignment = 0;
 	id<MTLDevice> mtlDev = getMTLDevice();
+	MVKPixelFormats* mvkPixFmts = getPixelFormats();
 	if ([mtlDev respondsToSelector: @selector(minimumLinearTextureAlignmentForPixelFormat:)]) {
-		deviceAlignment = [mtlDev minimumLinearTextureAlignmentForPixelFormat: getPixelFormats()->getMTLPixelFormat(format)];
+		MTLPixelFormat mtlPixFmt = mvkPixFmts->getMTLPixelFormat(format);
+		if (mvkPixFmts->getChromaSubsamplingPlaneCount(format) >= 2) {
+			// Use plane 1 to get the alignment requirements. In a 2-plane format, this will
+			// typically have stricter alignment requirements due to it being a 2-component format.
+			mtlPixFmt = mvkPixFmts->getChromaSubsamplingPlaneMTLPixelFormat(format, 1);
+		}
+		deviceAlignment = [mtlDev minimumLinearTextureAlignmentForPixelFormat: mtlPixFmt];
 	}
 	return deviceAlignment ? deviceAlignment : _pProperties->limits.minTexelBufferOffsetAlignment;
 }
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h
index b88450f..227fe3e 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h
@@ -144,8 +144,10 @@
     MVKImageMemoryBinding(MVKDevice* device, MVKImage* image, uint8_t planeIndex);
 
     MVKImage* _image;
+    id<MTLBuffer> _mtlTexelBuffer = nil;
+    NSUInteger _mtlTexelBufferOffset = 0;
     uint8_t _planeIndex;
-    bool _usesTexelBuffer;
+    bool _ownsTexelBuffer = false;
 };
 
 
@@ -367,6 +369,8 @@
 	bool _is3DCompressed;
 	bool _isAliasable;
 	bool _hasExtendedUsage;
+	bool _hasMutableFormat;
+	bool _isLinearForAtomics;
 };
 
 
@@ -587,6 +591,7 @@
 	 * This is a static function that can be used to validate image view formats prior to creating one.
 	 */
 	static VkResult validateSwizzledMTLPixelFormat(const VkImageViewCreateInfo* pCreateInfo,
+												   VkImageUsageFlags usage,
 												   MVKVulkanAPIObject* apiObject,
 												   bool hasNativeSwizzleSupport,
 												   bool hasShaderSwizzleSupport,
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
index 173655d..b99eef9 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm
@@ -51,10 +51,10 @@
                            newTextureWithDescriptor: mtlTexDesc
                            iosurface: _image->_ioSurface
                            plane: _planeIndex];
-        } else if (memoryBinding->_usesTexelBuffer) {
-            _mtlTexture = [memoryBinding->_deviceMemory->getMTLBuffer()
+        } else if (memoryBinding->_mtlTexelBuffer) {
+            _mtlTexture = [memoryBinding->_mtlTexelBuffer
                            newTextureWithDescriptor: mtlTexDesc
-                           offset: memoryBinding->getDeviceMemoryOffset()
+                           offset: memoryBinding->_mtlTexelBufferOffset
                            bytesPerRow: _subresources[0].layout.rowPitch];
         } else if (memoryBinding->_deviceMemory->getMTLHeap() && !_image->getIsDepthStencil()) {
             // Metal support for depth/stencil from heaps is flaky
@@ -123,7 +123,7 @@
     mtlTexDesc.mipmapLevelCount = _image->_mipLevels;
     mtlTexDesc.sampleCount = mvkSampleCountFromVkSampleCountFlagBits(_image->_samples);
     mtlTexDesc.arrayLength = _image->_arrayLayers;
-    mtlTexDesc.usageMVK = _image->getPixelFormats()->getMTLTextureUsage(_image->_usage, mtlPixFmt, minUsage, _image->_isLinear, _image->_hasExtendedUsage);
+    mtlTexDesc.usageMVK = _image->getPixelFormats()->getMTLTextureUsage(_image->_usage, mtlPixFmt, minUsage, _image->_isLinear, _image->_hasMutableFormat, _image->_hasExtendedUsage);
     mtlTexDesc.storageModeMVK = _image->getMTLStorageMode();
     mtlTexDesc.cpuCacheMode = _image->getMTLCPUCacheMode();
 
@@ -306,7 +306,7 @@
 
 	MVKImageMemoryBinding* memBind = getMemoryBinding();
 	bool needsSync = memBind->needsHostReadSync(srcStageMask, dstStageMask, barrier);
-	bool needsPull = (!memBind->_usesTexelBuffer &&
+	bool needsPull = ((!memBind->_mtlTexelBuffer || memBind->_ownsTexelBuffer) &&
 					  memBind->isMemoryHostCoherent() &&
 					  barrier.newLayout == VK_IMAGE_LAYOUT_GENERAL &&
 					  mvkIsAnyFlagEnabled(barrier.dstAccessMask, (VK_ACCESS_HOST_READ_BIT | VK_ACCESS_MEMORY_READ_BIT)));
@@ -375,9 +375,10 @@
         case VK_STRUCTURE_TYPE_MEMORY_DEDICATED_REQUIREMENTS: {
             auto* dedicatedReqs = (VkMemoryDedicatedRequirements*)next;
             bool writable = mvkIsAnyFlagEnabled(_image->_usage, VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
+            bool canUseTexelBuffer = _device->_pMetalFeatures->texelBuffers && _image->_isLinear && !_image->getIsCompressed();
             dedicatedReqs->requiresDedicatedAllocation = _requiresDedicatedMemoryAllocation;
             dedicatedReqs->prefersDedicatedAllocation = (dedicatedReqs->requiresDedicatedAllocation ||
-                                                        (!_usesTexelBuffer && (writable || !_device->_pMetalFeatures->placementHeaps)));
+                                                        (!canUseTexelBuffer && (writable || !_device->_pMetalFeatures->placementHeaps)));
             break;
         }
         default:
@@ -392,11 +393,34 @@
     if (_deviceMemory) { _deviceMemory->removeImageMemoryBinding(this); }
     MVKResource::bindDeviceMemory(mvkMem, memOffset);
 
-    _usesTexelBuffer = _device->_pMetalFeatures->texelBuffers && _deviceMemory && _deviceMemory->_mtlBuffer; // Texel buffers available
-    _usesTexelBuffer = _usesTexelBuffer && (isMemoryHostAccessible() || _device->_pMetalFeatures->placementHeaps) && _image->_isLinear && !_image->getIsCompressed(); // Applicable memory layout
+    bool usesTexelBuffer = _device->_pMetalFeatures->texelBuffers && _deviceMemory; // Texel buffers available
+    usesTexelBuffer = usesTexelBuffer && (isMemoryHostAccessible() || _device->_pMetalFeatures->placementHeaps) && _image->_isLinear && !_image->getIsCompressed(); // Applicable memory layout
 
     // macOS before 10.15.5 cannot use shared memory for texel buffers.
-    _usesTexelBuffer = _usesTexelBuffer && (_device->_pMetalFeatures->sharedLinearTextures || !isMemoryHostCoherent());
+    usesTexelBuffer = usesTexelBuffer && (_device->_pMetalFeatures->sharedLinearTextures || !isMemoryHostCoherent());
+
+    if (_image->_isLinearForAtomics) {
+        if (usesTexelBuffer && _deviceMemory->ensureMTLBuffer()) {
+            _mtlTexelBuffer = _deviceMemory->_mtlBuffer;
+            _mtlTexelBufferOffset = getDeviceMemoryOffset();
+        } else {
+            // Create our own buffer for this.
+            if (_deviceMemory && _deviceMemory->_mtlHeap && _image->getMTLStorageMode() == _deviceMemory->_mtlStorageMode) {
+                _mtlTexelBuffer = [_deviceMemory->_mtlHeap newBufferWithLength: _byteCount options: _deviceMemory->getMTLResourceOptions() offset: getDeviceMemoryOffset()];
+                if (_image->_isAliasable) { [_mtlTexelBuffer makeAliasable]; }
+            } else {
+                _mtlTexelBuffer = [getMTLDevice() newBufferWithLength: _byteCount options: _image->getMTLStorageMode() << MTLResourceStorageModeShift];
+            }
+            if (!_mtlTexelBuffer) {
+                return reportError(VK_ERROR_OUT_OF_DEVICE_MEMORY, "Could not create an MTLBuffer for an image that requires a buffer backing store. Images that can be used for atomic accesses must have a texel buffer backing them.");
+            }
+            _mtlTexelBufferOffset = 0;
+            _ownsTexelBuffer = true;
+        }
+    } else if (usesTexelBuffer && _deviceMemory->_mtlBuffer) {
+        _mtlTexelBuffer = _deviceMemory->_mtlBuffer;
+        _mtlTexelBufferOffset = getDeviceMemoryOffset();
+    }
 
     flushToDevice(getDeviceMemoryOffset(), getByteCount());
     return _deviceMemory ? _deviceMemory->addImageMemoryBinding(this) : VK_SUCCESS;
@@ -420,6 +444,9 @@
     for(uint8_t planeIndex = beginPlaneIndex(); planeIndex < endPlaneIndex(); planeIndex++) {
         _image->_planes[planeIndex]->propagateDebugName();
     }
+    if (_ownsTexelBuffer) {
+        setLabelIfNotNil(_mtlTexelBuffer, _image->_debugName);
+    }
 }
 
 // Returns whether the specified image memory barrier requires a sync between this
@@ -437,7 +464,7 @@
 #endif
 }
 
-bool MVKImageMemoryBinding::shouldFlushHostMemory() { return isMemoryHostAccessible() && !_usesTexelBuffer; }
+bool MVKImageMemoryBinding::shouldFlushHostMemory() { return isMemoryHostAccessible() && (!_mtlTexelBuffer || _ownsTexelBuffer); }
 
 // Flushes the device memory at the specified memory range into the MTLTexture. Updates
 // all subresources that overlap the specified range and are in an updatable layout state.
@@ -489,14 +516,12 @@
     return (_image->_memoryBindings.size() > 1) ? _planeIndex : (uint8_t)_image->_memoryBindings.size();
 }
 
-MVKImageMemoryBinding::MVKImageMemoryBinding(MVKDevice* device, MVKImage* image, uint8_t planeIndex) : MVKResource(device) {
-    _image = image;
-    _planeIndex = planeIndex;
-    _usesTexelBuffer = false;
+MVKImageMemoryBinding::MVKImageMemoryBinding(MVKDevice* device, MVKImage* image, uint8_t planeIndex) : MVKResource(device), _image(image), _planeIndex(planeIndex) {
 }
 
 MVKImageMemoryBinding::~MVKImageMemoryBinding() {
     if (_deviceMemory) { _deviceMemory->removeImageMemoryBinding(this); }
+    if (_ownsTexelBuffer) { [_mtlTexelBuffer release]; }
 }
 
 
@@ -529,7 +554,7 @@
 
 VkExtent3D MVKImage::getExtent3D(uint8_t planeIndex, uint32_t mipLevel) {
     VkExtent3D extent = _extent;
-    if (_hasChromaSubsampling) {
+    if (_hasChromaSubsampling && planeIndex > 0) {
         extent.width /= _planes[planeIndex]->_blockTexelSize.width;
         extent.height /= _planes[planeIndex]->_blockTexelSize.height;
     }
@@ -537,14 +562,16 @@
 }
 
 VkDeviceSize MVKImage::getBytesPerRow(uint8_t planeIndex, uint32_t mipLevel) {
-    size_t bytesPerRow = getPixelFormats()->getBytesPerRow(_vkFormat, getExtent3D(planeIndex, mipLevel).width);
+    MTLPixelFormat planeMTLPixFmt = getPixelFormats()->getChromaSubsamplingPlaneMTLPixelFormat(_vkFormat, planeIndex);
+    size_t bytesPerRow = getPixelFormats()->getBytesPerRow(planeMTLPixFmt, getExtent3D(planeIndex, mipLevel).width);
     return mvkAlignByteCount(bytesPerRow, _rowByteAlignment);
 }
 
 VkDeviceSize MVKImage::getBytesPerLayer(uint8_t planeIndex, uint32_t mipLevel) {
+    MTLPixelFormat planeMTLPixFmt = getPixelFormats()->getChromaSubsamplingPlaneMTLPixelFormat(_vkFormat, planeIndex);
     VkExtent3D extent = getExtent3D(planeIndex, mipLevel);
     size_t bytesPerRow = getBytesPerRow(planeIndex, mipLevel);
-    return getPixelFormats()->getBytesPerLayer(_vkFormat, bytesPerRow, extent.height);
+    return getPixelFormats()->getBytesPerLayer(planeMTLPixFmt, bytesPerRow, extent.height);
 }
 
 VkResult MVKImage::getSubresourceLayout(const VkImageSubresource* pSubresource,
@@ -804,8 +831,16 @@
 	MVKPixelFormats* pixFmts = getPixelFormats();
     _vkFormat = pCreateInfo->format;
 	_usage = pCreateInfo->usage;
+	_hasMutableFormat = mvkIsAnyFlagEnabled(pCreateInfo->flags, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT);
 	_hasExtendedUsage = mvkIsAnyFlagEnabled(pCreateInfo->flags, VK_IMAGE_CREATE_EXTENDED_USAGE_BIT);
 
+    // If this is a storage image of format R32_UINT or R32_SINT, or MUTABLE_FORMAT is set
+    // and R32_UINT is in the set of possible view formats, then we must use a texel buffer,
+    // or image atomics won't work.
+    // TODO: Also add handling for VK_KHR_image_format_list here.
+    _isLinearForAtomics = _isLinear && mvkIsAnyFlagEnabled(_usage, VK_IMAGE_USAGE_STORAGE_BIT) &&
+                          ((_vkFormat == VK_FORMAT_R32_UINT || _vkFormat == VK_FORMAT_R32_SINT) ||
+                           (_hasMutableFormat && pixFmts->getViewClass(_vkFormat) == MVKMTLViewClass::Color32));
 	_is3DCompressed = (getImageType() == VK_IMAGE_TYPE_3D) && (pixFmts->getFormatType(pCreateInfo->format) == kMVKFormatCompressed) && !_device->_pMetalFeatures->native3DCompressedTextures;
 	_isDepthStencilAttachment = (mvkAreAllFlagsEnabled(pCreateInfo->usage, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) ||
 								 mvkAreAllFlagsEnabled(pixFmts->getVkFormatProperties(pCreateInfo->format).optimalTilingFeatures, VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT));
@@ -842,6 +877,16 @@
             memoryBinding->_byteCount += sizeAndAlign.size;
             memoryBinding->_byteAlignment = std::max(memoryBinding->_byteAlignment, (VkDeviceSize)sizeAndAlign.align);
             _isAliasable = mvkIsAnyFlagEnabled(pCreateInfo->flags, VK_IMAGE_CREATE_ALIAS_BIT);
+        } else if (_isLinearForAtomics && _device->_pMetalFeatures->placementHeaps) {
+            NSUInteger bufferLength = 0;
+            for (uint32_t mipLvl = 0; mipLvl < _mipLevels; mipLvl++) {
+                VkExtent3D mipExtent = getExtent3D(planeIndex, mipLvl);
+                bufferLength += getBytesPerLayer(planeIndex, mipLvl) * mipExtent.depth * _arrayLayers;
+            }
+            MTLSizeAndAlign sizeAndAlign = [_device->getMTLDevice() heapBufferSizeAndAlignWithLength: bufferLength options: MTLResourceStorageModePrivate];
+            memoryBinding->_byteCount += sizeAndAlign.size;
+            memoryBinding->_byteAlignment = std::max(std::max(memoryBinding->_byteAlignment, _rowByteAlignment), (VkDeviceSize)sizeAndAlign.align);
+            _isAliasable = mvkIsAnyFlagEnabled(pCreateInfo->flags, VK_IMAGE_CREATE_ALIAS_BIT);
         } else {
             for (uint32_t mipLvl = 0; mipLvl < _mipLevels; mipLvl++) {
                 VkExtent3D mipExtent = getExtent3D(planeIndex, mipLvl);
@@ -881,6 +926,10 @@
 		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Under Metal, multisampling cannot be used with compressed images. Setting sample count to 1."));
 		validSamples = VK_SAMPLE_COUNT_1_BIT;
 	}
+	if (getPixelFormats()->getChromaSubsamplingPlaneCount(pCreateInfo->format) > 0) {
+		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Under Metal, multisampling cannot be used with chroma subsampled images. Setting sample count to 1."));
+		validSamples = VK_SAMPLE_COUNT_1_BIT;
+	}
 
 	if (pCreateInfo->arrayLayers > 1) {
 		if ( !_device->_pMetalFeatures->multisampleArrayTextures ) {
@@ -901,6 +950,7 @@
 
 	bool is2D = (getImageType() == VK_IMAGE_TYPE_2D);
 	bool isCompressed = pixFmts->getFormatType(pCreateInfo->format) == kMVKFormatCompressed;
+	bool isChromaSubsampled = pixFmts->getChromaSubsamplingPlaneCount(pCreateInfo->format) > 0;
 
 #if MVK_IOS_OR_TVOS
 	if (isCompressed && !is2D) {
@@ -917,6 +967,16 @@
 	}
 #endif
 
+	if (isChromaSubsampled && !is2D) {
+		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Under Metal, chroma subsampled formats may only be used with 2D images."));
+	}
+	if (isChromaSubsampled && mvkIsAnyFlagEnabled(pCreateInfo->flags, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT)) {
+		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Under Metal, chroma subsampled formats may not be used with cube images."));
+	}
+	if (isChromaSubsampled && (pCreateInfo->arrayLayers > 1)) {
+		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Chroma-subsampled formats may only have one array layer."));
+	}
+
 	if ((pixFmts->getFormatType(pCreateInfo->format) == kMVKFormatDepthStencil) && !is2D ) {
 		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Under Metal, depth/stencil formats may only be used with 2D images."));
 	}
@@ -940,6 +1000,10 @@
 
 	if (validMipLevels == 1) { return validMipLevels; }
 
+	if (getPixelFormats()->getChromaSubsamplingPlaneCount(pCreateInfo->format) == 1) {
+		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Under Metal, GBGR and BGRG images cannot use mipmaps. Setting mip levels to 1."));
+		validMipLevels = 1;
+	}
 	if (getImageType() == VK_IMAGE_TYPE_1D) {
 		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : Under Metal, native 1D images cannot use mipmaps. Setting mip levels to 1. Consider enabling MVK_CONFIG_TEXTURE_1D_AS_2D."));
 		validMipLevels = 1;
@@ -967,6 +1031,10 @@
 		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : If tiling is VK_IMAGE_TILING_LINEAR, format must not be a compressed format."));
 		isLin = false;
 	}
+	if (getPixelFormats()->getChromaSubsamplingPlaneCount(pCreateInfo->format) == 1) {
+		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : If tiling is VK_IMAGE_TILING_LINEAR, format must not be a single-plane chroma subsampled format."));
+		isLin = false;
+	}
 
 	if (pCreateInfo->mipLevels > 1) {
 		setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "vkCreateImage() : If tiling is VK_IMAGE_TILING_LINEAR, mipLevels must be 1."));
@@ -1318,7 +1386,7 @@
         mtlTextureType = MTLTextureType3D;
         sliceRange = NSMakeRange(0, 1);
     }
-    id<MTLTexture> mtlTex = _imageView->_image->getMTLTexture(MVKImage::getPlaneFromVkImageAspectFlags(_imageView->_subresourceRange.aspectMask));
+    id<MTLTexture> mtlTex = _imageView->_image->getMTLTexture(_planeIndex);
     if (_device->_pMetalFeatures->nativeTextureSwizzle && _packedSwizzle) {
         return [mtlTex newTextureViewWithPixelFormat: _mtlPixFmt
                                          textureType: mtlTextureType
@@ -1347,6 +1415,7 @@
 
     bool useSwizzle;
 	_imageView->setConfigurationResult(_imageView->validateSwizzledMTLPixelFormat(pCreateInfo,
+																				  _imageView->_usage,
 																				  _imageView,
 																				  _device->_pMetalFeatures->nativeTextureSwizzle,
 																				  _device->_pMVKConfig->fullImageViewSwizzle,
@@ -1471,15 +1540,15 @@
             beginPlaneIndex = 0,
             endPlaneIndex = subsamplingPlaneCount;
     if (subsamplingPlaneCount == 0) {
-        endPlaneIndex = 1;
-        mtlPixFmtOfPlane[0] = getPixelFormats()->getMTLPixelFormat(pCreateInfo->format);
+        if (_subresourceRange.aspectMask & (VK_IMAGE_ASPECT_PLANE_0_BIT | VK_IMAGE_ASPECT_PLANE_1_BIT | VK_IMAGE_ASPECT_PLANE_2_BIT)) {
+            beginPlaneIndex = MVKImage::getPlaneFromVkImageAspectFlags(_subresourceRange.aspectMask);
+        }
+        endPlaneIndex = beginPlaneIndex + 1;
+        mtlPixFmtOfPlane[beginPlaneIndex] = getPixelFormats()->getMTLPixelFormat(pCreateInfo->format);
     } else {
         if (!mvkVkComponentMappingsMatch(pCreateInfo->components, {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A})) {
             setConfigurationResult(reportError(VK_ERROR_FEATURE_NOT_PRESENT, "Image view swizzling for multi planar formats is not supported."));
         }
-        if (_subresourceRange.aspectMask & (VK_IMAGE_ASPECT_PLANE_0_BIT | VK_IMAGE_ASPECT_PLANE_1_BIT | VK_IMAGE_ASPECT_PLANE_2_BIT)) {
-            beginPlaneIndex = endPlaneIndex = MVKImage::getPlaneFromVkImageAspectFlags(_subresourceRange.aspectMask);
-        }
     }
     for (uint8_t planeIndex = beginPlaneIndex; planeIndex < endPlaneIndex; planeIndex++) {
         _planes.push_back(new MVKImageViewPlane(this, planeIndex, mtlPixFmtOfPlane[planeIndex], pCreateInfo));
@@ -1487,6 +1556,7 @@
 }
 
 VkResult MVKImageView::validateSwizzledMTLPixelFormat(const VkImageViewCreateInfo* pCreateInfo,
+													  VkImageUsageFlags usage,
 													  MVKVulkanAPIObject* apiObject,
 													  bool hasNativeSwizzleSupport,
 													  bool hasShaderSwizzleSupport,
@@ -1501,7 +1571,8 @@
 	// If we have an identity swizzle, we're all good.
 	if (SWIZZLE_MATCHES(R, G, B, A)) {
 		// Change to stencil-only format if only stencil aspect is requested
-		if (pCreateInfo->subresourceRange.aspectMask == VK_IMAGE_ASPECT_STENCIL_BIT) {
+		if (pCreateInfo->subresourceRange.aspectMask == VK_IMAGE_ASPECT_STENCIL_BIT &&
+			mvkIsAnyFlagEnabled(usage, (VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT))) {
 			if (mtlPixFmt == MTLPixelFormatDepth32Float_Stencil8)
 				mtlPixFmt = MTLPixelFormatX32_Stencil8;
 #if MVK_MACOS
@@ -1558,7 +1629,8 @@
 
 		case MTLPixelFormatDepth32Float_Stencil8:
 			// If aspect mask looking only for stencil then change to stencil-only format even if shader swizzling is needed
-			if (pCreateInfo->subresourceRange.aspectMask == VK_IMAGE_ASPECT_STENCIL_BIT) {
+			if (pCreateInfo->subresourceRange.aspectMask == VK_IMAGE_ASPECT_STENCIL_BIT &&
+				mvkIsAnyFlagEnabled(usage, (VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT))) {
 				mtlPixFmt = MTLPixelFormatX32_Stencil8;
 				if (SWIZZLE_MATCHES(R, ANY, ANY, ANY)) {
 					return VK_SUCCESS;
@@ -1569,7 +1641,8 @@
 #if MVK_MACOS
 		case MTLPixelFormatDepth24Unorm_Stencil8:
 			// If aspect mask looking only for stencil then change to stencil-only format even if shader swizzling is needed
-			if (pCreateInfo->subresourceRange.aspectMask == VK_IMAGE_ASPECT_STENCIL_BIT) {
+			if (pCreateInfo->subresourceRange.aspectMask == VK_IMAGE_ASPECT_STENCIL_BIT &&
+				mvkIsAnyFlagEnabled(usage, (VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT))) {
 				mtlPixFmt = MTLPixelFormatX24_Stencil8;
 				if (SWIZZLE_MATCHES(R, ANY, ANY, ANY)) {
 					return VK_SUCCESS;
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h b/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h
index e87063e..fa576ab 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.h
@@ -256,6 +256,9 @@
 	/** Returns the MSLFormatResolution of the specified chroma-subsampling (YCbCr) VkFormat */
 	SPIRV_CROSS_NAMESPACE::MSLFormatResolution getChromaSubsamplingResolution(VkFormat vkFormat);
 
+	/** Returns the MTLPixelFormat of the specified chroma-subsampling (YCbCr) VkFormat for the specified plane. */
+	MTLPixelFormat getChromaSubsamplingPlaneMTLPixelFormat(VkFormat vkFormat, uint8_t planeIndex);
+
     /** Returns the number of planes, blockTexelSize,  bytesPerBlock and mtlPixFmt of each plane of the specified chroma-subsampling (YCbCr) VkFormat into the given arrays */
     uint8_t getChromaSubsamplingPlanes(VkFormat vkFormat, VkExtent2D blockTexelSize[3], uint32_t bytesPerBlock[3], MTLPixelFormat mtlPixFmt[3]);
 
@@ -315,6 +318,9 @@
 	/** Returns the Metal format capabilities supported by the specified Metal format. */
 	MVKMTLFmtCaps getCapabilities(MTLPixelFormat mtlFormat, bool isExtended = false);
 
+	/** Returns the Metal view class of the specified Vulkan format. */
+	MVKMTLViewClass getViewClass(VkFormat vkFormat);
+
 	/** Returns the Metal view class of the specified Metal format. */
 	MVKMTLViewClass getViewClass(MTLPixelFormat mtlFormat);
 
@@ -350,6 +356,7 @@
 									   MTLPixelFormat mtlFormat,
 									   MTLTextureUsage minUsage = MTLTextureUsageUnknown,
                                        bool isLinear = false,
+                                       bool isMutableFormat = true,
                                        bool isExtended = false);
 
 	/** Enumerates all formats that support the given features, calling a specified function for each one. */
diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.mm b/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.mm
index d27b4b2..d937e7b 100644
--- a/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.mm
+++ b/MoltenVK/MoltenVK/GPUObjects/MVKPixelFormats.mm
@@ -190,7 +190,7 @@
 
 	// If the MTLPixelFormat is not supported but VkFormat is valid,
 	// attempt to substitute a different format and potentially report an error.
-	if ( !mtlPixFmt && vkFormat ) {
+	if ( !mtlPixFmt && vkFormat && vkDesc.chromaSubsamplingPlaneCount <= 1 ) {
 		mtlPixFmt = vkDesc.mtlPixelFormatSubstitute;
 
 		// Report an error if there is no substitute, or the first time a substitution is made.
@@ -250,6 +250,23 @@
                                        : SPIRV_CROSS_NAMESPACE::MSL_FORMAT_RESOLUTION_420;
 }
 
+MTLPixelFormat MVKPixelFormats::getChromaSubsamplingPlaneMTLPixelFormat(VkFormat vkFormat, uint8_t planeIndex) {
+    uint8_t planes = getChromaSubsamplingPlaneCount(vkFormat);
+    uint8_t bits = getChromaSubsamplingComponentBits(vkFormat);
+    switch(planes) {
+        default:
+        case 1:
+            return getMTLPixelFormat(vkFormat);
+        case 2:
+            if (planeIndex == 1) {
+                return (bits == 8) ? MTLPixelFormatRG8Unorm : MTLPixelFormatRG16Unorm;
+            }
+            /* fallthrough */
+        case 3:
+            return (bits == 8) ? MTLPixelFormatR8Unorm : MTLPixelFormatR16Unorm;
+    }
+}
+
 uint8_t MVKPixelFormats::getChromaSubsamplingPlanes(VkFormat vkFormat, VkExtent2D blockTexelSize[3], uint32_t bytesPerBlock[3], MTLPixelFormat mtlPixFmt[3]) {
     uint8_t planes = getChromaSubsamplingPlaneCount(vkFormat);
     uint8_t bits = getChromaSubsamplingComponentBits(vkFormat);
@@ -273,7 +290,7 @@
             return 0;
         case 1:
             bytesPerBlock[0] *= 4;
-            mtlPixFmt[0] = (bits == 8) ? MTLPixelFormatRGBA8Unorm : MTLPixelFormatRGBA16Unorm;
+            mtlPixFmt[0] = getMTLPixelFormat(vkFormat);
             break;
         case 2:
             blockTexelSize[0] = VkExtent2D{1, 1};
@@ -335,6 +352,10 @@
     return caps;
 }
 
+MVKMTLViewClass MVKPixelFormats::getViewClass(VkFormat vkFormat) {
+    return getMTLPixelFormatDesc(getVkFormatDesc(vkFormat).mtlPixelFormat).mtlViewClass;
+}
+
 MVKMTLViewClass MVKPixelFormats::getViewClass(MTLPixelFormat mtlFormat) {
     return getMTLPixelFormatDesc(mtlFormat).mtlViewClass;
 }
@@ -465,6 +486,7 @@
 													MTLPixelFormat mtlFormat,
 													MTLTextureUsage minUsage,
                                                     bool isLinear,
+                                                    bool isMutableFormat,
                                                     bool isExtended) {
 	bool isDepthFmt = isDepthFormat(mtlFormat);
 	bool isStencilFmt = isStencilFormat(mtlFormat);
@@ -514,13 +536,21 @@
 	}
 
 	// Create view on, but only on color formats, or combined depth-stencil formats if supported by the GPU...
-	if (mvkIsAnyFlagEnabled(vkImageUsageFlags, (VK_IMAGE_USAGE_TRANSFER_SRC_BIT |	 		// May use temp view if transfer involves format change
-												VK_IMAGE_USAGE_SAMPLED_BIT |
+	if ((mvkIsAnyFlagEnabled(vkImageUsageFlags, (VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) || 		// May use temp view if transfer involves format change
+		 (isMutableFormat &&
+		  mvkIsAnyFlagEnabled(vkImageUsageFlags, (VK_IMAGE_USAGE_SAMPLED_BIT |
+												  VK_IMAGE_USAGE_STORAGE_BIT |
+												  VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT |
+												  VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)))) &&
+		isColorFormat) {
+
+		mvkEnableFlags(mtlUsage, MTLTextureUsagePixelFormatView);
+	}
+	if (mvkIsAnyFlagEnabled(vkImageUsageFlags, (VK_IMAGE_USAGE_TRANSFER_SRC_BIT | 		// May use temp view if transfer involves format change
+		 										VK_IMAGE_USAGE_SAMPLED_BIT |
 												VK_IMAGE_USAGE_STORAGE_BIT |
-												VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT |
-												VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
-												VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT)) &&
-		(isColorFormat || (isCombinedDepthStencilFmt && supportsStencilViews))) {
+												VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT)) &&
+		isCombinedDepthStencilFmt && supportsStencilViews) {
 
 		mvkEnableFlags(mtlUsage, MTLTextureUsagePixelFormatView);
 	}
@@ -837,9 +867,9 @@
     addVkFormatDescChromaSubsampling( G8_B8_R8_3PLANE_422_UNORM, Invalid, 3, 8, 2, 1, 4 );
     addVkFormatDescChromaSubsampling( G8_B8R8_2PLANE_422_UNORM, Invalid, 2, 8, 2, 1, 4 );
     addVkFormatDescChromaSubsampling( G8_B8_R8_3PLANE_444_UNORM, Invalid, 3, 8, 1, 1, 3 );
-    addVkFormatDescChromaSubsampling( R10X6_UNORM_PACK16, Invalid, 0, 10, 1, 1, 2 );
-    addVkFormatDescChromaSubsampling( R10X6G10X6_UNORM_2PACK16, Invalid, 0, 10, 1, 1, 4 );
-    addVkFormatDescChromaSubsampling( R10X6G10X6B10X6A10X6_UNORM_4PACK16, Invalid, 0, 10, 1, 1, 8 );
+    addVkFormatDescChromaSubsampling( R10X6_UNORM_PACK16, R16Unorm, 0, 10, 1, 1, 2 );
+    addVkFormatDescChromaSubsampling( R10X6G10X6_UNORM_2PACK16, RG16Unorm, 0, 10, 1, 1, 4 );
+    addVkFormatDescChromaSubsampling( R10X6G10X6B10X6A10X6_UNORM_4PACK16, RGBA16Unorm, 0, 10, 1, 1, 8 );
     addVkFormatDescChromaSubsampling( G10X6B10X6G10X6R10X6_422_UNORM_4PACK16, Invalid, 1, 10, 2, 1, 8 );
     addVkFormatDescChromaSubsampling( B10X6G10X6R10X6G10X6_422_UNORM_4PACK16, Invalid, 1, 10, 2, 1, 8 );
     addVkFormatDescChromaSubsampling( G10X6_B10X6_R10X6_3PLANE_420_UNORM_3PACK16, Invalid, 3, 10, 2, 2, 12 );
@@ -847,9 +877,9 @@
     addVkFormatDescChromaSubsampling( G10X6_B10X6_R10X6_3PLANE_422_UNORM_3PACK16, Invalid, 3, 10, 2, 1, 8 );
     addVkFormatDescChromaSubsampling( G10X6_B10X6R10X6_2PLANE_422_UNORM_3PACK16, Invalid, 2, 10, 2, 1, 8 );
     addVkFormatDescChromaSubsampling( G10X6_B10X6_R10X6_3PLANE_444_UNORM_3PACK16, Invalid, 3, 10, 1, 1, 6 );
-    addVkFormatDescChromaSubsampling( R12X4_UNORM_PACK16, Invalid, 0, 12, 1, 1, 2 );
-    addVkFormatDescChromaSubsampling( R12X4G12X4_UNORM_2PACK16, Invalid, 0, 12, 1, 1, 4 );
-    addVkFormatDescChromaSubsampling( R12X4G12X4B12X4A12X4_UNORM_4PACK16, Invalid, 0, 12, 1, 1, 8 );
+    addVkFormatDescChromaSubsampling( R12X4_UNORM_PACK16, R16Unorm, 0, 12, 1, 1, 2 );
+    addVkFormatDescChromaSubsampling( R12X4G12X4_UNORM_2PACK16, RG16Unorm, 0, 12, 1, 1, 4 );
+    addVkFormatDescChromaSubsampling( R12X4G12X4B12X4A12X4_UNORM_4PACK16, RGBA16Unorm, 0, 12, 1, 1, 8 );
     addVkFormatDescChromaSubsampling( G12X4B12X4G12X4R12X4_422_UNORM_4PACK16, Invalid, 1, 12, 2, 1, 8 );
     addVkFormatDescChromaSubsampling( B12X4G12X4R12X4G12X4_422_UNORM_4PACK16, Invalid, 1, 12, 2, 1, 8 );
     addVkFormatDescChromaSubsampling( G12X4_B12X4_R12X4_3PLANE_420_UNORM_3PACK16, Invalid, 3, 12, 2, 2, 12 );
@@ -1620,9 +1650,7 @@
 	kMVKVkFormatFeatureFlagsTexDSAtt    = (VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT),
 	kMVKVkFormatFeatureFlagsTexBlend    = (VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT),
     kMVKVkFormatFeatureFlagsTexTransfer          = (VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
-                                                    VK_FORMAT_FEATURE_TRANSFER_DST_BIT |
-                                                    VK_FORMAT_FEATURE_BLIT_SRC_BIT |
-                                                    VK_FORMAT_FEATURE_BLIT_DST_BIT),
+                                                    VK_FORMAT_FEATURE_TRANSFER_DST_BIT),
     kMVKVkFormatFeatureFlagsTexChromaSubsampling = (VK_FORMAT_FEATURE_MIDPOINT_CHROMA_SAMPLES_BIT_KHR |
                                                     VK_FORMAT_FEATURE_SAMPLED_IMAGE_YCBCR_CONVERSION_LINEAR_FILTER_BIT_KHR),
     kMVKVkFormatFeatureFlagsTexMultiPlanar       = (VK_FORMAT_FEATURE_COSITED_CHROMA_SAMPLES_BIT_KHR |
@@ -1650,12 +1678,13 @@
     vkProps.linearTilingFeatures = kMVKVkFormatFeatureFlagsTexNone;
 
     // Chroma subsampling and multi planar features
-    if (getChromaSubsamplingComponentBits(vkDesc.vkFormat) > 0) {
-        vkProps.optimalTilingFeatures = kMVKVkFormatFeatureFlagsTexTransfer;
-    }
     uint8_t chromaSubsamplingPlaneCount = getChromaSubsamplingPlaneCount(vkDesc.vkFormat);
-    if (chromaSubsamplingPlaneCount > 0) {
-        mtlPixFmtCaps = kMVKMTLFmtCapsRF;
+    uint8_t chromaSubsamplingComponentBits = getChromaSubsamplingComponentBits(vkDesc.vkFormat);
+    if (chromaSubsamplingComponentBits > 0) {
+        if (mtlPixFmtCaps != 0 || chromaSubsamplingPlaneCount > 1) {
+            mtlPixFmtCaps = kMVKMTLFmtCapsRF;
+            vkProps.optimalTilingFeatures = kMVKVkFormatFeatureFlagsTexTransfer;
+        }
         enableFormatFeatures(ChromaSubsampling, Tex, mtlPixFmtCaps, vkProps.optimalTilingFeatures);
     }
     if (chromaSubsamplingPlaneCount > 1) {
@@ -1670,8 +1699,15 @@
 	enableFormatFeatures(DSAtt, Tex, mtlPixFmtCaps, vkProps.optimalTilingFeatures);
 	enableFormatFeatures(Blend, Tex, mtlPixFmtCaps, vkProps.optimalTilingFeatures);
 
+	if (chromaSubsamplingComponentBits > 0) {
+		// Vulkan forbids blits between chroma-subsampled formats.
+		mvkDisableFlags(vkProps.optimalTilingFeatures, (VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT));
+	}
+
 	// Linear tiling is not available to depth/stencil or compressed formats.
-	if ( !(vkDesc.formatType == kMVKFormatDepthStencil || vkDesc.formatType == kMVKFormatCompressed) ) {
+	// GBGR and BGRG formats also do not support linear tiling in Metal.
+	if ( !(vkDesc.formatType == kMVKFormatDepthStencil || vkDesc.formatType == kMVKFormatCompressed ||
+		   (chromaSubsamplingPlaneCount == 1 && vkDesc.blockTexelSize.width > 1)) ) {
 		// Start with optimal tiling features, and modify.
 		vkProps.linearTilingFeatures = vkProps.optimalTilingFeatures;
 
@@ -1686,9 +1722,10 @@
 #endif
 	}
 
-	// Texel buffers are not available to depth/stencil or compressed formats.
+	// Texel buffers are not available to depth/stencil, compressed, or chroma subsampled formats.
 	vkProps.bufferFeatures = kMVKVkFormatFeatureFlagsTexNone;
-	if ( !(vkDesc.formatType == kMVKFormatDepthStencil || vkDesc.formatType == kMVKFormatCompressed) ) {
+	if ( !(vkDesc.formatType == kMVKFormatDepthStencil || vkDesc.formatType == kMVKFormatCompressed ||
+		   chromaSubsamplingComponentBits > 0) ) {
 		enableFormatFeatures(Read, Buf, mtlPixFmtCaps, vkProps.bufferFeatures);
 		enableFormatFeatures(Write, Buf, mtlPixFmtCaps, vkProps.bufferFeatures);
 		enableFormatFeatures(Atomic, Buf, mtlPixFmtCaps, vkProps.bufferFeatures);