Browse Source

vulkan: implement MSAA textures.

Sasha Szpakowski 8 months ago
parent
commit
fe4fc84659

+ 104 - 63
src/modules/graphics/vulkan/Graphics.cpp

@@ -326,7 +326,7 @@ void Graphics::submitGpuCommands(SubmitMode submitMode, void *screenshotCallback
 
 	if (submitMode == SUBMIT_PRESENT)
 	{
-		VkImage backbufferImage = fakeBackbuffer != nullptr ? (VkImage)fakeBackbuffer->getHandle() : swapChainImages.at(imageIndex);
+		VkImage backbufferImage = fakeBackbuffer != nullptr ? (VkImage)fakeBackbuffer->getRenderTargetHandle() : swapChainImages.at(imageIndex);
 
 		if (pendingScreenshotCallbacks.empty())
 		{
@@ -1193,13 +1193,8 @@ static bool computeDispatchBarrierFlags(Shader *shader, VkAccessFlags &dstAccess
 
 		// All writable images use the GENERAL layout.
 		// TODO: this is pretty messy.
-		VkAccessFlags texAccessFlags = 0;
-		VkPipelineStageFlags texStageFlags = 0;
 		bool depthStencil  = isPixelFormatDepthStencil(tex->getPixelFormat());
-		Vulkan::setImageLayoutTransitionOptions(false, tex->isRenderTarget(), depthStencil, VK_IMAGE_LAYOUT_GENERAL, texAccessFlags, texStageFlags);
-		
-		dstAccessFlags |= texAccessFlags;
-		dstStageFlags |= texStageFlags;
+		Vulkan::addImageLayoutTransitionOptions(false, tex->isRenderTarget(), depthStencil, VK_IMAGE_LAYOUT_GENERAL, dstAccessFlags, dstStageFlags);
 	}
 
 	for (const auto &info : shader->getActiveStorageBufferInfo())
@@ -1285,7 +1280,7 @@ void Graphics::setRenderTargetsInternal(const RenderTargets &rts, int pixelw, in
 	if (isWindow)
 		setDefaultRenderPass();
 	else
-		setRenderPass(rts, pixelw, pixelh, hasSRGBtexture);
+		setRenderPass(rts, pixelw, pixelh);
 }
 
 // END IMPLEMENTATION OVERRIDDEN FUNCTIONS
@@ -2120,8 +2115,9 @@ VkFramebuffer Graphics::createFramebuffer(FramebufferConfiguration &configuratio
 	if (configuration.staticData.depthView)
 		attachments.push_back(configuration.staticData.depthView);
 
-	if (configuration.staticData.resolveView)
-		attachments.push_back(configuration.staticData.resolveView);
+	// Resolve attachments after everything else to match createRenderPass.
+	for (const auto &colorResolveView : configuration.colorResolveViews)
+		attachments.push_back(colorResolveView);
 
 	VkFramebufferCreateInfo createInfo{};
 	createInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
@@ -2198,6 +2194,7 @@ VkRenderPass Graphics::createRenderPass(RenderPassConfiguration &configuration)
 
 	std::vector<VkAttachmentDescription> attachments;
 	std::vector<VkAttachmentReference> colorAttachmentRefs;
+	std::vector<VkAttachmentReference> colorResolveAttachmentRefs;
 
 	uint32_t attachment = 0;
 	for (const auto &colorAttachment : configuration.colorAttachments)
@@ -2214,24 +2211,33 @@ VkRenderPass Graphics::createRenderPass(RenderPassConfiguration &configuration)
 		colorDescription.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
 		colorDescription.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
 		colorDescription.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
-		colorDescription.initialLayout = colorAttachment.layout;
-		colorDescription.finalLayout = colorAttachment.layout;
+		if (colorAttachment.msaaSamples > 1)
+		{
+			colorDescription.initialLayout = colorAttachment.msaaLayout;
+			colorDescription.finalLayout = colorAttachment.msaaLayout;
+		}
+		else
+		{
+			colorDescription.initialLayout = colorAttachment.layout;
+			colorDescription.finalLayout = colorAttachment.layout;
+		}
 		attachments.push_back(colorDescription);
 
-		VkAccessFlags texBeginAccessFlags = 0;
-		VkPipelineStageFlags texBeginStageFlags = 0;
-		Vulkan::setImageLayoutTransitionOptions(true, true, false, colorAttachment.layout, texBeginAccessFlags, texBeginStageFlags);
-		beginDependency.srcAccessMask |= texBeginAccessFlags;
-		beginDependency.srcStageMask |= texBeginStageFlags;
+		// I had a TODO here, but I don't remember why...
+		if (colorAttachment.layout != VK_IMAGE_LAYOUT_UNDEFINED)
+		{
+			Vulkan::addImageLayoutTransitionOptions(true, true, false, colorAttachment.layout, beginDependency.srcAccessMask, beginDependency.srcStageMask);
+			Vulkan::addImageLayoutTransitionOptions(false, true, false, colorAttachment.layout, endDependency.dstAccessMask, endDependency.dstStageMask);
+		}
 
-		VkAccessFlags texEndAccessFlags = 0;
-		VkPipelineStageFlags texEndStageFlags = 0;
-		Vulkan::setImageLayoutTransitionOptions(false, true, false, colorAttachment.layout, texEndAccessFlags, texEndStageFlags);
-		endDependency.dstAccessMask |= texEndAccessFlags;
-		endDependency.dstStageMask |= texEndStageFlags;
+		if (colorAttachment.msaaLayout != VK_IMAGE_LAYOUT_UNDEFINED)
+		{
+			Vulkan::addImageLayoutTransitionOptions(true, true, false, colorAttachment.msaaLayout, beginDependency.srcAccessMask, beginDependency.srcStageMask);
+			Vulkan::addImageLayoutTransitionOptions(false, true, false, colorAttachment.msaaLayout, endDependency.dstAccessMask, endDependency.dstStageMask);
+		}
 	}
 
-	subPass.colorAttachmentCount = static_cast<uint32_t>(colorAttachmentRefs.size());
+	subPass.colorAttachmentCount = static_cast<uint32_t>(configuration.colorAttachments.size());
 	subPass.pColorAttachments = colorAttachmentRefs.data();
 
 	VkAttachmentReference depthStencilAttachmentRef{};
@@ -2252,36 +2258,43 @@ VkRenderPass Graphics::createRenderPass(RenderPassConfiguration &configuration)
 		depthStencilAttachment.finalLayout = configuration.staticData.depthStencilAttachment.layout;
 		attachments.push_back(depthStencilAttachment);
 
-		VkAccessFlags texBeginAccessFlags = 0;
-		VkPipelineStageFlags texBeginStageFlags = 0;
-		Vulkan::setImageLayoutTransitionOptions(true, true, true, configuration.staticData.depthStencilAttachment.layout, texBeginAccessFlags, texBeginStageFlags);
-		beginDependency.srcAccessMask |= texBeginAccessFlags;
-		beginDependency.srcStageMask |= texBeginStageFlags;
-
-		VkAccessFlags texEndAccessFlags = 0;
-		VkPipelineStageFlags texEndStageFlags = 0;
-		Vulkan::setImageLayoutTransitionOptions(false, true, true, configuration.staticData.depthStencilAttachment.layout, texEndAccessFlags, texEndStageFlags);
-		endDependency.dstAccessMask |= texEndAccessFlags;
-		endDependency.dstStageMask |= texEndStageFlags;
+		Vulkan::addImageLayoutTransitionOptions(true, true, true, configuration.staticData.depthStencilAttachment.layout, beginDependency.srcAccessMask, beginDependency.srcStageMask);
+		Vulkan::addImageLayoutTransitionOptions(false, true, true, configuration.staticData.depthStencilAttachment.layout, endDependency.dstAccessMask, endDependency.dstStageMask);
 	}
 
-	VkAttachmentReference colorAttachmentResolveRef{};
+	// Add resolve attachments after everything else to make pClearValues simpler to implement.
 	if (configuration.staticData.resolve)
 	{
-		colorAttachmentResolveRef.attachment = attachment++;
-		colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
-		subPass.pResolveAttachments = &colorAttachmentResolveRef;
+		for (const auto &colorAttachment : configuration.colorAttachments)
+		{
+			VkAttachmentReference reference{};
+			reference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
-		VkAttachmentDescription colorAttachmentResolve{};
-		colorAttachmentResolve.format = configuration.colorAttachments.at(0).format;
-		colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT;
-		colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
-		colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
-		colorAttachmentResolve.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
-		colorAttachmentResolve.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
-		colorAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
-		colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
-		attachments.push_back(colorAttachmentResolve);
+			if (colorAttachment.layout == VK_IMAGE_LAYOUT_UNDEFINED)
+			{
+				reference.attachment = VK_ATTACHMENT_UNUSED;
+				colorResolveAttachmentRefs.push_back(reference);
+			}
+			else
+			{
+				reference.attachment = attachment++;
+				colorResolveAttachmentRefs.push_back(reference);
+
+				VkAttachmentDescription resolveDescription{};
+				resolveDescription.format = colorAttachment.format;
+				resolveDescription.samples = VK_SAMPLE_COUNT_1_BIT;
+				resolveDescription.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
+				resolveDescription.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
+				resolveDescription.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
+				resolveDescription.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
+				resolveDescription.initialLayout = colorAttachment.layout;
+				resolveDescription.finalLayout = colorAttachment.layout;
+
+				attachments.push_back(resolveDescription);
+			}
+		}
+
+		subPass.pResolveAttachments = colorResolveAttachmentRefs.data();
 	}
 
 	std::array<VkSubpassDependency, 2> dependencies = { beginDependency, endDependency };
@@ -2496,8 +2509,6 @@ void Graphics::setDefaultRenderPass()
 
 	RenderPassConfiguration renderPassConfiguration{};
 
-	renderPassConfiguration.colorAttachments.push_back({ swapChainImageFormat, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_ATTACHMENT_LOAD_OP_LOAD, msaaSamples });
-
 	VkFormat dsformat = backbufferHasDepth || backbufferHasStencil ? depthStencilFormat : VK_FORMAT_UNDEFINED;
 	renderPassConfiguration.staticData.depthStencilAttachment = { dsformat, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, VK_ATTACHMENT_LOAD_OP_LOAD, VK_ATTACHMENT_LOAD_OP_LOAD, msaaSamples };
 	if (msaaSamples & VK_SAMPLE_COUNT_1_BIT)
@@ -2512,19 +2523,22 @@ void Graphics::setDefaultRenderPass()
 
 	if (msaaSamples & VK_SAMPLE_COUNT_1_BIT)
 	{
+		renderPassConfiguration.colorAttachments.push_back({ swapChainImageFormat, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_UNDEFINED, VK_ATTACHMENT_LOAD_OP_LOAD, msaaSamples });
+
 		if (!swapChainImageViews.empty())
 			framebufferConfiguration.colorViews.push_back(swapChainImageViews.at(imageIndex));
 		else
 			framebufferConfiguration.colorViews.push_back(fakeBackbuffer->getRenderTargetView(0, 0));
-		framebufferConfiguration.staticData.resolveView = VK_NULL_HANDLE;
 	}
 	else
 	{
+		renderPassConfiguration.colorAttachments.push_back({ swapChainImageFormat, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_ATTACHMENT_LOAD_OP_LOAD, msaaSamples });
+
 		framebufferConfiguration.colorViews.push_back(colorImageView);
 		if (!swapChainImageViews.empty())
-			framebufferConfiguration.staticData.resolveView = swapChainImageViews.at(imageIndex);
+			framebufferConfiguration.colorResolveViews.push_back(swapChainImageViews.at(imageIndex));
 		else
-			framebufferConfiguration.staticData.resolveView = fakeBackbuffer->getRenderTargetView(0, 0);
+			framebufferConfiguration.colorResolveViews.push_back(fakeBackbuffer->getRenderTargetView(0, 0));
 	}
 
 	renderPassState.renderPassConfiguration = std::move(renderPassConfiguration);
@@ -2554,35 +2568,62 @@ void Graphics::setDefaultRenderPass()
 	}
 }
 
