Merge "Initial drop of LLVM 10 to third_party/llvm-10.0"
diff --git a/src/Device/Blitter.hpp b/src/Device/Blitter.hpp
index 435cbaf..8006945 100644
--- a/src/Device/Blitter.hpp
+++ b/src/Device/Blitter.hpp
@@ -89,12 +89,6 @@
 		    , destSamples(destSamples)
 		{}
 
-		bool operator==(const State &state) const
-		{
-			static_assert(is_memcmparable<State>::value, "Cannot memcmp State");
-			return memcmp(this, &state, sizeof(State)) == 0;
-		}
-
 		vk::Format sourceFormat;
 		vk::Format destFormat;
 		int srcSamples = 0;
diff --git a/src/Device/LRUCache.hpp b/src/Device/LRUCache.hpp
index d4d2bdb..dedca45 100644
--- a/src/Device/LRUCache.hpp
+++ b/src/Device/LRUCache.hpp
@@ -73,19 +73,6 @@
 	std::unordered_map<Key, Data, Hasher> constCache;
 };
 
-// Traits-like helper class for checking if objects can be compared using memcmp().
-// Useful for statically asserting if a cache key can implement operator==() with memcmp().
-template<typename T>
-struct is_memcmparable
-{
-// std::is_trivially_copyable is not available in older GCC versions.
-#if !defined(__GNUC__) || __GNUC__ > 5
-	static const bool value = std::is_trivially_copyable<T>::value;
-#else
-	// At least check it doesn't have virtual methods.
-	static const bool value = !std::is_polymorphic<T>::value;
-#endif
-};
 }  // namespace sw
 
 namespace sw {
diff --git a/src/Device/Memset.hpp b/src/Device/Memset.hpp
index 6ce53ae..877d10b 100644
--- a/src/Device/Memset.hpp
+++ b/src/Device/Memset.hpp
@@ -18,10 +18,19 @@
 #include <cstring>
 #include <type_traits>
 
+// GCC 8+ warns that
+// "‘void* memset(void*, int, size_t)’ clearing an object of non-trivial type ‘T’;
+//  use assignment or value-initialization instead [-Werror=class-memaccess]"
+// This is benign iff it happens before any of the base or member constructors are called.
+#if defined(__GNUC__) && (__GNUC__ >= 8)
+#	pragma GCC diagnostic push
+#	pragma GCC diagnostic ignored "-Wclass-memaccess"
+#endif
+
 namespace sw {
 
-// Helper class for clearing the memory of objects at construction.
-// Useful as the first base class of cache keys which may contain padding
+// Memset<> is a helper class for clearing the memory of objects at construction.
+// It is useful as the *first* base class of map keys which may contain padding
 // bytes or bits otherwise left uninitialized.
 template<class T>
 struct Memset
@@ -29,24 +38,49 @@
 	Memset(T *object, int val)
 	{
 		static_assert(std::is_base_of<Memset<T>, T>::value, "Memset<T> must only clear the memory of a type of which it is a base class");
+		::memset(object, 0, sizeof(T));
+	}
 
-// GCC 8+ warns that
-// "‘void* memset(void*, int, size_t)’ clearing an object of non-trivial type ‘T’;
-//  use assignment or value-initialization instead [-Werror=class-memaccess]"
-// This is benign iff it happens before any of the base or member constructrs are called.
-#if defined(__GNUC__) && (__GNUC__ >= 8)
-#	pragma GCC diagnostic push
-#	pragma GCC diagnostic ignored "-Wclass-memaccess"
-#endif
+	// Don't rely on the implicitly declared copy constructor and copy assignment operator.
+	// They can leave padding bytes uninitialized.
+	Memset(const Memset &rhs)
+	{
+		::memcpy(this, &rhs, sizeof(T));
+	}
 
-		memset(object, 0, sizeof(T));
+	Memset &operator=(const Memset &rhs)
+	{
+		::memcpy(this, &rhs, sizeof(T));
+		return *this;
+	}
 
-#if defined(__GNUC__) && (__GNUC__ >= 8)
-#	pragma GCC diagnostic pop
-#endif
+	// The compiler won't declare an implicit move constructor and move assignment operator
+	// due to having a user-defined copy constructor and copy assignment operator. Delete
+	// them for explicitness. We always want memcpy() being called.
+	Memset(const Memset &&rhs) = delete;
+	Memset &operator=(const Memset &&rhs) = delete;
+
+	friend bool operator==(const T &a, const T &b)
+	{
+		return ::memcmp(&a, &b, sizeof(T)) == 0;
+	}
+
+	friend bool operator!=(const T &a, const T &b)
+	{
+		return ::memcmp(&a, &b, sizeof(T)) != 0;
+	}
+
+	friend bool operator<(const T &a, const T &b)
+	{
+		return ::memcmp(&a, &b, sizeof(T)) < 0;
 	}
 };
 
 }  // namespace sw
 
