Implement basic SPIR-V texture sampling

Replaces VkDescriptorImageInfo with a more concrete
SampledImageDescriptor, which contains an sw::Texture that SamplerCore
uses.

The parameters can be looked up statically only because the
shaderSampledImageArrayDynamicIndexing feature is currently not
supported.

Bug b/129523279

Test: dEQP-VK.texture.filtering.2d.formats.r8g8b8a8_unorm.nearest
Change-Id: I619b5b48b2b4552d9bfc70b087df2c31eabb49ea
Reviewed-on: https://swiftshader-review.googlesource.com/c/SwiftShader/+/28434
Presubmit-Ready: Nicolas Capens <nicolascapens@google.com>
Kokoro-Presubmit: kokoro <noreply+kokoro@google.com>
Tested-by: Nicolas Capens <nicolascapens@google.com>
Reviewed-by: Alexis Hétu <sugoi@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
diff --git a/src/Device/Renderer.cpp b/src/Device/Renderer.cpp
index 983faae..245f219 100644
--- a/src/Device/Renderer.cpp
+++ b/src/Device/Renderer.cpp
@@ -510,8 +510,8 @@
 				{
 					VkOffset3D offset = { 0, 0, static_cast<int32_t>(context->renderTargetLayer[index]) };
 					data->colorBuffer[index] = (unsigned int*)context->renderTarget[index]->getOffsetPointer(offset, VK_IMAGE_ASPECT_COLOR_BIT);
-					data->colorPitchB[index] = context->renderTarget[index]->rowPitchBytes(VK_IMAGE_ASPECT_COLOR_BIT);
-					data->colorSliceB[index] = context->renderTarget[index]->slicePitchBytes(VK_IMAGE_ASPECT_COLOR_BIT);
+					data->colorPitchB[index] = context->renderTarget[index]->rowPitchBytes(VK_IMAGE_ASPECT_COLOR_BIT, 0);
+					data->colorSliceB[index] = context->renderTarget[index]->slicePitchBytes(VK_IMAGE_ASPECT_COLOR_BIT, 0);
 				}
 			}
 
@@ -522,16 +522,16 @@
 			{
 				VkOffset3D offset = { 0, 0, static_cast<int32_t>(context->depthBufferLayer) };
 				data->depthBuffer = (float*)context->depthBuffer->getOffsetPointer(offset, VK_IMAGE_ASPECT_DEPTH_BIT);
-				data->depthPitchB = context->depthBuffer->rowPitchBytes(VK_IMAGE_ASPECT_DEPTH_BIT);
-				data->depthSliceB = context->depthBuffer->slicePitchBytes(VK_IMAGE_ASPECT_DEPTH_BIT);
+				data->depthPitchB = context->depthBuffer->rowPitchBytes(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
+				data->depthSliceB = context->depthBuffer->slicePitchBytes(VK_IMAGE_ASPECT_DEPTH_BIT, 0);
 			}
 
 			if(draw->stencilBuffer)
 			{
 				VkOffset3D offset = { 0, 0, static_cast<int32_t>(context->stencilBufferLayer) };
 				data->stencilBuffer = (unsigned char*)context->stencilBuffer->getOffsetPointer(offset, VK_IMAGE_ASPECT_STENCIL_BIT);
-				data->stencilPitchB = context->stencilBuffer->rowPitchBytes(VK_IMAGE_ASPECT_STENCIL_BIT);
-				data->stencilSliceB = context->stencilBuffer->slicePitchBytes(VK_IMAGE_ASPECT_STENCIL_BIT);
+				data->stencilPitchB = context->stencilBuffer->rowPitchBytes(VK_IMAGE_ASPECT_STENCIL_BIT, 0);
+				data->stencilSliceB = context->stencilBuffer->slicePitchBytes(VK_IMAGE_ASPECT_STENCIL_BIT, 0);
 			}
 		}
 
@@ -881,9 +881,9 @@
 							query->state = vk::Query::FINISHED;
 						}
 
-						// Manual unlocking is done before notifying, to avoid

-						// waking up the waiting thread only to block again

-						mutexLock.unlock();

+						// Manual unlocking is done before notifying, to avoid
+						// waking up the waiting thread only to block again
+						mutexLock.unlock();
 						query->condition.notify_one();
 					}
 
diff --git a/src/Device/Sampler.hpp b/src/Device/Sampler.hpp
index f86db85..0013b8d 100644
--- a/src/Device/Sampler.hpp
+++ b/src/Device/Sampler.hpp
@@ -56,13 +56,13 @@
 		float4 heightLOD;
 		float4 depthLOD;
 
-		word4 borderColor4[4];
-		float4 borderColorF[4];
-		float maxAnisotropy;
+		word4 borderColor4[4];   // FIXME(b/129523279): Part of Vulkan sampler.
+		float4 borderColorF[4];  // FIXME(b/129523279): Part of Vulkan sampler.
+		float maxAnisotropy;     // FIXME(b/129523279): Part of Vulkan sampler.
 		int baseLevel;
 		int maxLevel;