-void Graphics::setRenderPass(const RenderTargets &rts, int pixelw, int pixelh, bool hasSRGBtexture)
+void Graphics::setRenderPass(const RenderTargets &rts, int pixelw, int pixelh)
 {
-	// fixme: hasSRGBtexture
 	RenderPassConfiguration renderPassConfiguration{};
+	VkSampleCountFlagBits msaa = VK_SAMPLE_COUNT_1_BIT;
+
 	for (const auto &color : rts.colors)
+	{
+		auto tex = (Texture *)color.texture;
 		renderPassConfiguration.colorAttachments.push_back({ 
-			Vulkan::getTextureFormat(color.texture->getPixelFormat()).internalFormat,
-			((Texture*)color.texture)->getImageLayout(),
+			Vulkan::getTextureFormat(tex->getPixelFormat()).internalFormat,
+			tex->getImageLayout(),
+			tex->getMSAAImageLayout(),
 			VK_ATTACHMENT_LOAD_OP_LOAD,
-			dynamic_cast<Texture*>(color.texture)->getMsaaSamples() });
+			tex->getMsaaSamples() });
+
+		if (tex->getMSAAImageLayout() != VK_IMAGE_LAYOUT_UNDEFINED && tex->getImageLayout() != VK_IMAGE_LAYOUT_UNDEFINED)
+			renderPassConfiguration.staticData.resolve = true;
+
+		msaa = tex->getMsaaSamples();
+	}
+
 	if (rts.depthStencil.texture != nullptr)