+// Restore -Wclass-memaccess
+#if defined(__GNUC__) && (__GNUC__ >= 8)
+#	pragma GCC diagnostic pop
+#endif
+
 #endif  // sw_Memset_hpp
\ No newline at end of file
diff --git a/src/Device/PixelProcessor.cpp b/src/Device/PixelProcessor.cpp
index b24d2d3..6e05935 100644
--- a/src/Device/PixelProcessor.cpp
+++ b/src/Device/PixelProcessor.cpp
@@ -44,8 +44,7 @@
 		return false;
 	}
 
-	static_assert(is_memcmparable<State>::value, "Cannot memcmp State");
-	return memcmp(static_cast<const States *>(this), static_cast<const States *>(&state), sizeof(States)) == 0;
+	return *static_cast<const States *>(this) == static_cast<const States &>(state);
 }
 
 PixelProcessor::PixelProcessor()
diff --git a/src/Device/SetupProcessor.cpp b/src/Device/SetupProcessor.cpp
index 9a9a15f..19e7843 100644
--- a/src/Device/SetupProcessor.cpp
+++ b/src/Device/SetupProcessor.cpp
@@ -47,8 +47,7 @@
 		return false;
 	}
 
-	static_assert(is_memcmparable<State>::value, "Cannot memcmp States");
-	return memcmp(static_cast<const States *>(this), static_cast<const States *>(&state), sizeof(States)) == 0;
+	return *static_cast<const States *>(this) == static_cast<const States &>(state);
 }
 
 SetupProcessor::SetupProcessor()
diff --git a/src/Device/VertexProcessor.cpp b/src/Device/VertexProcessor.cpp
index c7f4d54..a2a33c5 100644
--- a/src/Device/VertexProcessor.cpp
+++ b/src/Device/VertexProcessor.cpp
@@ -51,8 +51,7 @@
 		return false;
 	}
 
-	static_assert(is_memcmparable<State>::value, "Cannot memcmp States");
-	return memcmp(static_cast<const States *>(this), static_cast<const States *>(&state), sizeof(States)) == 0;
+	return *static_cast<const States *>(this) == static_cast<const States &>(state);
 }
 
 VertexProcessor::VertexProcessor()
diff --git a/src/Pipeline/SpirvShaderSampling.cpp b/src/Pipeline/SpirvShaderSampling.cpp
index 23acd23..5434645 100644
--- a/src/Pipeline/SpirvShaderSampling.cpp
+++ b/src/Pipeline/SpirvShaderSampling.cpp
@@ -84,12 +84,9 @@
 		samplerState.compareOp = sampler->compareOp;
 		samplerState.unnormalizedCoordinates = (sampler->unnormalizedCoordinates != VK_FALSE);
 