-		float minLod;
-		float maxLod;
+		float minLod;  // FIXME(b/129523279): Part of Vulkan sampler.
+		float maxLod;  // FIXME(b/129523279): Part of Vulkan sampler.
 	};
 
 	enum SamplerType
diff --git a/src/Pipeline/SamplerCore.cpp b/src/Pipeline/SamplerCore.cpp
index d799252..fd83369 100644
--- a/src/Pipeline/SamplerCore.cpp
+++ b/src/Pipeline/SamplerCore.cpp
@@ -227,7 +227,7 @@
 		return c;
 	}
 
-	Vector4f SamplerCore::sampleTexture(Pointer<Byte> &texture, Float4 &u, Float4 &v, Float4 &w, Float4 &q, Float4 &bias, Vector4f &dsx, Vector4f &dsy, Vector4f &offset, SamplerFunction function)
+	Vector4f SamplerCore::sampleTextureF(Pointer<Byte> &texture, Float4 &u, Float4 &v, Float4 &w, Float4 &q, Float4 &bias, Vector4f &dsx, Vector4f &dsy, Vector4f &offset, SamplerFunction function)
 	{
 		Vector4f c;
 
diff --git a/src/Pipeline/SamplerCore.hpp b/src/Pipeline/SamplerCore.hpp
index 684c1a7..0567fcd 100644
--- a/src/Pipeline/SamplerCore.hpp
+++ b/src/Pipeline/SamplerCore.hpp
@@ -51,7 +51,7 @@
 		SamplerCore(Pointer<Byte> &constants, const Sampler::State &state);
 
 		Vector4s sampleTexture(Pointer<Byte> &texture, Float4 &u, Float4 &v, Float4 &w, Float4 &q, Float4 &bias, Vector4f &dsx, Vector4f &dsy);
-		Vector4f sampleTexture(Pointer<Byte> &texture, Float4 &u, Float4 &v, Float4 &w, Float4 &q, Float4 &bias, Vector4f &dsx, Vector4f &dsy, Vector4f &offset, SamplerFunction function);
+		Vector4f sampleTextureF(Pointer<Byte> &texture, Float4 &u, Float4 &v, Float4 &w, Float4 &q, Float4 &bias, Vector4f &dsx, Vector4f &dsy, Vector4f &offset, SamplerFunction function);
 		static Vector4f textureSize(Pointer<Byte> &mipmap, Float4 &lod);
 
 	private:
diff --git a/src/Pipeline/SpirvShader.cpp b/src/Pipeline/SpirvShader.cpp
index 11b91ff..d212ff1 100644
--- a/src/Pipeline/SpirvShader.cpp
+++ b/src/Pipeline/SpirvShader.cpp
@@ -14,6 +14,7 @@
 
 #include "SpirvShader.hpp"
 
+#include "SamplerCore.hpp"
 #include "System/Math.hpp"
 #include "Vulkan/VkBuffer.hpp"
 #include "Vulkan/VkDebug.hpp"
@@ -27,6 +28,7 @@
 
 #ifdef Bool
 #undef Bool // b/127920555
+#undef None
 #endif
 
 namespace
@@ -338,6 +340,12 @@
 					break; // Correctly handled.
 
 				case spv::StorageClassUniformConstant:
+					// This storage class is for data stored within the descriptor itself,
+					// unlike StorageClassUniform which contains handles to buffers.
+					// For Vulkan it corresponds with samplers, images, or combined image samplers.
+					object.kind = Object::Kind::SampledImage;
+					break;
+
 				case spv::StorageClassWorkgroup:
 				case spv::StorageClassCrossWorkgroup:
 				case spv::StorageClassGeneric:
@@ -470,9 +478,12 @@
 				break;
 
 			case spv::OpFConvert:
+				UNIMPLEMENTED("No valid uses for OpFConvert until we support multiple bit widths enabled by features such as Float16/Float64 etc.");
+				break;
+
 			case spv::OpSConvert:
 			case spv::OpUConvert:
-				UNIMPLEMENTED("No valid uses for Op*Convert until we support multiple bit widths");
+				UNIMPLEMENTED("No valid uses for Op*Convert until we support multiple bit widths enabled by features such as Int16/Int64 etc.");
 				break;
 
 			case spv::OpLoad:
@@ -597,18 +608,10 @@
 			case spv::OpFwidthFine:
 			case spv::OpAtomicLoad:
 			case spv::OpPhi:
-				// Instructions that yield an intermediate value or divergent
-				// pointer
-			{
-				Type::ID typeId = insn.word(1);
-				Object::ID resultId = insn.word(2);
-				auto &object = defs[resultId];
-				object.type = typeId;
-				object.kind = (getType(typeId).opcode() == spv::OpTypePointer)
-					? Object::Kind::DivergentPointer : Object::Kind::Intermediate;
-				object.definition = insn;
+			case spv::OpImageSampleImplicitLod:
+				// Instructions that yield an intermediate value or divergent pointer
+				DefineResult(insn);
 				break;
-			}
 
 			case spv::OpStore:
 			case spv::OpAtomicStore:
@@ -1852,6 +1855,8 @@
 		case spv::OpTypeStruct:
 		case spv::OpTypePointer:
 		case spv::OpTypeFunction:
+		case spv::OpTypeImage:
+		case spv::OpTypeSampledImage:
 		case spv::OpExecutionMode:
 		case spv::OpMemoryModel:
 		case spv::OpFunction:
@@ -2056,6 +2061,9 @@
 		case spv::OpKill:
 			return EmitKill(insn, state);
 
+		case spv::OpImageSampleImplicitLod:
+			return EmitImageSampleImplicitLod(insn, state);
+			
 		default:
 			UNIMPLEMENTED("opcode: %s", OpcodeName(opcode).c_str());
 			break;
@@ -2095,6 +2103,20 @@
 			routine->createPointer(resultId, &routine->getVariable(resultId)[0]);
 			break;
 		}