+	{
+		auto tex = (Texture *)rts.depthStencil.texture;
 		renderPassConfiguration.staticData.depthStencilAttachment = {
 			Vulkan::getTextureFormat(rts.depthStencil.texture->getPixelFormat()).internalFormat,
-			((Texture*)rts.depthStencil.texture)->getImageLayout(),
+			tex->getImageLayout(),
 			VK_ATTACHMENT_LOAD_OP_LOAD,
 			VK_ATTACHMENT_LOAD_OP_LOAD,
-			dynamic_cast<Texture*>(rts.depthStencil.texture)->getMsaaSamples() };
+			tex->getMsaaSamples() };
+
+		msaa = tex->getMsaaSamples();
+	}
 
 	FramebufferConfiguration configuration{};
 
 	for (const auto &color : rts.colors)
 	{
 		auto tex = (Texture*)color.texture;
-		configuration.colorViews.push_back(tex->getRenderTargetView(color.mipmap, color.slice));
+		if (tex->getMSAA() > 1)
+		{
+			configuration.colorViews.push_back(tex->getMSAARenderTargetView(color.mipmap, color.slice));
+			configuration.colorResolveViews.push_back(tex->getRenderTargetView(color.mipmap, color.slice));
+		}
+		else
+		{
+			configuration.colorViews.push_back(tex->getRenderTargetView(color.mipmap, color.slice));
+		}
 	}
 	if (rts.depthStencil.texture != nullptr)
 	{
 		auto tex = (Texture*)rts.depthStencil.texture;
-		configuration.staticData.depthView = tex->getRenderTargetView(rts.depthStencil.mipmap, rts.depthStencil.slice);
+		if (tex->getMSAA() > 1)
+			configuration.staticData.depthView = tex->getMSAARenderTargetView(rts.depthStencil.mipmap, rts.depthStencil.slice);
+		else
+			configuration.staticData.depthView = tex->getRenderTargetView(rts.depthStencil.mipmap, rts.depthStencil.slice);
 	}
 
 	configuration.staticData.width = static_cast<uint32_t>(pixelw);