-		if(sampler->ycbcrConversion)
-		{
-			samplerState.ycbcrModel = sampler->ycbcrConversion->ycbcrModel;
-			samplerState.studioSwing = (sampler->ycbcrConversion->ycbcrRange == VK_SAMPLER_YCBCR_RANGE_ITU_NARROW);
-			samplerState.swappedChroma = (sampler->ycbcrConversion->components.r != VK_COMPONENT_SWIZZLE_R);
-		}
+		samplerState.ycbcrModel = sampler->ycbcrModel;
+		samplerState.studioSwing = sampler->studioSwing;
+		samplerState.swappedChroma = sampler->swappedChroma;
 
 		samplerState.mipLodBias = sampler->mipLodBias;
 		samplerState.maxAnisotropy = sampler->maxAnisotropy;
@@ -270,7 +267,7 @@
 		return MIPMAP_POINT;  // Samplerless operations (OpImageFetch) can take an integer Lod operand.
 	}
 
-	if(sampler->ycbcrConversion)
+	if(sampler->ycbcrModel != VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY)
 	{
 		// TODO(b/151263485): Check image view level count instead.
 		return MIPMAP_NONE;
diff --git a/src/Vulkan/VkBufferView.cpp b/src/Vulkan/VkBufferView.cpp
index a8b49a0..3e048f1 100644
--- a/src/Vulkan/VkBufferView.cpp
+++ b/src/Vulkan/VkBufferView.cpp
@@ -19,7 +19,8 @@
 namespace vk {
 
 BufferView::BufferView(const VkBufferViewCreateInfo *pCreateInfo, void *mem)
-    : buffer(vk::Cast(pCreateInfo->buffer))
+    : id(pCreateInfo->format)
+    , buffer(vk::Cast(pCreateInfo->buffer))
     , format(pCreateInfo->format)
     , offset(pCreateInfo->offset)
 {
@@ -38,4 +39,4 @@
 	return buffer->getOffsetPointer(offset);
 }
 
-}  // namespace vk
\ No newline at end of file
+}  // namespace vk
diff --git a/src/Vulkan/VkBufferView.hpp b/src/Vulkan/VkBufferView.hpp
index 98b68cc..aa13fb5 100644
--- a/src/Vulkan/VkBufferView.hpp
+++ b/src/Vulkan/VkBufferView.hpp
@@ -38,7 +38,8 @@
 	uint32_t getRangeInBytes() const { return static_cast<uint32_t>(range); }
 	VkFormat getFormat() const { return format; }
 
-	const uint32_t id = ImageView::nextID++;  // ID space for sampling function cache, shared with imageviews
+	const Identifier id;
+
 private:
 	Buffer *buffer;
 	VkFormat format;
diff --git a/src/Vulkan/VkDevice.cpp b/src/Vulkan/VkDevice.cpp
index 7f9bcc0..33c9731 100644
--- a/src/Vulkan/VkDevice.cpp
+++ b/src/Vulkan/VkDevice.cpp
@@ -59,6 +59,44 @@
 	cache.updateConstCache();
 }
 
+Device::SamplerIndexer::~SamplerIndexer()
+{
+	ASSERT(map.empty());
+}
+
+uint32_t Device::SamplerIndexer::index(const SamplerState &samplerState)
+{
+	std::lock_guard<std::mutex> lock(mutex);
+
+	auto it = map.find(samplerState);
+
+	if(it != map.end())
+	{
+		it->second.count++;
+		return it->second.id;
+	}
+
+	nextID++;
+
+	map.emplace(samplerState, Identifier{ nextID, 1 });
+
+	return nextID;
+}
+
+void Device::SamplerIndexer::remove(const SamplerState &samplerState)
+{
+	std::lock_guard<std::mutex> lock(mutex);
+
+	auto it = map.find(samplerState);
+	ASSERT(it != map.end());
+
+	auto count = --it->second.count;
+	if(count == 0)
+	{
+		map.erase(it);
+	}
+}
+
 Device::Device(const VkDeviceCreateInfo *pCreateInfo, void *mem, PhysicalDevice *physicalDevice, const VkPhysicalDeviceFeatures *enabledFeatures, const std::shared_ptr<marl::Scheduler> &scheduler)
     : physicalDevice(physicalDevice)
     , queues(reinterpret_cast<Queue *>(mem))