+		case spv::StorageClassUniformConstant:
+		{
+			const auto &d = descriptorDecorations.at(resultId);
+			ASSERT(d.DescriptorSet >= 0);
+			ASSERT(d.Binding >= 0);
+
+			uint32_t arrayIndex = 0;  // TODO(b/129523279)
+			auto setLayout = routine->pipelineLayout->getDescriptorSetLayout(d.DescriptorSet);
+			size_t bindingOffset = setLayout->getBindingOffset(d.Binding, arrayIndex);
+			Pointer<Byte> set = routine->descriptorSets[d.DescriptorSet];  // DescriptorSet*
+			Pointer<Byte> binding = Pointer<Byte>(set + bindingOffset);    // SampledImageDescriptor*
+			routine->createPointer(resultId, binding);
+			break;
+		}
 		case spv::StorageClassUniform:
 		case spv::StorageClassStorageBuffer:
 		{
@@ -2133,6 +2155,16 @@
 		ASSERT(Type::ID(insn.word(1)) == result.type);
 		ASSERT(!atomic || getType(getType(pointer.type).element).opcode() == spv::OpTypeInt);  // Vulkan 1.1: "Atomic instructions must declare a scalar 32-bit integer type, for the value pointed to by Pointer."
 
+		if(pointer.kind == Object::Kind::SampledImage)
+		{
+			// Just propagate the pointer.
+			// TODO(b/129523279)
+			auto &ptr = routine->getPointer(pointerId);
+			routine->createPointer(resultId, ptr);
+
+			return EmitResult::Continue;
+		}
+
 		if(atomic)
 		{
 			Object::ID semanticsId = insn.word(5);
@@ -4125,6 +4157,86 @@
 		return EmitResult::Continue;
 	}
 
+	SpirvShader::EmitResult SpirvShader::EmitImageSampleImplicitLod(InsnIterator insn, EmitState *state) const
+	{
+		Type::ID resultTypeId = insn.word(1);
+		Object::ID resultId = insn.word(2);
+		Object::ID sampledImageId = insn.word(3);
+		Object::ID coordinateId = insn.word(4);
+		auto &resultType = getType(resultTypeId);
+
+		auto &result = state->routine->createIntermediate(resultId, resultType.sizeInComponents);
+		auto &sampledImage = state->routine->getPointer(sampledImageId);
+		auto coordinate = GenericValue(this, state->routine, coordinateId);
+
+		Pointer<Byte> constants;  // FIXME(b/129523279)
+
+		const DescriptorDecorations &d = descriptorDecorations.at(sampledImageId);
+		uint32_t arrayIndex = 0;  // TODO(b/129523279)
+		auto setLayout = state->routine->pipelineLayout->getDescriptorSetLayout(d.DescriptorSet);
+		size_t bindingOffset = setLayout->getBindingOffset(d.Binding, arrayIndex);
+
+		const uint8_t *p = reinterpret_cast<const uint8_t*>(state->descriptorSets[d.DescriptorSet]) + bindingOffset;
+		const auto *t = reinterpret_cast<const vk::SampledImageDescriptor*>(p);
+
+		Sampler::State samplerState;
+		samplerState.textureType = TEXTURE_2D;                  ASSERT(t->imageView->getType() == VK_IMAGE_VIEW_TYPE_2D);  // TODO(b/129523279)
+		samplerState.textureFormat = t->imageView->getFormat();
+		samplerState.textureFilter = FILTER_POINT;              ASSERT(t->sampler->magFilter == VK_FILTER_NEAREST); ASSERT(t->sampler->minFilter == VK_FILTER_NEAREST);  // TODO(b/129523279)
+
+		samplerState.addressingModeU = ADDRESSING_WRAP;         ASSERT(t->sampler->addressModeU == VK_SAMPLER_ADDRESS_MODE_REPEAT);  // TODO(b/129523279)
+		samplerState.addressingModeV = ADDRESSING_WRAP;         ASSERT(t->sampler->addressModeV == VK_SAMPLER_ADDRESS_MODE_REPEAT);  // TODO(b/129523279)
+		samplerState.addressingModeW = ADDRESSING_WRAP;         ASSERT(t->sampler->addressModeW == VK_SAMPLER_ADDRESS_MODE_REPEAT);  // TODO(b/129523279)
+		samplerState.mipmapFilter = MIPMAP_POINT;               ASSERT(t->sampler->mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST);  // TODO(b/129523279)
+		samplerState.sRGB = false;                              ASSERT(t->imageView->getFormat().isSRGBformat() == false);  // TODO(b/129523279)
+		samplerState.swizzleR = SWIZZLE_RED;                    ASSERT(t->imageView->getComponentMapping().r == VK_COMPONENT_SWIZZLE_R);  // TODO(b/129523279)
+		samplerState.swizzleG = SWIZZLE_GREEN;                  ASSERT(t->imageView->getComponentMapping().g == VK_COMPONENT_SWIZZLE_G);  // TODO(b/129523279)
+		samplerState.swizzleB = SWIZZLE_BLUE;                   ASSERT(t->imageView->getComponentMapping().b == VK_COMPONENT_SWIZZLE_B);  // TODO(b/129523279)
+		samplerState.swizzleA = SWIZZLE_ALPHA;                  ASSERT(t->imageView->getComponentMapping().a == VK_COMPONENT_SWIZZLE_A);  // TODO(b/129523279)
+		samplerState.highPrecisionFiltering = false;
+		samplerState.compare = COMPARE_BYPASS;                  ASSERT(t->sampler->compareEnable == VK_FALSE);  // TODO(b/129523279)
+
+	//	minLod  // TODO(b/129523279)
+	//	maxLod  // TODO(b/129523279)
+	//	borderColor  // TODO(b/129523279)
+		ASSERT(t->sampler->mipLodBias == 0.0f);  // TODO(b/129523279)
+		ASSERT(t->sampler->anisotropyEnable == VK_FALSE);  // TODO(b/129523279)
+		ASSERT(t->sampler->unnormalizedCoordinates == VK_FALSE);  // TODO(b/129523279)
+
+		SamplerCore sampler(constants, samplerState);
+
+		Pointer<Byte> texture = sampledImage + OFFSET(vk::SampledImageDescriptor, texture); // sw::Texture*
+		SIMD::Float u = coordinate.Float(0);
+		SIMD::Float v = coordinate.Float(1);
+		SIMD::Float w(0);     // TODO(b/129523279)
+		SIMD::Float q(0);     // TODO(b/129523279)
+		SIMD::Float bias(0);  // TODO(b/129523279)
+		Vector4f dsx;         // TODO(b/129523279)
+		Vector4f dsy;         // TODO(b/129523279)
+		Vector4f offset;      // TODO(b/129523279)
+		SamplerFunction samplerFunction = { Implicit, None };   ASSERT(insn.wordCount() == 5);  // TODO(b/129523279)
+
+		Vector4f sample = sampler.sampleTextureF(texture, u, v, w, q, bias, dsx, dsy, offset, samplerFunction);
+
+		if(getType(resultType.element).opcode() == spv::OpTypeFloat)
+		{
+			result.move(0, sample.x);
+			result.move(1, sample.y);
+			result.move(2, sample.z);
+			result.move(3, sample.w);
+		}
+		else
+		{
+			// TODO(b/129523279): Add a Sampler::sampleTextureI() method.
+			result.move(0, As<SIMD::Int>(sample.x * SIMD::Float(0xFF)));
+			result.move(1, As<SIMD::Int>(sample.y * SIMD::Float(0xFF)));
+			result.move(2, As<SIMD::Int>(sample.z * SIMD::Float(0xFF)));
+			result.move(3, As<SIMD::Int>(sample.w * SIMD::Float(0xFF)));
+		}
+
+		return EmitResult::Continue;
+	}
+
 	void SpirvShader::emitEpilog(SpirvRoutine *routine) const
 	{
 		for (auto insn : *this)
diff --git a/src/Pipeline/SpirvShader.hpp b/src/Pipeline/SpirvShader.hpp
index 35120af..16a97cc 100644
--- a/src/Pipeline/SpirvShader.hpp
+++ b/src/Pipeline/SpirvShader.hpp
@@ -283,6 +283,9 @@
 				// A pointer to a vk::DescriptorSet*.
 				// Pointer held by SpirvRoutine::pointers.
 				DescriptorSet,
+
+				// Pointer to an image/sampler descriptor.
+				SampledImage,
 			};
 
 			Kind kind = Kind::Unknown;
@@ -719,6 +722,7 @@
 		EmitResult EmitReturn(InsnIterator insn, EmitState *state) const;
 		EmitResult EmitKill(InsnIterator insn, EmitState *state) const;
 		EmitResult EmitPhi(InsnIterator insn, EmitState *state) const;
+		EmitResult EmitImageSampleImplicitLod(InsnIterator insn, EmitState *state) const;
 
 		// OpcodeName() returns the name of the opcode op.
 		// If NDEBUG is defined, then OpcodeName() will only return the numerical code.
diff --git a/src/Vulkan/VkDescriptorSetLayout.cpp b/src/Vulkan/VkDescriptorSetLayout.cpp
index 1d57347..106e3f2 100644
--- a/src/Vulkan/VkDescriptorSetLayout.cpp
+++ b/src/Vulkan/VkDescriptorSetLayout.cpp
@@ -13,7 +13,10 @@
 // limitations under the License.
 
 #include "VkDescriptorSetLayout.hpp"
+
 #include "VkDescriptorSet.hpp"
+#include "VkSampler.hpp"
+#include "VkImageView.hpp"
 #include "System/Types.hpp"
 
 #include <algorithm>
@@ -90,6 +93,7 @@
 	case VK_DESCRIPTOR_TYPE_SAMPLER:
 	case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
 	case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE:
+		return sizeof(SampledImageDescriptor);
 	case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
 	case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT:
 		return sizeof(VkDescriptorImageInfo);
@@ -153,8 +157,8 @@
 		{
 			for(uint32_t j = 0; j < bindings[i].descriptorCount; j++)
 			{
-				VkDescriptorImageInfo* imageInfo = reinterpret_cast<VkDescriptorImageInfo*>(mem);
-				imageInfo->sampler = bindings[i].pImmutableSamplers[j];
+				SampledImageDescriptor* imageSamplerDescriptor = reinterpret_cast<SampledImageDescriptor*>(mem);
+				imageSamplerDescriptor->sampler = vk::Cast(bindings[i].pImmutableSamplers[j]);
 				mem += typeSize;
 			}
 		}
@@ -233,25 +237,25 @@
 	return &descriptorSet->data[byteOffset];
 }
 