@@ -2606,7 +2647,7 @@ void Graphics::setRenderPass(const RenderTargets &rts, int pixelw, int pixelh, b
 	renderPassState.pipeline = VK_NULL_HANDLE;
 	renderPassState.width = static_cast<float>(pixelw);
 	renderPassState.height = static_cast<float>(pixelh);
-	renderPassState.msaa = VK_SAMPLE_COUNT_1_BIT;
+	renderPassState.msaa = msaa;
 	renderPassState.numColorAttachments = static_cast<uint32_t>(rts.colors.size());
 	renderPassState.packedColorAttachmentFormats = 0;
 	for (size_t i = 0; i < rts.colors.size(); i++)

+ 6 - 3
src/modules/graphics/vulkan/Graphics.h

@@ -50,6 +50,7 @@ struct ColorAttachment
 {
 	VkFormat format = VK_FORMAT_UNDEFINED;
 	VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
+	VkImageLayout msaaLayout = VK_IMAGE_LAYOUT_UNDEFINED;
 	VkAttachmentLoadOp loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
 	VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT;
 
@@ -57,6 +58,7 @@ struct ColorAttachment
 	{
 		return format == attachment.format &&
 			layout == attachment.layout &&
+			msaaLayout == attachment.msaaLayout &&
 			loadOp == attachment.loadOp &&
 			msaaSamples == attachment.msaaSamples;
 	}
@@ -112,11 +114,11 @@ struct RenderPassConfigurationHasher
 struct FramebufferConfiguration
 {
 	std::vector<VkImageView> colorViews;
+	std::vector<VkImageView> colorResolveViews;
 
 	struct StaticFramebufferConfiguration
 	{
 		VkImageView depthView = VK_NULL_HANDLE;
-		VkImageView resolveView = VK_NULL_HANDLE;
 
 		uint32_t width = 0;
 		uint32_t height = 0;
@@ -126,7 +128,7 @@ struct FramebufferConfiguration
 
 	bool operator==(const FramebufferConfiguration &conf) const
 	{
-		return colorViews == conf.colorViews &&
+		return colorViews == conf.colorViews && colorResolveViews == conf.colorResolveViews &&
 			(memcmp(&staticData, &conf.staticData, sizeof(StaticFramebufferConfiguration)) == 0);
 	}
 };
@@ -137,6 +139,7 @@ struct FramebufferConfigurationHasher
 	{
 		size_t hashes[] = {
 			XXH32(configuration.colorViews.data(), configuration.colorViews.size() * sizeof(VkImageView), 0),
+			XXH32(configuration.colorResolveViews.data(), configuration.colorResolveViews.size() * sizeof(VkImageView), 0),
 			XXH32(&configuration.staticData, sizeof(configuration.staticData), 0),
 		};
 
@@ -337,7 +340,7 @@ private:
 		VertexAttributesID attributesID,
 		const BufferBindings &buffers, graphics::Texture *texture,
 		PrimitiveType, CullMode);
-	void setRenderPass(const RenderTargets &rts, int pixelw, int pixelh, bool hasSRGBtexture);
+	void setRenderPass(const RenderTargets &rts, int pixelw, int pixelh);
 	void setDefaultRenderPass();
 	void startRenderPass();
 	void endRenderPass();

+ 1 - 1
src/modules/graphics/vulkan/Shader.cpp

@@ -1044,7 +1044,7 @@ void Shader::setTextureDescriptor(const UniformInfo *info, love::graphics::Textu
 
 	// Samplers may change after this call, so they're set just before the
 	// descriptor set is used instead of here.
-	VkImageView view = vkTexture != nullptr ? (VkImageView)vkTexture->getRenderTargetHandle() : VK_NULL_HANDLE;
+	VkImageView view = vkTexture != nullptr ? (VkImageView)vkTexture->getHandle() : VK_NULL_HANDLE;
 	if (view != imageInfo.imageView)
 	{
 		imageInfo.imageLayout = vkTexture != nullptr ? vkTexture->getImageLayout() : VK_IMAGE_LAYOUT_UNDEFINED;

+ 194 - 113
src/modules/graphics/vulkan/Texture.cpp

@@ -24,6 +24,7 @@
 #include "Buffer.h"
 
 #include <limits>
+#include <array>
 
 namespace love
 {
@@ -132,7 +133,7 @@ bool Texture::loadVolatile()
 		imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
 		imageInfo.usage = usageFlags;
 		imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
-		imageInfo.samples = msaaSamples;
+		imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
 
 		VkImageFormatListCreateInfo viewFormatsInfo{};
 		viewFormatsInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO;
@@ -153,26 +154,54 @@ bool Texture::loadVolatile()
 
 		imageAllocationCreateInfo.usage = VMA_MEMORY_USAGE_AUTO;
 
-		if (vmaCreateImage(allocator, &imageInfo, &imageAllocationCreateInfo, &textureImage, &textureImageAllocation, nullptr) != VK_SUCCESS)
-			throw love::Exception("failed to create image");
+		if ((msaaSamples & VK_SAMPLE_COUNT_1_BIT) != 0 || readable)
+		{
+			if (vmaCreateImage(allocator, &imageInfo, &imageAllocationCreateInfo, &imageData.image, &imageData.allocation, nullptr) != VK_SUCCESS)
+				throw love::Exception("failed to create image");
+		}
+
+		if ((msaaSamples & VK_SAMPLE_COUNT_1_BIT) == 0)
+		{
+			imageInfo.samples = msaaSamples;
+			if (vmaCreateImage(allocator, &imageInfo, &imageAllocationCreateInfo, &msaaImageData.image, &msaaImageData.allocation, nullptr) != VK_SUCCESS)
+				throw love::Exception("failed to create MSAA image");
+		}
 
 		auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
 
 		if (computeWrite)
-			imageLayout = VK_IMAGE_LAYOUT_GENERAL;
+			imageData.layout = VK_IMAGE_LAYOUT_GENERAL;
 		else if (readable)
-			imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+			imageData.layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
 		else if (renderTarget && isPixelFormatDepthStencil(format))
-			imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+			imageData.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
 		else if (renderTarget)
-			imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+			imageData.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 		else // TODO: is there a better layout for this situation?
-			imageLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+			imageData.layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+
+		if (imageData.image != VK_NULL_HANDLE)
+		{
+			Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget,
+				VK_IMAGE_LAYOUT_UNDEFINED, imageData.layout,
+				0, VK_REMAINING_MIP_LEVELS,
+				0, VK_REMAINING_ARRAY_LAYERS);
+		}
 
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget,
-			VK_IMAGE_LAYOUT_UNDEFINED, imageLayout,
-			0, VK_REMAINING_MIP_LEVELS,
-			0, VK_REMAINING_ARRAY_LAYERS);
+		if (msaaImageData.image != VK_NULL_HANDLE)
+		{
+			if (renderTarget && isPixelFormatDepthStencil(format))
+				msaaImageData.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+			else if (renderTarget)
+				msaaImageData.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+			else // TODO: is there a better layout for this situation?
+				msaaImageData.layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
+
+			Vulkan::cmdTransitionImageLayout(commandBuffer, msaaImageData.image, format, renderTarget,
+				VK_IMAGE_LAYOUT_UNDEFINED, msaaImageData.layout,
+				0, VK_REMAINING_MIP_LEVELS,
+				0, VK_REMAINING_ARRAY_LAYERS);
+		}
 
 		bool hasdata = slices.get(0, 0) != nullptr;
 
@@ -195,14 +224,20 @@ bool Texture::loadVolatile()
 			}
 		}
 		else
-			clear();
+		{
+			if (imageData.image != VK_NULL_HANDLE)
+				clear(imageData);
+			if (msaaImageData.image != VK_NULL_HANDLE)
+				clear(msaaImageData);
+		}
 	}
 	else
 	{
 		Texture *roottex = (Texture *) rootView.texture;
-		textureImage = roottex->textureImage;
-		textureImageAllocation = VK_NULL_HANDLE;
-		imageLayout = roottex->imageLayout;
+		imageData = roottex->imageData;
+		msaaImageData = roottex->msaaImageData;
+		imageData.allocation = VK_NULL_HANDLE;
+		msaaImageData.allocation = VK_NULL_HANDLE;
 		msaaSamples = roottex->msaaSamples;
 	}
 
@@ -214,34 +249,8 @@ bool Texture::loadVolatile()
 
 	if (renderTarget)
 	{
-		renderTargetImageViews.resize(getMipmapCount());
-		for (int mip = 0; mip < getMipmapCount(); mip++)
-		{
-			renderTargetImageViews.at(mip).resize(layerCount);
-
-			for (int slice = 0; slice < layerCount; slice++)
-			{
-				VkImageViewCreateInfo viewInfo{};
-				viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
-				viewInfo.image = textureImage;
-				viewInfo.viewType = Vulkan::getImageViewType(getTextureType());
-				if (viewInfo.viewType == VK_IMAGE_VIEW_TYPE_CUBE)
-					viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
-				viewInfo.format = vulkanFormat.internalFormat;
-				viewInfo.subresourceRange.aspectMask = imageAspect;
-				viewInfo.subresourceRange.baseMipLevel = mip + rootView.startMipmap;
-				viewInfo.subresourceRange.levelCount = 1;
-				viewInfo.subresourceRange.baseArrayLayer = slice + rootView.startLayer;
-				viewInfo.subresourceRange.layerCount = 1;
-				viewInfo.components.r = vulkanFormat.swizzleR;
-				viewInfo.components.g = vulkanFormat.swizzleG;
-				viewInfo.components.b = vulkanFormat.swizzleB;
-				viewInfo.components.a = vulkanFormat.swizzleA;
-
-				if (vkCreateImageView(device, &viewInfo, nullptr, &renderTargetImageViews.at(mip).at(slice)) != VK_SUCCESS)
-					throw love::Exception("could not create render target image view");
-			}
-		}
+		createRenderTargetImageViews(imageData);
+		createRenderTargetImageViews(msaaImageData);
 	}
 
 	if (!debugName.empty())
@@ -253,15 +262,25 @@ bool Texture::loadVolatile()
 			if (root)
 			{
 				nameInfo.objectType = VK_OBJECT_TYPE_IMAGE;
-				nameInfo.objectHandle = (uint64_t)textureImage;
+				nameInfo.objectHandle = (uint64_t)imageData.image;
 			}
 			else
 			{
 				nameInfo.objectType = VK_OBJECT_TYPE_IMAGE_VIEW;
-				nameInfo.objectHandle = (uint64_t)textureImageView;
+				nameInfo.objectHandle = (uint64_t)imageData.imageView;
 			}
 			nameInfo.pObjectName = debugName.c_str();
 			vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
+
+			if (msaaImageData.image != VK_NULL_HANDLE && root)
+			{
+				nameInfo.objectType = VK_OBJECT_TYPE_IMAGE;
+				nameInfo.objectHandle = (uint64_t)msaaImageData.image;
+
+				std::string msaaName = debugName + " (MSAA buffer)";
+				nameInfo.pObjectName = msaaName.c_str();
+				vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
+			}
 		}
 	}
 