@@ -99,6 +137,7 @@
 	// FIXME (b/119409619): use an allocator here so we can control all memory allocations
 	blitter.reset(new sw::Blitter());
 	samplingRoutineCache.reset(new SamplingRoutineCache());
+	samplerIndexer.reset(new SamplerIndexer());
 
 #ifdef ENABLE_VK_DEBUGGER
 	static auto port = getenv("VK_DEBUGGER_PORT");
@@ -279,4 +318,14 @@
 	return samplingRoutineCacheMutex;
 }
 
+uint32_t Device::indexSampler(const SamplerState &samplerState)
+{
+	return samplerIndexer->index(samplerState);
+}
+
+void Device::removeSampler(const SamplerState &samplerState)
+{
+	samplerIndexer->remove(samplerState);
+}
+
 }  // namespace vk
diff --git a/src/Vulkan/VkDevice.hpp b/src/Vulkan/VkDevice.hpp
index 0a19f50..c2ba927 100644
--- a/src/Vulkan/VkDevice.hpp
+++ b/src/Vulkan/VkDevice.hpp
@@ -16,8 +16,11 @@
 #define VK_DEVICE_HPP_
 
 #include "VkObject.hpp"
+#include "VkSampler.hpp"
 #include "Device/LRUCache.hpp"
 #include "Reactor/Routine.hpp"
+
+#include <map>
 #include <memory>
 #include <mutex>
 
@@ -98,6 +101,30 @@
 	rr::Routine *findInConstCache(const SamplingRoutineCache::Key &key) const;
 	void updateSamplingRoutineConstCache();
 
+	class SamplerIndexer
+	{
+	public:
+		~SamplerIndexer();
+
+		uint32_t index(const SamplerState &samplerState);
+		void remove(const SamplerState &samplerState);
+
+	private:
+		struct Identifier
+		{
+			uint32_t id;
+			uint32_t count;  // Number of samplers sharing this state identifier.
+		};
+
+		std::map<SamplerState, Identifier> map;  // guarded by mutex
+		std::mutex mutex;
+
+		uint32_t nextID = 0;
+	};
+
+	uint32_t indexSampler(const SamplerState &samplerState);
+	void removeSampler(const SamplerState &samplerState);
+
 	std::shared_ptr<vk::dbg::Context> getDebuggerContext() const
 	{
 #ifdef ENABLE_VK_DEBUGGER
@@ -118,7 +145,9 @@
 	typedef char ExtensionName[VK_MAX_EXTENSION_NAME_SIZE];
 	ExtensionName *extensions = nullptr;
 	const VkPhysicalDeviceFeatures enabledFeatures = {};
+
 	std::shared_ptr<marl::Scheduler> scheduler;
+	std::unique_ptr<SamplerIndexer> samplerIndexer;
 
 #ifdef ENABLE_VK_DEBUGGER
 	struct
diff --git a/src/Vulkan/VkFormat.cpp b/src/Vulkan/VkFormat.cpp
index 2638439..c3cc876 100644
--- a/src/Vulkan/VkFormat.cpp
+++ b/src/Vulkan/VkFormat.cpp
@@ -2567,4 +2567,55 @@
 	return false;
 }
 