-const uint8_t* DescriptorSetLayout::GetInputData(const VkWriteDescriptorSet& descriptorWrites)
+const uint8_t* DescriptorSetLayout::GetInputData(const VkWriteDescriptorSet& writeDescriptorSet)
 {
-	switch(descriptorWrites.descriptorType)
+	switch(writeDescriptorSet.descriptorType)
 	{
 	case VK_DESCRIPTOR_TYPE_SAMPLER:
 	case VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER:
 	case VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE:
 	case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
 	case VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT:
-		return reinterpret_cast<const uint8_t*>(descriptorWrites.pImageInfo);
+		return reinterpret_cast<const uint8_t*>(writeDescriptorSet.pImageInfo);
 	case VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER:
 	case VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER:
-		return reinterpret_cast<const uint8_t*>(descriptorWrites.pTexelBufferView);
+		return reinterpret_cast<const uint8_t*>(writeDescriptorSet.pTexelBufferView);
 		break;
 	case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER:
 	case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER:
 	case VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC:
 	case VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC:
-		return reinterpret_cast<const uint8_t*>(descriptorWrites.pBufferInfo);
+		return reinterpret_cast<const uint8_t*>(writeDescriptorSet.pBufferInfo);
 		break;
 	default:
 		UNIMPLEMENTED("descriptorType");
@@ -259,24 +263,186 @@
 	}
 }
 