@@ -272,26 +291,29 @@ bool Texture::loadVolatile()
 
 void Texture::unloadVolatile()
 {
-	if (textureImage == VK_NULL_HANDLE)
-		return;
+	std::array<VulkanImageData *, 2> datas = {&imageData, &msaaImageData};
 
-	vgfx->queueCleanUp([
-		device = device, 
-		textureImageView = textureImageView, 
-		allocator = allocator, 
-		textureImage = textureImage, 
-		textureImageAllocation = textureImageAllocation,
-		textureImageViews = std::move(renderTargetImageViews)] () {
-		vkDestroyImageView(device, textureImageView, nullptr);
-		if (textureImageAllocation)
-			vmaDestroyImage(allocator, textureImage, textureImageAllocation);
-		for (const auto &views : textureImageViews)
-			for (const auto &view : views)
-				vkDestroyImageView(device, view, nullptr);
-	});
+	for (const VulkanImageData *data : datas)
+	{
+		if (data->image == VK_NULL_HANDLE)
+			continue;
+
+		vgfx->queueCleanUp([
+			device = device,
+			allocator = allocator,
+			imageData = std::move(*data)] () {
+			if (imageData.imageView != VK_NULL_HANDLE)
+				vkDestroyImageView(device, imageData.imageView, nullptr);
+			if (imageData.allocation != VK_NULL_HANDLE)
+				vmaDestroyImage(allocator, imageData.image, imageData.allocation);
+			for (const auto &views : imageData.renderTargetImageViews)
+				for (const auto &view : views)
+					vkDestroyImageView(device, view, nullptr);
+		});
+	}
 
-	textureImage = VK_NULL_HANDLE;
-	textureImageAllocation = VK_NULL_HANDLE;
+	imageData = {};
+	msaaImageData = {};
 
 	updateGraphicsMemorySize(false);
 }
@@ -303,7 +325,7 @@ Texture::~Texture()
 
 ptrdiff_t Texture::getRenderTargetHandle() const
 {
-	return (ptrdiff_t)textureImageView;
+	return (ptrdiff_t)imageData.image;
 }
 
 ptrdiff_t Texture::getSamplerHandle() const
@@ -313,7 +335,16 @@ ptrdiff_t Texture::getSamplerHandle() const
 
 VkImageView Texture::getRenderTargetView(int mip, int layer)
 {
-	return renderTargetImageViews.at(mip).at(layer);
+	if (imageData.renderTargetImageViews.empty())
+		return imageData.imageView;
+	return imageData.renderTargetImageViews.at(mip).at(layer);
+}
+
+VkImageView Texture::getMSAARenderTargetView(int mip, int layer)
+{
+	if (msaaImageData.renderTargetImageViews.empty())
+		return msaaImageData.imageView;
+	return msaaImageData.renderTargetImageViews.at(mip).at(layer);
 }
 
 VkSampleCountFlagBits Texture::getMsaaSamples() const
@@ -328,7 +359,7 @@ int Texture::getMSAA() const
 
 ptrdiff_t Texture::getHandle() const
 {
-	return (ptrdiff_t)textureImage;
+	return (ptrdiff_t)imageData.imageView;
 }
 
 void Texture::setSamplerState(const SamplerState &s)
@@ -339,7 +370,12 @@ void Texture::setSamplerState(const SamplerState &s)
 
 VkImageLayout Texture::getImageLayout() const
 {
-	return imageLayout;
+	return imageData.layout;
+}
+
+VkImageLayout Texture::getMSAAImageLayout() const
+{
+	return msaaImageData.layout;
 }
 
 void Texture::createTextureImageView()
@@ -347,7 +383,7 @@ void Texture::createTextureImageView()
 	auto vulkanFormat = Vulkan::getTextureFormat(format);
 	VkImageViewCreateInfo viewInfo{};
 	viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