+static constexpr uint8_t pack(VkFormat format)
+{
+	if(format > VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM)
+	{
+		return 0;
+	}
+
+	// 0 - 184 direct mapping
+	if(format >= 0 && format <= VK_FORMAT_ASTC_12x12_SRGB_BLOCK)
+	{
+		return uint8_t(format);
+	}
+
+	// 10001560xx -> 185 - 218
+	if(format >= VK_FORMAT_G8B8G8R8_422_UNORM && format <= VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM)
+	{
+		return uint8_t(format - VK_FORMAT_G8B8G8R8_422_UNORM + 185);
+	}
+
+	// 100005400x -> 219 - 226
+	if(format >= VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG && format <= VK_FORMAT_PVRTC2_4BPP_SRGB_BLOCK_IMG)
+	{
+		return uint8_t(format - VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG + 219);
+	}
+
+	// 10000660xx -> 227 - 240
+	if(format >= VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT && format <= VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK_EXT)
+	{
+		return uint8_t(format - VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT + 227);
+	}
+
+	return 0;
+}
+
+static_assert(VK_HEADER_VERSION == 128, "Update VkFormat to uint8_t mapping if needed");
+static_assert(pack(VK_FORMAT_UNDEFINED) == 0, "Incorrect VkFormat packed value");
+static_assert(pack(VK_FORMAT_ASTC_12x12_SRGB_BLOCK) == 184, "Incorrect VkFormat packed value");
+static_assert(pack(VK_FORMAT_G8B8G8R8_422_UNORM) == 185, "Incorrect VkFormat packed value");
+static_assert(pack(VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM) == 218, "Incorrect VkFormat packed value");
+static_assert(pack(VK_FORMAT_PVRTC1_2BPP_UNORM_BLOCK_IMG) == 219, "Incorrect VkFormat packed value");
+static_assert(pack(VK_FORMAT_PVRTC2_4BPP_SRGB_BLOCK_IMG) == 226, "Incorrect VkFormat packed value");
+static_assert(pack(VK_FORMAT_ASTC_4x4_SFLOAT_BLOCK_EXT) == 227, "Incorrect VkFormat packed value");
+static_assert(pack(VK_FORMAT_ASTC_12x12_SFLOAT_BLOCK_EXT) == 240, "Incorrect VkFormat packed value");
+
+uint8_t Format::mapTo8bit(VkFormat format)
+{
+	ASSERT(format <= VK_FORMAT_G16_B16_R16_3PLANE_444_UNORM);
+
+	return pack(format);
+}
+
 }  // namespace vk
diff --git a/src/Vulkan/VkFormat.h b/src/Vulkan/VkFormat.h
index 9e4b1f8..06752e1 100644
--- a/src/Vulkan/VkFormat.h
+++ b/src/Vulkan/VkFormat.h
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifndef VK_FORMAT_UTILS_HPP_
-#define VK_FORMAT_UTILS_HPP_
+#ifndef VK_FORMAT_HPP_
+#define VK_FORMAT_HPP_
 
 #include "System/Types.hpp"
 
@@ -25,7 +25,9 @@
 {
 public:
 	Format() {}
-	Format(VkFormat format) : format(format) {}
+	Format(VkFormat format)
+	    : format(format)
+	{}
 	inline operator VkFormat() const { return format; }
 
 	bool isUnsignedNormalized() const;
@@ -42,7 +44,7 @@
 	bool isFloatFormat() const;
 	bool isYcbcrFormat() const;
 
-	bool isCompatible(const Format& other) const;
+	bool isCompatible(const Format &other) const;
 	bool isCompressed() const;
 	VkFormat getDecompressedFormat() const;
 	int blockWidth() const;
@@ -65,6 +67,8 @@
 	bool has32bitIntegerTextureComponents() const;
 	bool isRGBComponent(int component) const;
 
+	static uint8_t mapTo8bit(VkFormat format);
+
 private:
 	VkFormat compatibleFormat() const;
 	int sliceBUnpadded(int width, int height, int border, bool target) const;
@@ -74,4 +78,4 @@
 
 }  // namespace vk
 
-#endif // VK_FORMAT_UTILS_HPP_
\ No newline at end of file
+#endif  // VK_FORMAT_HPP_
\ No newline at end of file
diff --git a/src/Vulkan/VkImageView.cpp b/src/Vulkan/VkImageView.cpp
index fc0c8bd..d9fbaf6 100644
--- a/src/Vulkan/VkImageView.cpp
+++ b/src/Vulkan/VkImageView.cpp
@@ -14,7 +14,9 @@
 
 #include "VkImageView.hpp"
 #include "VkImage.hpp"