-void DescriptorSetLayout::WriteDescriptorSet(const VkWriteDescriptorSet& descriptorWrites)
+void DescriptorSetLayout::WriteDescriptorSet(const VkWriteDescriptorSet& writeDescriptorSet)
 {
-	DescriptorSet* dstSet = vk::Cast(descriptorWrites.dstSet);
+	DescriptorSet* dstSet = vk::Cast(writeDescriptorSet.dstSet);
 	DescriptorSetLayout* dstLayout = dstSet->layout;
 	ASSERT(dstLayout);
-	ASSERT(dstLayout->bindings[dstLayout->getBindingIndex(descriptorWrites.dstBinding)].descriptorType == descriptorWrites.descriptorType);
+	ASSERT(dstLayout->bindings[dstLayout->getBindingIndex(writeDescriptorSet.dstBinding)].descriptorType == writeDescriptorSet.descriptorType);
 
 	size_t typeSize = 0;
-	uint8_t* memToWrite = dstLayout->getOffsetPointer(dstSet, descriptorWrites.dstBinding, descriptorWrites.dstArrayElement, descriptorWrites.descriptorCount, &typeSize);
+	uint8_t* memToWrite = dstLayout->getOffsetPointer(dstSet, writeDescriptorSet.dstBinding, writeDescriptorSet.dstArrayElement, writeDescriptorSet.descriptorCount, &typeSize);
 
-	// If the dstBinding has fewer than descriptorCount array elements remaining
-	// starting from dstArrayElement, then the remainder will be used to update
-	// the subsequent binding - dstBinding+1 starting at array element zero. If
-	// a binding has a descriptorCount of zero, it is skipped. This behavior
-	// applies recursively, with the update affecting consecutive bindings as
-	// needed to update all descriptorCount descriptors.
-	size_t writeSize = typeSize * descriptorWrites.descriptorCount;
-	memcpy(memToWrite, DescriptorSetLayout::GetInputData(descriptorWrites), writeSize);
+	if(writeDescriptorSet.descriptorType == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER)
+	{
+		SampledImageDescriptor *imageSampler = reinterpret_cast<SampledImageDescriptor*>(memToWrite);
+
+		for(uint32_t i = 0; i < writeDescriptorSet.descriptorCount; i++)
+		{
+			vk::Sampler *sampler = vk::Cast(writeDescriptorSet.pImageInfo[i].sampler);
+			vk::ImageView *imageView = vk::Cast(writeDescriptorSet.pImageInfo[i].imageView);
+
+			imageSampler[i].sampler = sampler;
+			imageSampler[i].imageView = imageView;
+
+			sw::Texture *texture = &imageSampler[i].texture;
+			memset(texture, 0, sizeof(sw::Texture));  // TODO(b/129523279): eliminate
+
+			auto &subresourceRange = imageView->getSubresourceRange();
+			int baseLevel = subresourceRange.baseMipLevel;
+
+			for(int mipmapLevel = 0; mipmapLevel < sw::MIPMAP_LEVELS; mipmapLevel++)
+			{
+				int level = mipmapLevel - baseLevel;  // Level within the image view
+				level = sw::clamp(level, 0, (int)subresourceRange.levelCount);
+
+				VkOffset3D offset = {0, 0, 0};
+				VkImageAspectFlagBits aspect = VK_IMAGE_ASPECT_COLOR_BIT;
+				void *buffer = imageView->getOffsetPointer(offset, aspect);
+
+				sw::Mipmap &mipmap = texture->mipmap[mipmapLevel];
+				mipmap.buffer[0] = buffer;
+
+				VkExtent3D extent = imageView->getMipLevelExtent(level);
+				Format format = imageView->getFormat();
+				int width = extent.width;
+				int height = extent.height;
+				int depth = extent.depth;
+				int pitchP = imageView->rowPitchBytes(aspect, level) / format.bytes();
+				int sliceP = imageView->slicePitchBytes(aspect, level) / format.bytes();
+
+				float exp2LOD = 1.0f;
+
+				if(mipmapLevel == 0)
+				{
+					texture->widthHeightLOD[0] = width * exp2LOD;
+					texture->widthHeightLOD[1] = width * exp2LOD;
+					texture->widthHeightLOD[2] = height * exp2LOD;
+					texture->widthHeightLOD[3] = height * exp2LOD;
+
+					texture->widthLOD[0] = width * exp2LOD;
+					texture->widthLOD[1] = width * exp2LOD;
+					texture->widthLOD[2] = width * exp2LOD;
+					texture->widthLOD[3] = width * exp2LOD;
+
+					texture->heightLOD[0] = height * exp2LOD;
+					texture->heightLOD[1] = height * exp2LOD;
+					texture->heightLOD[2] = height * exp2LOD;
+					texture->heightLOD[3] = height * exp2LOD;
+
+					texture->depthLOD[0] = depth * exp2LOD;
+					texture->depthLOD[1] = depth * exp2LOD;
+					texture->depthLOD[2] = depth * exp2LOD;
+					texture->depthLOD[3] = depth * exp2LOD;
+				}
+
+				if(format.isFloatFormat())
+				{
+					mipmap.fWidth[0] = (float)width / 65536.0f;
+					mipmap.fWidth[1] = (float)width / 65536.0f;
+					mipmap.fWidth[2] = (float)width / 65536.0f;
+					mipmap.fWidth[3] = (float)width / 65536.0f;
+
+					mipmap.fHeight[0] = (float)height / 65536.0f;
+					mipmap.fHeight[1] = (float)height / 65536.0f;
+					mipmap.fHeight[2] = (float)height / 65536.0f;
+					mipmap.fHeight[3] = (float)height / 65536.0f;
+
+					mipmap.fDepth[0] = (float)depth / 65536.0f;
+					mipmap.fDepth[1] = (float)depth / 65536.0f;
+					mipmap.fDepth[2] = (float)depth / 65536.0f;
+					mipmap.fDepth[3] = (float)depth / 65536.0f;
+				}
+
+				short halfTexelU = 0x8000 / width;
+				short halfTexelV = 0x8000 / height;
+				short halfTexelW = 0x8000 / depth;
+
+				mipmap.uHalf[0] = halfTexelU;
+				mipmap.uHalf[1] = halfTexelU;
+				mipmap.uHalf[2] = halfTexelU;
+				mipmap.uHalf[3] = halfTexelU;
+
+				mipmap.vHalf[0] = halfTexelV;
+				mipmap.vHalf[1] = halfTexelV;
+				mipmap.vHalf[2] = halfTexelV;
+				mipmap.vHalf[3] = halfTexelV;
+
+				mipmap.wHalf[0] = halfTexelW;
+				mipmap.wHalf[1] = halfTexelW;
+				mipmap.wHalf[2] = halfTexelW;
+				mipmap.wHalf[3] = halfTexelW;
+
+				mipmap.width[0] = width;
+				mipmap.width[1] = width;
+				mipmap.width[2] = width;
+				mipmap.width[3] = width;
+
+				mipmap.height[0] = height;
+				mipmap.height[1] = height;
+				mipmap.height[2] = height;
+				mipmap.height[3] = height;
+
+				mipmap.depth[0] = depth;
+				mipmap.depth[1] = depth;
+				mipmap.depth[2] = depth;
+				mipmap.depth[3] = depth;
+
+				mipmap.onePitchP[0] = 1;
+				mipmap.onePitchP[1] = pitchP;
+				mipmap.onePitchP[2] = 1;
+				mipmap.onePitchP[3] = pitchP;
+
+				mipmap.pitchP[0] = pitchP;
+				mipmap.pitchP[1] = pitchP;
+				mipmap.pitchP[2] = pitchP;
+				mipmap.pitchP[3] = pitchP;
+
+				mipmap.sliceP[0] = sliceP;
+				mipmap.sliceP[1] = sliceP;
+				mipmap.sliceP[2] = sliceP;
+				mipmap.sliceP[3] = sliceP;
+
+				// TODO(b/129523279)
+				if(false/*format == FORMAT_YV12_BT601 ||
+				   format == FORMAT_YV12_BT709 ||
+				   format == FORMAT_YV12_JFIF*/)
+				{
+					unsigned int YStride = pitchP;
+					unsigned int YSize = YStride * height;
+					unsigned int CStride = sw::align<16>(YStride / 2);
+					unsigned int CSize = CStride * height / 2;
+
+					mipmap.buffer[1] = (sw::byte*)mipmap.buffer[0] + YSize;
+					mipmap.buffer[2] = (sw::byte*)mipmap.buffer[1] + CSize;
+
+					texture->mipmap[1].width[0] = width / 2;
+					texture->mipmap[1].width[1] = width / 2;
+					texture->mipmap[1].width[2] = width / 2;
+					texture->mipmap[1].width[3] = width / 2;
+					texture->mipmap[1].height[0] = height / 2;
+					texture->mipmap[1].height[1] = height / 2;
+					texture->mipmap[1].height[2] = height / 2;
+					texture->mipmap[1].height[3] = height / 2;
+					texture->mipmap[1].onePitchP[0] = 1;
+					texture->mipmap[1].onePitchP[1] = CStride;
+					texture->mipmap[1].onePitchP[2] = 1;
+					texture->mipmap[1].onePitchP[3] = CStride;
+				}
+			}
+		}
+	}
+	else
+	{
+		// If the dstBinding has fewer than descriptorCount array elements remaining
+		// starting from dstArrayElement, then the remainder will be used to update
+		// the subsequent binding - dstBinding+1 starting at array element zero. If
+		// a binding has a descriptorCount of zero, it is skipped. This behavior
+		// applies recursively, with the update affecting consecutive bindings as
+		// needed to update all descriptorCount descriptors.
+		size_t writeSize = typeSize * writeDescriptorSet.descriptorCount;
+		memcpy(memToWrite, DescriptorSetLayout::GetInputData(writeDescriptorSet), writeSize);
+	}
 }
 
 void DescriptorSetLayout::CopyDescriptorSet(const VkCopyDescriptorSet& descriptorCopies)