-	viewInfo.image = textureImage;
+	viewInfo.image = imageData.image;
 	viewInfo.viewType = Vulkan::getImageViewType(getTextureType());
 	viewInfo.format = vulkanFormat.internalFormat;
 	viewInfo.subresourceRange.aspectMask = imageAspect;
@@ -364,11 +400,56 @@ void Texture::createTextureImageView()
 	viewInfo.components.b = vulkanFormat.swizzleB;
 	viewInfo.components.a = vulkanFormat.swizzleA;
 
-	if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS)
-		throw love::Exception("could not create texture image view");
+	imageData.imageView = VK_NULL_HANDLE;
+	if (imageData.image != VK_NULL_HANDLE && readable)
+	{
+		if (vkCreateImageView(device, &viewInfo, nullptr, &imageData.imageView) != VK_SUCCESS)
+			throw love::Exception("could not create texture image view");
+	}
+}
+
+void Texture::createRenderTargetImageViews(VulkanImageData &data)
+{
+	if (data.image == VK_NULL_HANDLE || (getMipmapCount() == 1 && layerCount == 1 && data.imageView != VK_NULL_HANDLE))
+	{
+		// Reuse the main image view in this situation, if it exists.
+		data.renderTargetImageViews.clear();
+		return;
+	}
+
+	auto vulkanFormat = Vulkan::getTextureFormat(format);
+
+	data.renderTargetImageViews.resize(getMipmapCount());
+	for (int mip = 0; mip < getMipmapCount(); mip++)
+	{
+		data.renderTargetImageViews.at(mip).resize(layerCount);
+
+		for (int slice = 0; slice < layerCount; slice++)
+		{
+			VkImageViewCreateInfo viewInfo{};
+			viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
+			viewInfo.image = data.image;
+			viewInfo.viewType = Vulkan::getImageViewType(getTextureType());
+			if (viewInfo.viewType == VK_IMAGE_VIEW_TYPE_CUBE)
+				viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
+			viewInfo.format = vulkanFormat.internalFormat;
+			viewInfo.subresourceRange.aspectMask = imageAspect;
+			viewInfo.subresourceRange.baseMipLevel = mip + rootView.startMipmap;
+			viewInfo.subresourceRange.levelCount = 1;
+			viewInfo.subresourceRange.baseArrayLayer = slice + rootView.startLayer;
+			viewInfo.subresourceRange.layerCount = 1;
+			viewInfo.components.r = vulkanFormat.swizzleR;
+			viewInfo.components.g = vulkanFormat.swizzleG;
+			viewInfo.components.b = vulkanFormat.swizzleB;
+			viewInfo.components.a = vulkanFormat.swizzleA;
+
+			if (vkCreateImageView(device, &viewInfo, nullptr, &data.renderTargetImageViews.at(mip).at(slice)) != VK_SUCCESS)
+				throw love::Exception("could not create render target image view");
+		}
+	}
 }
 
-void Texture::clear()
+void Texture::clear(const VulkanImageData &data)
 {
 	auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
 
@@ -379,12 +460,12 @@ void Texture::clear()
 	range.baseArrayLayer = 0;
 	range.layerCount = VK_REMAINING_ARRAY_LAYERS;
 
-	VkImageLayout clearLayout = imageLayout == VK_IMAGE_LAYOUT_GENERAL ? imageLayout : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+	VkImageLayout clearLayout = data.layout == VK_IMAGE_LAYOUT_GENERAL ? data.layout : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
 
-	if (clearLayout != imageLayout)
+	if (clearLayout != data.layout)
 	{
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget,
-			imageLayout, clearLayout,
+		Vulkan::cmdTransitionImageLayout(commandBuffer, data.image, format, renderTarget,
+			data.layout, clearLayout,
 			0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS);
 	}
 
@@ -393,18 +474,18 @@ void Texture::clear()
 		VkClearDepthStencilValue depthStencilColor{};
 		depthStencilColor.depth = 0.0f;
 		depthStencilColor.stencil = 0;
-		vkCmdClearDepthStencilImage(commandBuffer, textureImage, clearLayout, &depthStencilColor, 1, &range);
+		vkCmdClearDepthStencilImage(commandBuffer, data.image, clearLayout, &depthStencilColor, 1, &range);
 	}
 	else
 	{
 		auto clearColor = getClearColor(this, ColorD(0, 0, 0, 0));
-		vkCmdClearColorImage(commandBuffer, textureImage, clearLayout, &clearColor, 1, &range);
+		vkCmdClearColorImage(commandBuffer, data.image, clearLayout, &clearColor, 1, &range);
 	}
 
-	if (clearLayout != imageLayout)
+	if (clearLayout != data.layout)
 	{
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget,
-			clearLayout, imageLayout,
+		Vulkan::cmdTransitionImageLayout(commandBuffer, data.image, format, renderTarget,
+			clearLayout, data.layout,
 			0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS);
 	}
 }
@@ -450,15 +531,15 @@ void Texture::generateMipmapsInternal()
 {
 	auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
 
-	if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget,
-			imageLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 
+	if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
+		Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget,
+			imageData.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
 			rootView.startMipmap, static_cast<uint32_t>(getMipmapCount()),
 			rootView.startLayer, static_cast<uint32_t>(layerCount));
 
 	VkImageMemoryBarrier barrier{};
 	barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
-	barrier.image = textureImage;
+	barrier.image = imageData.image;
 	barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
 	barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
 	barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
@@ -477,7 +558,7 @@ void Texture::generateMipmapsInternal()
 		barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
 		barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
 
-		if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
+		if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
 			vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
 				0, nullptr,
 				0, nullptr,
@@ -498,29 +579,29 @@ void Texture::generateMipmapsInternal()
 		blit.dstSubresource.baseArrayLayer = rootView.startLayer;
 		blit.dstSubresource.layerCount = static_cast<uint32_t>(layerCount);
 