-#include <System/Math.hpp>
+#include "System/Math.hpp"
+
+#include <climits>
 
 namespace {
 
@@ -54,7 +56,28 @@
 
 namespace vk {
 
-std::atomic<uint32_t> ImageView::nextID(1);
+Identifier::Identifier(const Image *image, VkImageViewType type, VkFormat fmt, VkComponentMapping mapping)
+{
+	imageViewType = type;
+	format = Format::mapTo8bit(fmt);
+	r = mapping.r;
+	g = mapping.g;
+	b = mapping.b;
+	a = mapping.a;
+
+	// TODO(b/152224843): eliminate
+	auto extent = image->getMipLevelExtent(VK_IMAGE_ASPECT_COLOR_BIT, 0);
+	large = (extent.width > SHRT_MAX) ||
+	        (extent.height > SHRT_MAX) ||
+	        (extent.depth > SHRT_MAX);
+}
+
+Identifier::Identifier(VkFormat fmt)
+{
+	static_assert(VK_IMAGE_VIEW_TYPE_END_RANGE == 6, "VkImageViewType does not allow using 7 to indicate buffer view");
+	imageViewType = 7;  // Still fits in 3-bit field
+	format = Format::mapTo8bit(fmt);
+}
 
 ImageView::ImageView(const VkImageViewCreateInfo *pCreateInfo, void *mem, const vk::SamplerYcbcrConversion *ycbcrConversion)
     : image(vk::Cast(pCreateInfo->image))
@@ -63,6 +86,7 @@
     , components(ResolveComponentMapping(pCreateInfo->components, format))
     , subresourceRange(ResolveRemainingLevelsLayers(pCreateInfo->subresourceRange, image))
     , ycbcrConversion(ycbcrConversion)
+    , id(image, viewType, format, components)
 {
 }
 
diff --git a/src/Vulkan/VkImageView.hpp b/src/Vulkan/VkImageView.hpp
index 65dca48..08ed4af 100644
--- a/src/Vulkan/VkImageView.hpp
+++ b/src/Vulkan/VkImageView.hpp
@@ -27,6 +27,36 @@
 
 class SamplerYcbcrConversion;
 
+// Uniquely identifies state used by sampling routine generation.
+// ID space shared by image views and buffer views.
+union Identifier
+{
+	// Image view identifier
+	Identifier(const Image *image, VkImageViewType type, VkFormat format, VkComponentMapping mapping);
+
+	// Buffer view identifier
+	Identifier(VkFormat format);
+
+	operator uint32_t() const
+	{
+		static_assert(sizeof(Identifier) == sizeof(uint32_t), "Identifier must be 32-bit");
+		return id;
+	}
+
+	uint32_t id = 0;
+
+	struct
+	{
+		uint32_t imageViewType : 3;
+		uint32_t format : 8;
+		uint32_t r : 3;
+		uint32_t g : 3;
+		uint32_t b : 3;
+		uint32_t a : 3;
+		uint32_t large : 1;  // Has dimension larger than SHRT_MAX (see b/133429305).
+	};
+};
+
 class ImageView : public Object<ImageView, VkImageView>
 {
 public:
@@ -82,22 +112,20 @@
 	const VkImageSubresourceRange &getSubresourceRange() const { return subresourceRange; }
 	size_t getImageSizeInBytes() const { return image->getMemoryRequirements().size; }
 
-	const uint32_t id = nextID++;
-
 private:
-	static std::atomic<uint32_t> nextID;
-	friend class BufferView;  // ImageView/BufferView share the ID space above.
-
 	bool imageTypesMatch(VkImageType imageType) const;
 	const Image *getImage(Usage usage) const;
 
 	Image *const image = nullptr;
 	const VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D;
-	const Format format;
+	const Format format = VK_FORMAT_UNDEFINED;
 	const VkComponentMapping components = {};
 	const VkImageSubresourceRange subresourceRange = {};
 
 	const vk::SamplerYcbcrConversion *ycbcrConversion = nullptr;
+
+public:
+	const Identifier id;
 };
 
 // TODO(b/132437008): Also used by SamplerYcbcrConversion. Move somewhere centrally?
diff --git a/src/Vulkan/VkSampler.cpp b/src/Vulkan/VkSampler.cpp
index df62cc2..11cb00e 100644
--- a/src/Vulkan/VkSampler.cpp
+++ b/src/Vulkan/VkSampler.cpp
@@ -16,6 +16,36 @@
 
 namespace vk {
 
-std::atomic<uint32_t> Sampler::nextID(1);
+SamplerState::SamplerState(const VkSamplerCreateInfo *pCreateInfo, const vk::SamplerYcbcrConversion *ycbcrConversion)
+    : Memset(this, 0)
+    , magFilter(pCreateInfo->magFilter)
+    , minFilter(pCreateInfo->minFilter)
+    , mipmapMode(pCreateInfo->mipmapMode)
+    , addressModeU(pCreateInfo->addressModeU)
+    , addressModeV(pCreateInfo->addressModeV)
+    , addressModeW(pCreateInfo->addressModeW)
+    , mipLodBias(pCreateInfo->mipLodBias)
+    , anisotropyEnable(pCreateInfo->anisotropyEnable)
+    , maxAnisotropy(pCreateInfo->maxAnisotropy)
+    , compareEnable(pCreateInfo->compareEnable)
+    , compareOp(pCreateInfo->compareOp)
+    , minLod(ClampLod(pCreateInfo->minLod))
+    , maxLod(ClampLod(pCreateInfo->maxLod))
+    , borderColor(pCreateInfo->borderColor)
+    , unnormalizedCoordinates(pCreateInfo->unnormalizedCoordinates)
+{
+	if(ycbcrConversion)
+	{
+		ycbcrModel = ycbcrConversion->ycbcrModel;
+		studioSwing = (ycbcrConversion->ycbcrRange == VK_SAMPLER_YCBCR_RANGE_ITU_NARROW);
+		swappedChroma = (ycbcrConversion->components.r != VK_COMPONENT_SWIZZLE_R);
+	}
+}
+
+Sampler::Sampler(const VkSamplerCreateInfo *pCreateInfo, void *mem, const SamplerState &samplerState, uint32_t samplerID)
+    : SamplerState(samplerState)
+    , id(samplerID)
+{
+}
 
 }  // namespace vk
diff --git a/src/Vulkan/VkSampler.hpp b/src/Vulkan/VkSampler.hpp
index 5a6ecf0..d2f51ac 100644
--- a/src/Vulkan/VkSampler.hpp
+++ b/src/Vulkan/VkSampler.hpp
@@ -15,42 +15,18 @@
 #ifndef VK_SAMPLER_HPP_
 #define VK_SAMPLER_HPP_
 
-#include "VkDevice.hpp"
 #include "VkImageView.hpp"  // For ResolveIdentityMapping()
 #include "Device/Config.hpp"
+#include "Device/Memset.hpp"
 #include "System/Math.hpp"
 
 #include <atomic>
 
 namespace vk {
 
-class Sampler : public Object<Sampler, VkSampler>
+struct SamplerState : sw::Memset<SamplerState>
 {
-public:
-	Sampler(const VkSamplerCreateInfo *pCreateInfo, void *mem, const vk::SamplerYcbcrConversion *ycbcrConversion)
-	    : magFilter(pCreateInfo->magFilter)
-	    , minFilter(pCreateInfo->minFilter)
-	    , mipmapMode(pCreateInfo->mipmapMode)
-	    , addressModeU(pCreateInfo->addressModeU)
-	    , addressModeV(pCreateInfo->addressModeV)
-	    , addressModeW(pCreateInfo->addressModeW)
-	    , mipLodBias(pCreateInfo->mipLodBias)
-	    , anisotropyEnable(pCreateInfo->anisotropyEnable)
-	    , maxAnisotropy(pCreateInfo->maxAnisotropy)
-	    , compareEnable(pCreateInfo->compareEnable)
-	    , compareOp(pCreateInfo->compareOp)
-	    , minLod(ClampLod(pCreateInfo->minLod))
-	    , maxLod(ClampLod(pCreateInfo->maxLod))
-	    , borderColor(pCreateInfo->borderColor)
-	    , unnormalizedCoordinates(pCreateInfo->unnormalizedCoordinates)
-	    , ycbcrConversion(ycbcrConversion)
-	{
-	}
-
-	static size_t ComputeRequiredAllocationSize(const VkSamplerCreateInfo *pCreateInfo)
-	{
-		return 0;
-	}
+	SamplerState(const VkSamplerCreateInfo *pCreateInfo, const vk::SamplerYcbcrConversion *ycbcrConversion);
 
 	// Prevents accessing mipmap levels out of range.
 	static float ClampLod(float lod)
@@ -58,7 +34,6 @@
 		return sw::clamp(lod, 0.0f, (float)(sw::MAX_TEXTURE_LOD));
 	}
 
-	const uint32_t id = nextID++;
 	const VkFilter magFilter = VK_FILTER_NEAREST;
 	const VkFilter minFilter = VK_FILTER_NEAREST;
 	const VkSamplerMipmapMode mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
@@ -75,10 +50,22 @@
 	const VkBorderColor borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
 	const VkBool32 unnormalizedCoordinates = VK_FALSE;
 
-	const vk::SamplerYcbcrConversion *ycbcrConversion = nullptr;
+	VkSamplerYcbcrModelConversion ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY;
+	bool studioSwing = false;    // Narrow range
+	bool swappedChroma = false;  // Cb/Cr components in reverse order
+};
 
-private:
-	static std::atomic<uint32_t> nextID;
+class Sampler : public Object<Sampler, VkSampler>, public SamplerState
+{
+public:
+	Sampler(const VkSamplerCreateInfo *pCreateInfo, void *mem, const SamplerState &samplerState, uint32_t samplerID);
+
+	static size_t ComputeRequiredAllocationSize(const VkSamplerCreateInfo *pCreateInfo)
+	{
+		return 0;
+	}
+
+	const uint32_t id = 0;
 };
 
 class SamplerYcbcrConversion : public Object<SamplerYcbcrConversion, VkSamplerYcbcrConversion>
diff --git a/src/Vulkan/libVulkan.cpp b/src/Vulkan/libVulkan.cpp
index c583748..3e3e648 100644
--- a/src/Vulkan/libVulkan.cpp
+++ b/src/Vulkan/libVulkan.cpp
@@ -1857,7 +1857,18 @@
 		extensionCreateInfo = extensionCreateInfo->pNext;
 	}
 
-	return vk::Sampler::Create(pAllocator, pCreateInfo, pSampler, ycbcrConversion);
+	vk::SamplerState samplerState(pCreateInfo, ycbcrConversion);
+	uint32_t samplerID = vk::Cast(device)->indexSampler(samplerState);
+
+	VkResult result = vk::Sampler::Create(pAllocator, pCreateInfo, pSampler, samplerState, samplerID);
+
+	if(*pSampler == VK_NULL_HANDLE)
+	{
+		ASSERT(result != VK_SUCCESS);
+		vk::Cast(device)->removeSampler(samplerState);
+	}
+
+	return result;
 }
 
 VKAPI_ATTR void VKAPI_CALL vkDestroySampler(VkDevice device, VkSampler sampler, const VkAllocationCallbacks *pAllocator)
@@ -1865,7 +1876,12 @@
 	TRACE("(VkDevice device = %p, VkSampler sampler = %p, const VkAllocationCallbacks* pAllocator = %p)",
 	      device, static_cast<void *>(sampler), pAllocator);
 
-	vk::destroy(sampler, pAllocator);
+	if(sampler != VK_NULL_HANDLE)
+	{
+		vk::Cast(device)->removeSampler(*vk::Cast(sampler));
+
+		vk::destroy(sampler, pAllocator);
+	}
 }
 
 VKAPI_ATTR VkResult VKAPI_CALL vkCreateDescriptorSetLayout(VkDevice device, const VkDescriptorSetLayoutCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDescriptorSetLayout *pSetLayout)