diff --git a/src/Vulkan/VkDescriptorSetLayout.hpp b/src/Vulkan/VkDescriptorSetLayout.hpp
index d5aa560..6ccd12e 100644
--- a/src/Vulkan/VkDescriptorSetLayout.hpp
+++ b/src/Vulkan/VkDescriptorSetLayout.hpp
@@ -17,11 +17,25 @@
 
 #include "VkObject.hpp"
 
+#include "Vulkan/VkSampler.hpp"
+#include "Vulkan/VkImageView.hpp"
+#include "Device/Sampler.hpp"
+
 namespace vk
 {
 
 class DescriptorSet;
 
+// TODO(b/129523279): Move to the Device or Pipeline layer.
+struct SampledImageDescriptor
+{
+	// TODO(b/129523279): Minimize to the data actually needed.
+	vk::Sampler *sampler;
+	vk::ImageView *imageView;
+
+	sw::Texture texture;
+};
+
 class DescriptorSetLayout : public Object<DescriptorSetLayout, VkDescriptorSetLayout>
 {
 public:
diff --git a/src/Vulkan/VkFormat.cpp b/src/Vulkan/VkFormat.cpp
index c201389..d3a48c7 100644
--- a/src/Vulkan/VkFormat.cpp
+++ b/src/Vulkan/VkFormat.cpp
@@ -144,7 +144,6 @@
 
 bool Format::isSRGBreadable() const
 {
-	// Keep in sync with Capabilities::isSRGBreadable
 	switch(format)
 	{
 	case VK_FORMAT_B8G8R8A8_UNORM:
@@ -160,7 +159,6 @@
 
 bool Format::isSRGBwritable() const
 {
-	// Keep in sync with Capabilities::isSRGBwritable
 	switch(format)
 	{
 	case VK_FORMAT_UNDEFINED:
diff --git a/src/Vulkan/VkImageView.hpp b/src/Vulkan/VkImageView.hpp
index 5466d1e..4e54f8a 100644
--- a/src/Vulkan/VkImageView.hpp
+++ b/src/Vulkan/VkImageView.hpp
@@ -36,23 +36,28 @@
 	void clear(const VkClearValue& clearValue, VkImageAspectFlags aspectMask, const VkClearRect& renderArea);
 	void resolve(ImageView* resolveAttachment);
 
+	VkImageViewType getType() const { return viewType; }
 	Format getFormat() const { return format; }
 	int getSampleCount() const { return image->getSampleCountFlagBits(); }
-	int rowPitchBytes(VkImageAspectFlagBits aspect) const { return image->rowPitchBytes(aspect, subresourceRange.baseMipLevel); }
-	int slicePitchBytes(VkImageAspectFlagBits aspect) const { return image->slicePitchBytes(aspect, subresourceRange.baseMipLevel); }
+	int rowPitchBytes(VkImageAspectFlagBits aspect, uint32_t mipLevel) const { return image->rowPitchBytes(aspect, subresourceRange.baseMipLevel + mipLevel); }
+	int slicePitchBytes(VkImageAspectFlagBits aspect, uint32_t mipLevel) const { return image->slicePitchBytes(aspect, subresourceRange.baseMipLevel + mipLevel); }
+	VkExtent3D getMipLevelExtent(uint32_t mipLevel) const { return image->getMipLevelExtent(subresourceRange.baseMipLevel + mipLevel); }
 
 	void *getOffsetPointer(const VkOffset3D& offset, VkImageAspectFlagBits aspect) const;
 	bool hasDepthAspect() const { return (subresourceRange.aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) != 0; }
 	bool hasStencilAspect() const { return (subresourceRange.aspectMask & VK_IMAGE_ASPECT_STENCIL_BIT) != 0; }
 
-private:
-	bool                       imageTypesMatch(VkImageType imageType) const;
+	const VkComponentMapping &getComponentMapping() const { return components; }
+	const VkImageSubresourceRange &getSubresourceRange() const { return subresourceRange; }
 
-	Image*                     image = nullptr;
-	VkImageViewType            viewType = VK_IMAGE_VIEW_TYPE_2D;
-	Format                     format;
-	VkComponentMapping         components = {};
-	VkImageSubresourceRange    subresourceRange = {};
+private:
+	bool                          imageTypesMatch(VkImageType imageType) const;
+
+	Image *const                  image = nullptr;
+	const VkImageViewType         viewType = VK_IMAGE_VIEW_TYPE_2D;
+	const Format                  format;
+	const VkComponentMapping      components = {};
+	const VkImageSubresourceRange subresourceRange = {};
 };
 
 static inline ImageView* Cast(VkImageView object)
diff --git a/src/Vulkan/VkSampler.hpp b/src/Vulkan/VkSampler.hpp
index fc62389..5fa0b4e 100644
--- a/src/Vulkan/VkSampler.hpp
+++ b/src/Vulkan/VkSampler.hpp
@@ -49,22 +49,21 @@
 		return 0;
 	}
 
-private:
-	VkFilter                magFilter = VK_FILTER_NEAREST;
-	VkFilter                minFilter = VK_FILTER_NEAREST;
-	VkSamplerMipmapMode     mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
-	VkSamplerAddressMode    addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
-	VkSamplerAddressMode    addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
-	VkSamplerAddressMode    addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
-	float                   mipLodBias = 0.0f;
-	VkBool32                anisotropyEnable = VK_FALSE;
-	float                   maxAnisotropy = 0.0f;
-	VkBool32                compareEnable = VK_FALSE;
-	VkCompareOp             compareOp = VK_COMPARE_OP_NEVER;
-	float                   minLod = 0.0f;
-	float                   maxLod = 0.0f;
-	VkBorderColor           borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
-	VkBool32                unnormalizedCoordinates = VK_FALSE;
+	const VkFilter             magFilter = VK_FILTER_NEAREST;
+	const VkFilter             minFilter = VK_FILTER_NEAREST;
+	const VkSamplerMipmapMode  mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
+	const VkSamplerAddressMode addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+	const VkSamplerAddressMode addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+	const VkSamplerAddressMode addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
+	const float                mipLodBias = 0.0f;
+	const VkBool32             anisotropyEnable = VK_FALSE;
+	const float                maxAnisotropy = 0.0f;
+	const VkBool32             compareEnable = VK_FALSE;
+	const VkCompareOp          compareOp = VK_COMPARE_OP_NEVER;
+	const float                minLod = 0.0f;
+	const float                maxLod = 0.0f;
+	const VkBorderColor        borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
+	const VkBool32             unnormalizedCoordinates = VK_FALSE;
 };
 
 static inline Sampler* Cast(VkSampler object)