-		if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
+		if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
 		{
 			vkCmdBlitImage(commandBuffer,
-				textureImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-				textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+				imageData.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+				imageData.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
 				1, &blit,
 				VK_FILTER_LINEAR);
 		}
 		else
 		{
 			vkCmdBlitImage(commandBuffer,
-				textureImage, VK_IMAGE_LAYOUT_GENERAL,
-				textureImage, VK_IMAGE_LAYOUT_GENERAL,
+				imageData.image, VK_IMAGE_LAYOUT_GENERAL,
+				imageData.image, VK_IMAGE_LAYOUT_GENERAL,
 				1, &blit,
 				VK_FILTER_LINEAR);
 		}
 
 		barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
-		barrier.newLayout = imageLayout;
+		barrier.newLayout = imageData.layout;
 		barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
 		barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
 
-		if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
+		if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
 			vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
 				0, nullptr,
 				0, nullptr,
@@ -529,11 +610,11 @@ void Texture::generateMipmapsInternal()
 
 	barrier.subresourceRange.baseMipLevel = rootView.startMipmap + mipLevels - 1;
 	barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
-	barrier.newLayout = imageLayout;
+	barrier.newLayout = imageData.layout;
 	barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
 	barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
 
-	if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
+	if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
 		vkCmdPipelineBarrier(commandBuffer,
 			VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
 			0, nullptr,
@@ -587,31 +668,31 @@ void Texture::uploadByteData(const void *data, size_t size, int level, int slice
 
 	auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
 
-	if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
+	if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
 	{
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget,
-			imageLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 
+		Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget,
+			imageData.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
 			level, 1, baseLayer, 1);
 
 		vkCmdCopyBufferToImage(
 			commandBuffer,
 			stagingBuffer,
-			textureImage,
+			imageData.image,
 			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
 			1,
 			&region
 		);
 
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget,
-			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, imageLayout,
+		Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget,
+			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, imageData.layout,
 			level, 1, baseLayer, 1);
 	}
 	else
 		vkCmdCopyBufferToImage(
 			commandBuffer,
 			stagingBuffer,
-			textureImage,
-			imageLayout,
+			imageData.image,
+			imageData.layout,
 			1,
 			&region
 		);
@@ -641,16 +722,16 @@ void Texture::copyFromBuffer(graphics::Buffer *source, size_t sourceoffset, int
 	region.imageExtent.width = static_cast<uint32_t>(rect.w);
 	region.imageExtent.height = static_cast<uint32_t>(rect.h);
 
-	if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
+	if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
 	{
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget, imageLayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layers.mipLevel, 1, layers.baseArrayLayer, 1);
+		Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget, imageData.layout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, layers.mipLevel, 1, layers.baseArrayLayer, 1);
 
-		vkCmdCopyBufferToImage(commandBuffer, (VkBuffer)source->getHandle(), textureImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
+		vkCmdCopyBufferToImage(commandBuffer, (VkBuffer)source->getHandle(), imageData.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
 
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, imageLayout, layers.mipLevel, 1, layers.baseArrayLayer, 1);
+		Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, imageData.layout, layers.mipLevel, 1, layers.baseArrayLayer, 1);
 	}
 	else
-		vkCmdCopyBufferToImage(commandBuffer, (VkBuffer)source->getHandle(), textureImage, VK_IMAGE_LAYOUT_GENERAL, 1, &region);
+		vkCmdCopyBufferToImage(commandBuffer, (VkBuffer)source->getHandle(), imageData.image, VK_IMAGE_LAYOUT_GENERAL, 1, &region);
 }
 
 void Texture::copyToBuffer(graphics::Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth, size_t size)
@@ -674,16 +755,16 @@ void Texture::copyToBuffer(graphics::Buffer *dest, int slice, int mipmap, const
 	region.imageExtent.height = static_cast<uint32_t>(rect.h);
 	region.imageExtent.depth = 1;
 
-	if (imageLayout != VK_IMAGE_LAYOUT_GENERAL)
+	if (imageData.layout != VK_IMAGE_LAYOUT_GENERAL)
 	{
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget, imageLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layers.mipLevel, 1, layers.baseArrayLayer, 1);
+		Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget, imageData.layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, layers.mipLevel, 1, layers.baseArrayLayer, 1);
 
-		vkCmdCopyImageToBuffer(commandBuffer, textureImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, (VkBuffer) dest->getHandle(), 1, &region);
+		vkCmdCopyImageToBuffer(commandBuffer, imageData.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, (VkBuffer) dest->getHandle(), 1, &region);
 
-		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage, format, renderTarget, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, imageLayout, layers.mipLevel, 1, layers.baseArrayLayer, 1);
+		Vulkan::cmdTransitionImageLayout(commandBuffer, imageData.image, format, renderTarget, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, imageData.layout, layers.mipLevel, 1, layers.baseArrayLayer, 1);
 	}
 	else
-		vkCmdCopyImageToBuffer(commandBuffer, textureImage, VK_IMAGE_LAYOUT_GENERAL, (VkBuffer)dest->getHandle(), 1, &region);
+		vkCmdCopyImageToBuffer(commandBuffer, imageData.image, VK_IMAGE_LAYOUT_GENERAL, (VkBuffer)dest->getHandle(), 1, &region);
 
 	// TODO: This could be combined with the cmdTransitionImageLayout barrier.
 	((Buffer *)dest)->postGPUWriteBarrier(commandBuffer);

+ 16 - 6
src/modules/graphics/vulkan/Texture.h

@@ -51,6 +51,7 @@ public:
 	void setSamplerState(const SamplerState &s) override;
 
 	VkImageLayout getImageLayout() const;
+	VkImageLayout getMSAAImageLayout() const;
 
 	void copyFromBuffer(graphics::Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect) override;
 	void copyToBuffer(graphics::Buffer *dest, int slice, int mipmap, const Rect &rect, size_t destoffset, int destwidth, size_t size) override;
@@ -59,6 +60,7 @@ public:
 	ptrdiff_t getSamplerHandle() const override;
 
 	VkImageView getRenderTargetView(int mip, int layer);
+	VkImageView getMSAARenderTargetView(int mip, int layer);
 	VkSampleCountFlagBits getMsaaSamples() const;
 
 	void uploadByteData(const void *data, size_t size, int level, int slice, const Rect &r) override;
@@ -71,18 +73,26 @@ public:
 	static VkClearColorValue getClearColor(love::graphics::Texture *texture, const ColorD &color);
 
 private:
+
+	struct VulkanImageData
+	{
+		VkImage image = VK_NULL_HANDLE;
+		VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
+		VmaAllocation allocation = VK_NULL_HANDLE;
+		VkImageView imageView = VK_NULL_HANDLE;
+		std::vector<std::vector<VkImageView>> renderTargetImageViews;
+	};
+
 	void createTextureImageView();
-	void clear();
+	void createRenderTargetImageViews(VulkanImageData &data);
+	void clear(const VulkanImageData &data);
 
 	Graphics *vgfx = nullptr;
 	VkDevice device = VK_NULL_HANDLE;
 	VkImageAspectFlags imageAspect;
 	VmaAllocator allocator = VK_NULL_HANDLE;
-	VkImage textureImage = VK_NULL_HANDLE;
-	VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
-	VmaAllocation textureImageAllocation = VK_NULL_HANDLE;
-	VkImageView textureImageView = VK_NULL_HANDLE;
-	std::vector<std::vector<VkImageView>> renderTargetImageViews;
+	VulkanImageData imageData;
+	VulkanImageData msaaImageData;
 	VkSampler textureSampler = VK_NULL_HANDLE;
 	Slices slices;
 	int layerCount = 0;

+ 22 - 22
src/modules/graphics/vulkan/Vulkan.cpp

@@ -822,22 +822,22 @@ VkIndexType Vulkan::getVulkanIndexBufferType(IndexDataType type)
 	}
 }
 
-void Vulkan::setImageLayoutTransitionOptions(bool previous, bool renderTarget, bool depthStencil, VkImageLayout layout, VkAccessFlags &accessMask, VkPipelineStageFlags &stageFlags)
+void Vulkan::addImageLayoutTransitionOptions(bool previous, bool renderTarget, bool depthStencil, VkImageLayout layout, VkAccessFlags &accessMask, VkPipelineStageFlags &stageFlags)
 {
 	switch (layout)
 	{
 	case VK_IMAGE_LAYOUT_UNDEFINED:
-		accessMask = 0;
+		accessMask |= 0;
 		if (previous)
-			stageFlags = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
+			stageFlags |= VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
 		else
-			stageFlags = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
+			stageFlags |= VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
 		break;
 	case VK_IMAGE_LAYOUT_GENERAL:
 		// We use the general image layout for images that are both compute write and readable.
 		// todo: can we optimize this?
-		accessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT;
-		stageFlags = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
+		accessMask |= VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT;
+		stageFlags |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT;
 		if (renderTarget)
 		{
 			if (depthStencil)
@@ -853,28 +853,28 @@ void Vulkan::setImageLayoutTransitionOptions(bool previous, bool renderTarget, b
 		}
 		break;
 	case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
-		accessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
-		stageFlags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+		accessMask |= VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+		stageFlags |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
 		break;
 	case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
-		accessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
-		stageFlags = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
+		accessMask |= VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT;
+		stageFlags |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
 		break;
 	case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
-		accessMask = VK_ACCESS_SHADER_READ_BIT;
-		stageFlags = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
+		accessMask |= VK_ACCESS_SHADER_READ_BIT;
+		stageFlags |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
 		break;
 	case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
-		accessMask = VK_ACCESS_TRANSFER_READ_BIT;
-		stageFlags = VK_PIPELINE_STAGE_TRANSFER_BIT;
+		accessMask |= VK_ACCESS_TRANSFER_READ_BIT;
+		stageFlags |= VK_PIPELINE_STAGE_TRANSFER_BIT;
 		break;
 	case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
-		accessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
-		stageFlags = VK_PIPELINE_STAGE_TRANSFER_BIT;
+		accessMask |= VK_ACCESS_TRANSFER_WRITE_BIT;
+		stageFlags |= VK_PIPELINE_STAGE_TRANSFER_BIT;
 		break;
 	case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:
-		accessMask = 0;
-		stageFlags = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
+		accessMask |= 0;
+		stageFlags |= VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
 		break;
 	default:
 		throw love::Exception("unimplemented image layout");
@@ -897,11 +897,11 @@ void Vulkan::cmdTransitionImageLayout(VkCommandBuffer commandBuffer, VkImage ima
 
 	const PixelFormatInfo &info = getPixelFormatInfo(format);
 
-	VkPipelineStageFlags sourceStage;
-	VkPipelineStageFlags destinationStage;
+	VkPipelineStageFlags sourceStage = 0;
+	VkPipelineStageFlags destinationStage = 0;
 
-	setImageLayoutTransitionOptions(true, renderTarget, info.depth || info.stencil, oldLayout, barrier.srcAccessMask, sourceStage);
-	setImageLayoutTransitionOptions(false, renderTarget, info.depth || info.stencil, newLayout, barrier.dstAccessMask, destinationStage);
+	addImageLayoutTransitionOptions(true, renderTarget, info.depth || info.stencil, oldLayout, barrier.srcAccessMask, sourceStage);
+	addImageLayoutTransitionOptions(false, renderTarget, info.depth || info.stencil, newLayout, barrier.dstAccessMask, destinationStage);
 
 	if (info.color)
 		barrier.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_COLOR_BIT;

+ 2 - 2
src/modules/graphics/vulkan/Vulkan.h

@@ -80,8 +80,8 @@ public:
 	static VkStencilOp getStencilOp(StencilAction);
 	static VkIndexType getVulkanIndexBufferType(IndexDataType type);
 
-	static void setImageLayoutTransitionOptions(
-		bool previous, bool renderTarget, bool depthStencil, VkImageLayout layout, VkAccessFlags &accessMask, VkPipelineStageFlags &stageFlags);
+	static void addImageLayoutTransitionOptions(
+		bool previous, bool renderTarget, bool depthStencil, VkImageLayout layout, VkAccessFlags& accessMask, VkPipelineStageFlags& stageFlags);
 
 	static void cmdTransitionImageLayout(
 		VkCommandBuffer, VkImage, PixelFormat format, bool renderTarget, VkImageLayout oldLayout, VkImageLayout newLayout,