From 9ab19d057dd4c7355aeef4bf92890a7ba375330d Mon Sep 17 00:00:00 2001
From: Magnus Norddahl <dpjudas@users.noreply.github.com>
Date: Thu, 16 May 2019 18:26:47 +0200
Subject: [PATCH] - centralize how image transitions are done in the vulkan
 backend

---
 src/CMakeLists.txt                            |   1 +
 .../vulkan/renderer/vk_postprocess.cpp        | 245 +++++-------------
 .../vulkan/renderer/vk_postprocess.h          |  29 +--
 .../vulkan/renderer/vk_renderbuffers.cpp      | 103 ++++----
 .../vulkan/renderer/vk_renderbuffers.h        |  27 +-
 .../vulkan/renderer/vk_renderpass.cpp         |   2 +-
 .../vulkan/renderer/vk_renderstate.cpp        |   4 +-
 .../vulkan/system/vk_framebuffer.cpp          |  36 +--
 .../vulkan/textures/vk_hwtexture.cpp          | 175 +++++--------
 src/rendering/vulkan/textures/vk_hwtexture.h  |  14 +-
 .../vulkan/textures/vk_imagetransition.cpp    | 130 ++++++++++
 .../vulkan/textures/vk_imagetransition.h      |  39 +++
 12 files changed, 385 insertions(+), 420 deletions(-)
 create mode 100644 src/rendering/vulkan/textures/vk_imagetransition.cpp
 create mode 100644 src/rendering/vulkan/textures/vk_imagetransition.h

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a2039a3b2..463ac9092 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -918,6 +918,7 @@ set (VULKAN_SOURCES
 	rendering/vulkan/shaders/vk_shader.cpp
 	rendering/vulkan/textures/vk_samplers.cpp
 	rendering/vulkan/textures/vk_hwtexture.cpp
+	rendering/vulkan/textures/vk_imagetransition.cpp
 	rendering/vulkan/thirdparty/volk/volk.c
 	rendering/vulkan/thirdparty/vk_mem_alloc/vk_mem_alloc.cpp
 )
diff --git a/src/rendering/vulkan/renderer/vk_postprocess.cpp b/src/rendering/vulkan/renderer/vk_postprocess.cpp
index ee454a88d..78df34630 100644
--- a/src/rendering/vulkan/renderer/vk_postprocess.cpp
+++ b/src/rendering/vulkan/renderer/vk_postprocess.cpp
@@ -7,6 +7,7 @@
 #include "vulkan/system/vk_buffers.h"
 #include "vulkan/system/vk_swapchain.h"
 #include "vulkan/renderer/vk_renderstate.h"
+#include "vulkan/textures/vk_imagetransition.h"
 #include "hwrenderer/utility/hw_cvars.h"
 #include "hwrenderer/postprocessing/hw_postprocess.h"
 #include "hwrenderer/postprocessing/hw_postprocess_cvars.h"
@@ -30,11 +31,11 @@ void VkPostprocess::SetActiveRenderTarget()
 	auto fb = GetVulkanFrameBuffer();
 	auto buffers = fb->GetBuffers();
 
-	VkPPImageTransition imageTransition;
-	imageTransition.addImage(buffers->PipelineImage[mCurrentPipelineImage].get(), &buffers->PipelineLayout[mCurrentPipelineImage], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, false);
+	VkImageTransition imageTransition;
+	imageTransition.addImage(&buffers->PipelineImage[mCurrentPipelineImage], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, false);
 	imageTransition.execute(fb->GetDrawCommands());
 
-	fb->GetRenderState()->SetRenderTarget(buffers->PipelineView[mCurrentPipelineImage].get(), nullptr, buffers->GetWidth(), buffers->GetHeight(), VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_1_BIT);
+	fb->GetRenderState()->SetRenderTarget(buffers->PipelineImage[mCurrentPipelineImage].View.get(), nullptr, buffers->GetWidth(), buffers->GetHeight(), VK_FORMAT_R16G16B16A16_SFLOAT, VK_SAMPLE_COUNT_1_BIT);
 }
 
 void VkPostprocess::PostProcessScene(int fixedcm, const std::function<void()> &afterBloomDrawEndScene2D)
@@ -70,14 +71,14 @@ void VkPostprocess::BlitSceneToPostprocess()
 
 	mCurrentPipelineImage = 0;
 
-	VkPPImageTransition imageTransition0;
-	imageTransition0.addImage(buffers->SceneColor.get(), &buffers->SceneColorLayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, false);
-	imageTransition0.addImage(buffers->PipelineImage[mCurrentPipelineImage].get(), &buffers->PipelineLayout[mCurrentPipelineImage], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true);
-	imageTransition0.execute(fb->GetDrawCommands());
+	VkImageTransition imageTransition;
+	imageTransition.addImage(&buffers->SceneColor, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, false);
+	imageTransition.addImage(&buffers->PipelineImage[mCurrentPipelineImage], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true);
+	imageTransition.execute(fb->GetDrawCommands());
 
 	if (buffers->GetSceneSamples() != VK_SAMPLE_COUNT_1_BIT)
 	{
-		auto sceneColor = buffers->SceneColor.get();
+		auto sceneColor = buffers->SceneColor.Image.get();
 		VkImageResolve resolve = {};
 		resolve.srcOffset = { 0, 0, 0 };
 		resolve.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
@@ -92,12 +93,12 @@ void VkPostprocess::BlitSceneToPostprocess()
 		resolve.extent = { (uint32_t)sceneColor->width, (uint32_t)sceneColor->height, 1 };
 		cmdbuffer->resolveImage(
 			sceneColor->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-			buffers->PipelineImage[mCurrentPipelineImage]->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+			buffers->PipelineImage[mCurrentPipelineImage].Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
 			1, &resolve);
 	}
 	else
 	{
-		auto sceneColor = buffers->SceneColor.get();
+		auto sceneColor = buffers->SceneColor.Image.get();
 		VkImageBlit blit = {};
 		blit.srcOffsets[0] = { 0, 0, 0 };
 		blit.srcOffsets[1] = { sceneColor->width, sceneColor->height, 1 };
@@ -113,7 +114,7 @@ void VkPostprocess::BlitSceneToPostprocess()
 		blit.dstSubresource.layerCount = 1;
 		cmdbuffer->blitImage(
 			sceneColor->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-			buffers->PipelineImage[mCurrentPipelineImage]->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+			buffers->PipelineImage[mCurrentPipelineImage].Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
 			1, &blit, VK_FILTER_NEAREST);
 	}
 }
@@ -123,51 +124,48 @@ void VkPostprocess::ImageTransitionScene(bool undefinedSrcLayout)
 	auto fb = GetVulkanFrameBuffer();
 	auto buffers = fb->GetBuffers();
 
-	VkPPImageTransition imageTransition;
-	imageTransition.addImage(buffers->SceneColor.get(), &buffers->SceneColorLayout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
-	imageTransition.addImage(buffers->SceneFog.get(), &buffers->SceneFogLayout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
-	imageTransition.addImage(buffers->SceneNormal.get(), &buffers->SceneNormalLayout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
-	imageTransition.addImage(buffers->SceneDepthStencil.get(), &buffers->SceneDepthStencilLayout, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
+	VkImageTransition imageTransition;
+	imageTransition.addImage(&buffers->SceneColor, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
+	imageTransition.addImage(&buffers->SceneFog, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
+	imageTransition.addImage(&buffers->SceneNormal, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
+	imageTransition.addImage(&buffers->SceneDepthStencil, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, undefinedSrcLayout);
 	imageTransition.execute(fb->GetDrawCommands());
 }
 
-void VkPostprocess::BlitCurrentToImage(VulkanImage *dstimage, VkImageLayout *dstlayout, VkImageLayout finallayout)
+void VkPostprocess::BlitCurrentToImage(VkTextureImage *dstimage, VkImageLayout finallayout)
 {
 	auto fb = GetVulkanFrameBuffer();
 
 	fb->GetRenderState()->EndRenderPass();
 
-	auto srcimage = fb->GetBuffers()->PipelineImage[mCurrentPipelineImage].get();
-	auto srclayout = &fb->GetBuffers()->PipelineLayout[mCurrentPipelineImage];
+	auto srcimage = &fb->GetBuffers()->PipelineImage[mCurrentPipelineImage];
 	auto cmdbuffer = fb->GetDrawCommands();
 
-	*dstlayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; // needed by VkPPImageTransition.addImage. Actual layout is undefined.
-
-	VkPPImageTransition imageTransition0;
-	imageTransition0.addImage(srcimage, srclayout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, false);
-	imageTransition0.addImage(dstimage, dstlayout, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true);
+	VkImageTransition imageTransition0;
+	imageTransition0.addImage(srcimage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, false);
+	imageTransition0.addImage(dstimage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true);
 	imageTransition0.execute(cmdbuffer);
 
 	VkImageBlit blit = {};
 	blit.srcOffsets[0] = { 0, 0, 0 };
-	blit.srcOffsets[1] = { srcimage->width, srcimage->height, 1 };
+	blit.srcOffsets[1] = { srcimage->Image->width, srcimage->Image->height, 1 };
 	blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
 	blit.srcSubresource.mipLevel = 0;
 	blit.srcSubresource.baseArrayLayer = 0;
 	blit.srcSubresource.layerCount = 1;
 	blit.dstOffsets[0] = { 0, 0, 0 };
-	blit.dstOffsets[1] = { dstimage->width, dstimage->height, 1 };
+	blit.dstOffsets[1] = { dstimage->Image->width, dstimage->Image->height, 1 };
 	blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
 	blit.dstSubresource.mipLevel = 0;
 	blit.dstSubresource.baseArrayLayer = 0;
 	blit.dstSubresource.layerCount = 1;
 	cmdbuffer->blitImage(
-		srcimage->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
-		dstimage->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+		srcimage->Image->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
+		dstimage->Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
 		1, &blit, VK_FILTER_NEAREST);
 
-	VkPPImageTransition imageTransition1;
-	imageTransition1.addImage(dstimage, dstlayout, finallayout, false);
+	VkImageTransition imageTransition1;
+	imageTransition1.addImage(dstimage, finallayout, false);
 	imageTransition1.execute(cmdbuffer);
 }
 
@@ -179,7 +177,7 @@ void VkPostprocess::DrawPresentTexture(const IntRect &box, bool applyGamma, bool
 	hw_postprocess.customShaders.Run(&renderstate, "screen");
 
 	PresentUniforms uniforms;
-	if (!applyGamma /*|| framebuffer->IsHWGammaActive()*/)
+	if (!applyGamma)
 	{
 		uniforms.InvGamma = 1.0f;
 		uniforms.Contrast = 1.0f;
@@ -263,8 +261,8 @@ void VkPostprocess::UpdateShadowMap()
 		auto fb = GetVulkanFrameBuffer();
 		auto buffers = fb->GetBuffers();
 
-		VkPPImageTransition imageTransition;
-		imageTransition.addImage(buffers->Shadowmap.get(), &buffers->ShadowmapLayout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, false);
+		VkImageTransition imageTransition;
+		imageTransition.addImage(&buffers->Shadowmap, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, false);
 		imageTransition.execute(fb->GetDrawCommands());
 
 		screen->mShadowMap.FinishUpdate();
@@ -345,14 +343,14 @@ VkPPTexture::VkPPTexture(PPTexture *texture)
 		imgbuilder.setUsage(VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT);
 	if (!imgbuilder.isFormatSupported(fb->device))
 		I_FatalError("Vulkan device does not support the image format required by a postprocess texture\n");
-	Image = imgbuilder.create(fb->device);
-	Image->SetDebugName("VkPPTexture");
+	TexImage.Image = imgbuilder.create(fb->device);
+	TexImage.Image->SetDebugName("VkPPTexture");
 	Format = format;
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(Image.get(), format);
-	View = viewbuilder.create(fb->device);
-	View->SetDebugName("VkPPTextureView");
+	viewbuilder.setImage(TexImage.Image.get(), format);
+	TexImage.View = viewbuilder.create(fb->device);
+	TexImage.View->SetDebugName("VkPPTextureView");
 
 	if (texture->Data)
 	{
@@ -363,9 +361,9 @@ VkPPTexture::VkPPTexture(PPTexture *texture)
 		Staging = stagingbuilder.create(fb->device);
 		Staging->SetDebugName("VkPPTextureStaging");
 
-		PipelineBarrier barrier0;
-		barrier0.addImage(Image.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
-		barrier0.execute(fb->GetTransferCommands(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+		VkImageTransition barrier0;
+		barrier0.addImage(&TexImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true);
+		barrier0.execute(fb->GetTransferCommands());
 
 		void *data = Staging->Map(0, totalsize);
 		memcpy(data, texture->Data.get(), totalsize);
@@ -377,19 +375,17 @@ VkPPTexture::VkPPTexture(PPTexture *texture)
 		region.imageExtent.depth = 1;
 		region.imageExtent.width = texture->Width;
 		region.imageExtent.height = texture->Height;
-		fb->GetTransferCommands()->copyBufferToImage(Staging->buffer, Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
+		fb->GetTransferCommands()->copyBufferToImage(Staging->buffer, TexImage.Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
 
-		PipelineBarrier barrier1;
-		barrier1.addImage(Image.get(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
-		barrier1.execute(fb->GetTransferCommands(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
-		Layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+		VkImageTransition barrier1;
+		barrier1.addImage(&TexImage, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, false);
+		barrier1.execute(fb->GetTransferCommands());
 	}
 	else
 	{
-		PipelineBarrier barrier;
-		barrier.addImage(Image.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT);
-		barrier.execute(fb->GetTransferCommands(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
-		Layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+		VkImageTransition barrier;
+		barrier.addImage(&TexImage, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true);
+		barrier.execute(fb->GetTransferCommands());
 	}
 }
 
@@ -542,16 +538,16 @@ VulkanDescriptorSet *VkPPRenderState::GetInput(VkPPRenderPassSetup *passSetup, c
 	descriptors->SetDebugName("VkPostprocess.descriptors");
 
 	WriteDescriptors write;
-	VkPPImageTransition imageTransition;
+	VkImageTransition imageTransition;
 
 	for (unsigned int index = 0; index < textures.Size(); index++)
 	{
 		const PPTextureInput &input = textures[index];
 		VulkanSampler *sampler = pp->GetSampler(input.Filter, input.Wrap);
-		TextureImage tex = GetTexture(input.Type, input.Texture);
+		VkTextureImage *tex = GetTexture(input.Type, input.Texture);
 
-		write.addCombinedImageSampler(descriptors.get(), index, tex.view, sampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
-		imageTransition.addImage(tex.image, tex.layout, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, false);
+		write.addCombinedImageSampler(descriptors.get(), index, tex->DepthOnlyView ? tex->DepthOnlyView.get() : tex->View.get(), sampler, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
+		imageTransition.addImage(tex, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, false);
 	}
 
 	if (bindShadowMapBuffers)
@@ -573,21 +569,21 @@ VulkanFramebuffer *VkPPRenderState::GetOutput(VkPPRenderPassSetup *passSetup, co
 {
 	auto fb = GetVulkanFrameBuffer();
 
-	TextureImage tex = GetTexture(output.Type, output.Texture);
+	VkTextureImage *tex = GetTexture(output.Type, output.Texture);
 
 	VkImageView view;
 	int w, h;
-	if (tex.view)
+	if (tex)
 	{
-		VkPPImageTransition imageTransition;
-		imageTransition.addImage(tex.image, tex.layout, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, output.Type == PPTextureType::NextPipelineTexture);
+		VkImageTransition imageTransition;
+		imageTransition.addImage(tex, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, output.Type == PPTextureType::NextPipelineTexture);
 		if (stencilTest)
-			imageTransition.addImage(fb->GetBuffers()->SceneDepthStencil.get(), &fb->GetBuffers()->SceneDepthStencilLayout, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false);
+			imageTransition.addImage(&fb->GetBuffers()->SceneDepthStencil, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false);
 		imageTransition.execute(fb->GetDrawCommands());
 
-		view = tex.view->view;
-		w = tex.image->width;
-		h = tex.image->height;
+		view = tex->View->view;
+		w = tex->Image->width;
+		h = tex->Image->height;
 	}
 	else
 	{
@@ -604,9 +600,8 @@ VulkanFramebuffer *VkPPRenderState::GetOutput(VkPPRenderPassSetup *passSetup, co
 		builder.setSize(w, h);
 		builder.addAttachment(view);
 		if (stencilTest)
-			builder.addAttachment(fb->GetBuffers()->SceneDepthStencilView.get());
+			builder.addAttachment(fb->GetBuffers()->SceneDepthStencil.View.get());
 		framebuffer = builder.create(GetVulkanFrameBuffer()->device);
-		framebuffer->SetDebugName(tex.debugname);
 	}
 
 	framebufferWidth = w;
@@ -614,10 +609,9 @@ VulkanFramebuffer *VkPPRenderState::GetOutput(VkPPRenderPassSetup *passSetup, co
 	return framebuffer.get();
 }
 
-VkPPRenderState::TextureImage VkPPRenderState::GetTexture(const PPTextureType &type, PPTexture *pptexture)
+VkTextureImage *VkPPRenderState::GetTexture(const PPTextureType &type, PPTexture *pptexture)
 {
 	auto fb = GetVulkanFrameBuffer();
-	TextureImage tex = {};
 
 	if (type == PPTextureType::CurrentPipelineTexture || type == PPTextureType::NextPipelineTexture)
 	{
@@ -625,67 +619,42 @@ VkPPRenderState::TextureImage VkPPRenderState::GetTexture(const PPTextureType &t
 		if (type == PPTextureType::NextPipelineTexture)
 			idx = (idx + 1) % VkRenderBuffers::NumPipelineImages;
 
-		tex.image = fb->GetBuffers()->PipelineImage[idx].get();
-		tex.view = fb->GetBuffers()->PipelineView[idx].get();
-		tex.layout = &fb->GetBuffers()->PipelineLayout[idx];
-		tex.debugname = "PipelineTexture";
+		return &fb->GetBuffers()->PipelineImage[idx];
 	}
 	else if (type == PPTextureType::PPTexture)
 	{
 		auto vktex = GetVkTexture(pptexture);
-		tex.image = vktex->Image.get();
-		tex.view = vktex->View.get();
-		tex.layout = &vktex->Layout;
-		tex.debugname = "PPTexture";
+		return &vktex->TexImage;
 	}
 	else if (type == PPTextureType::SceneColor)
 	{
-		tex.image = fb->GetBuffers()->SceneColor.get();
-		tex.view = fb->GetBuffers()->SceneColorView.get();
-		tex.layout = &fb->GetBuffers()->SceneColorLayout;
-		tex.debugname = "SceneColor";
+		return &fb->GetBuffers()->SceneColor;
 	}
 	else if (type == PPTextureType::SceneNormal)
 	{
-		tex.image = fb->GetBuffers()->SceneNormal.get();
-		tex.view = fb->GetBuffers()->SceneNormalView.get();
-		tex.layout = &fb->GetBuffers()->SceneNormalLayout;
-		tex.debugname = "SceneNormal";
+		return &fb->GetBuffers()->SceneNormal;
 	}
 	else if (type == PPTextureType::SceneFog)
 	{
-		tex.image = fb->GetBuffers()->SceneFog.get();
-		tex.view = fb->GetBuffers()->SceneFogView.get();
-		tex.layout = &fb->GetBuffers()->SceneFogLayout;
-		tex.debugname = "SceneFog";
+		return &fb->GetBuffers()->SceneFog;
 	}
 	else if (type == PPTextureType::SceneDepth)
 	{
-		tex.image = fb->GetBuffers()->SceneDepthStencil.get();
-		tex.view = fb->GetBuffers()->SceneDepthView.get();
-		tex.layout = &fb->GetBuffers()->SceneDepthStencilLayout;
-		tex.debugname = "SceneDepth";
+		return &fb->GetBuffers()->SceneDepthStencil;
 	}
 	else if (type == PPTextureType::ShadowMap)
 	{
-		tex.image = fb->GetBuffers()->Shadowmap.get();
-		tex.view = fb->GetBuffers()->ShadowmapView.get();
-		tex.layout = &fb->GetBuffers()->ShadowmapLayout;
-		tex.debugname = "Shadowmap";
+		return &fb->GetBuffers()->Shadowmap;
 	}
 	else if (type == PPTextureType::SwapChain)
 	{
-		tex.image = nullptr;
-		tex.view = nullptr;
-		tex.layout = nullptr;
-		tex.debugname = "SwapChain";
+		return nullptr;
 	}
 	else
 	{
 		I_FatalError("VkPPRenderState::GetTexture not implemented yet for this texture type");
+		return nullptr;
 	}
-
-	return tex;
 }
 
 VkPPShader *VkPPRenderState::GetVkShader(PPShader *shader)
@@ -805,79 +774,3 @@ void VkPPRenderPassSetup::CreateRenderPass(const VkPPRenderPassKey &key)
 	RenderPass = builder.create(GetVulkanFrameBuffer()->device);
 	RenderPass->SetDebugName("VkPPRenderPassSetup.RenderPass");
 }
-
-/////////////////////////////////////////////////////////////////////////////
-
-void VkPPImageTransition::addImage(VulkanImage *image, VkImageLayout *layout, VkImageLayout targetLayout, bool undefinedSrcLayout)
-{
-	if (*layout == targetLayout)
-		return;
-
-	VkAccessFlags srcAccess = 0;
-	VkAccessFlags dstAccess = 0;
-	VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
-
-	switch (*layout)
-	{
-	case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
-		srcAccess = VK_ACCESS_TRANSFER_READ_BIT;
-		srcStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
-		srcAccess = VK_ACCESS_SHADER_READ_BIT;
-		srcStageMask |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
-		srcAccess = VK_ACCESS_TRANSFER_WRITE_BIT;
-		srcStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
-		srcAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
-		srcStageMask |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
-		srcAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
-		srcStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
-		aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
-		break;
-	default:
-		I_FatalError("Unimplemented src image layout transition\n");
-	}
-
-	switch (targetLayout)
-	{
-	case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
-		dstAccess = VK_ACCESS_TRANSFER_READ_BIT;
-		dstStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
-		dstAccess = VK_ACCESS_SHADER_READ_BIT;
-		dstStageMask |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
-		dstAccess = VK_ACCESS_TRANSFER_WRITE_BIT;
-		dstStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
-		dstAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
-		dstStageMask |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
-		break;
-	case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
-		srcAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
-		srcStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
-		aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
-		break;
-	default:
-		I_FatalError("Unimplemented dst image layout transition\n");
-	}
-
-	barrier.addImage(image, undefinedSrcLayout ? VK_IMAGE_LAYOUT_UNDEFINED : *layout, targetLayout, srcAccess, dstAccess, aspectMask);
-	needbarrier = true;
-	*layout = targetLayout;
-}
-
-void VkPPImageTransition::execute(VulkanCommandBuffer *cmdbuffer)
-{
-	if (needbarrier)
-		barrier.execute(cmdbuffer, srcStageMask, dstStageMask);
-}
diff --git a/src/rendering/vulkan/renderer/vk_postprocess.h b/src/rendering/vulkan/renderer/vk_postprocess.h
index 667bf28b6..bc9f247c1 100644
--- a/src/rendering/vulkan/renderer/vk_postprocess.h
+++ b/src/rendering/vulkan/renderer/vk_postprocess.h
@@ -8,6 +8,7 @@
 #include "hwrenderer/postprocessing/hw_postprocess.h"
 #include "vulkan/system/vk_objects.h"
 #include "vulkan/system/vk_builders.h"
+#include "vulkan/textures/vk_imagetransition.h"
 
 class FString;
 
@@ -34,19 +35,6 @@ public:
 	bool operator!=(const VkPPRenderPassKey &other) const { return memcmp(this, &other, sizeof(VkPPRenderPassKey)) != 0; }
 };
 
-class VkPPImageTransition
-{
-public:
-	void addImage(VulkanImage *image, VkImageLayout *layout, VkImageLayout targetLayout, bool undefinedSrcLayout);
-	void execute(VulkanCommandBuffer *cmdbuffer);
-
-private:
-	PipelineBarrier barrier;
-	VkPipelineStageFlags srcStageMask = 0;
-	VkPipelineStageFlags dstStageMask = 0;
-	bool needbarrier = false;
-};
-
 class VkPostprocess
 {
 public:
@@ -67,7 +55,7 @@ public:
 	void ImageTransitionScene(bool undefinedSrcLayout);
 
 	void BlitSceneToPostprocess();
-	void BlitCurrentToImage(VulkanImage *image, VkImageLayout *layout, VkImageLayout finallayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
+	void BlitCurrentToImage(VkTextureImage *image, VkImageLayout finallayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
 	void DrawPresentTexture(const IntRect &box, bool applyGamma, bool clearBorders);
 
 private:
@@ -101,10 +89,8 @@ class VkPPTexture : public PPTextureBackend
 public:
 	VkPPTexture(PPTexture *texture);
 
-	std::unique_ptr<VulkanImage> Image;
-	std::unique_ptr<VulkanImageView> View;
+	VkTextureImage TexImage;
 	std::unique_ptr<VulkanBuffer> Staging;
-	VkImageLayout Layout = VK_IMAGE_LAYOUT_UNDEFINED;
 	VkFormat Format;
 };
 
@@ -143,12 +129,5 @@ private:
 	VkPPShader *GetVkShader(PPShader *shader);
 	VkPPTexture *GetVkTexture(PPTexture *texture);
 
-	struct TextureImage
-	{
-		VulkanImage *image;
-		VulkanImageView *view;
-		VkImageLayout *layout;
-		const char *debugname;
-	};
-	TextureImage GetTexture(const PPTextureType &type, PPTexture *tex);
+	VkTextureImage *GetTexture(const PPTextureType &type, PPTexture *tex);
 };
diff --git a/src/rendering/vulkan/renderer/vk_renderbuffers.cpp b/src/rendering/vulkan/renderer/vk_renderbuffers.cpp
index 29524599f..7bde9789e 100644
--- a/src/rendering/vulkan/renderer/vk_renderbuffers.cpp
+++ b/src/rendering/vulkan/renderer/vk_renderbuffers.cpp
@@ -71,39 +71,32 @@ void VkRenderBuffers::CreatePipeline(int width, int height)
 	for (int i = 0; i < NumPipelineImages; i++)
 	{
 		PipelineImage[i].reset();
-		PipelineView[i].reset();
 	}
 
-	PipelineBarrier barrier;
+	VkImageTransition barrier;
 	for (int i = 0; i < NumPipelineImages; i++)
 	{
 		ImageBuilder builder;
 		builder.setSize(width, height);
 		builder.setFormat(VK_FORMAT_R16G16B16A16_SFLOAT);
 		builder.setUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);
-		PipelineImage[i] = builder.create(fb->device);
-		PipelineImage[i]->SetDebugName("VkRenderBuffers.PipelineImage");
+		PipelineImage[i].Image = builder.create(fb->device);
+		PipelineImage[i].Image->SetDebugName("VkRenderBuffers.PipelineImage");
 
 		ImageViewBuilder viewbuilder;
-		viewbuilder.setImage(PipelineImage[i].get(), VK_FORMAT_R16G16B16A16_SFLOAT);
-		PipelineView[i] = viewbuilder.create(fb->device);
-		PipelineView[i]->SetDebugName("VkRenderBuffers.PipelineView");
+		viewbuilder.setImage(PipelineImage[i].Image.get(), VK_FORMAT_R16G16B16A16_SFLOAT);
+		PipelineImage[i].View = viewbuilder.create(fb->device);
+		PipelineImage[i].View->SetDebugName("VkRenderBuffers.PipelineView");
 
-		barrier.addImage(PipelineImage[i].get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
-		PipelineLayout[i] = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+		barrier.addImage(&PipelineImage[i], VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true);
 	}
-	barrier.execute(fb->GetDrawCommands(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT);
+	barrier.execute(fb->GetDrawCommands());
 }
 
 void VkRenderBuffers::CreateScene(int width, int height, VkSampleCountFlagBits samples)
 {
 	auto fb = GetVulkanFrameBuffer();
 
-	SceneColorView.reset();
-	SceneDepthStencilView.reset();
-	SceneDepthView.reset();
-	SceneNormalView.reset();
-	SceneFogView.reset();
 	SceneColor.reset();
 	SceneDepthStencil.reset();
 	SceneNormal.reset();
@@ -114,14 +107,12 @@ void VkRenderBuffers::CreateScene(int width, int height, VkSampleCountFlagBits s
 	CreateSceneNormal(width, height, samples);
 	CreateSceneFog(width, height, samples);
 
-	PipelineBarrier barrier;
-	barrier.addImage(SceneColor.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
-	barrier.addImage(SceneDepthStencil.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
-	barrier.addImage(SceneNormal.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
-	barrier.addImage(SceneFog.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
-	barrier.execute(fb->GetDrawCommands(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT);
-
-	SceneColorLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
+	VkImageTransition barrier;
+	barrier.addImage(&SceneColor, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true);
+	barrier.addImage(&SceneDepthStencil, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, true);
+	barrier.addImage(&SceneNormal, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true);
+	barrier.addImage(&SceneFog, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true);
+	barrier.execute(fb->GetDrawCommands());
 }
 
 void VkRenderBuffers::CreateSceneColor(int width, int height, VkSampleCountFlagBits samples)
@@ -133,13 +124,13 @@ void VkRenderBuffers::CreateSceneColor(int width, int height, VkSampleCountFlagB
 	builder.setSamples(samples);
 	builder.setFormat(VK_FORMAT_R16G16B16A16_SFLOAT);
 	builder.setUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
-	SceneColor = builder.create(fb->device);
-	SceneColor->SetDebugName("VkRenderBuffers.SceneColor");
+	SceneColor.Image = builder.create(fb->device);
+	SceneColor.Image->SetDebugName("VkRenderBuffers.SceneColor");
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(SceneColor.get(), VK_FORMAT_R16G16B16A16_SFLOAT);
-	SceneColorView = viewbuilder.create(fb->device);
-	SceneColorView->SetDebugName("VkRenderBuffers.SceneColorView");
+	viewbuilder.setImage(SceneColor.Image.get(), VK_FORMAT_R16G16B16A16_SFLOAT);
+	SceneColor.View = viewbuilder.create(fb->device);
+	SceneColor.View->SetDebugName("VkRenderBuffers.SceneColorView");
 }
 
 void VkRenderBuffers::CreateSceneDepthStencil(int width, int height, VkSampleCountFlagBits samples)
@@ -160,17 +151,18 @@ void VkRenderBuffers::CreateSceneDepthStencil(int width, int height, VkSampleCou
 			I_FatalError("This device does not support any of the required depth stencil image formats.");
 		}
 	}
-	SceneDepthStencil = builder.create(fb->device);
-	SceneDepthStencil->SetDebugName("VkRenderBuffers.SceneDepthStencil");
+	SceneDepthStencil.Image = builder.create(fb->device);
+	SceneDepthStencil.Image->SetDebugName("VkRenderBuffers.SceneDepthStencil");
+	SceneDepthStencil.AspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(SceneDepthStencil.get(), SceneDepthStencilFormat, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
-	SceneDepthStencilView = viewbuilder.create(fb->device);
-	SceneDepthStencilView->SetDebugName("VkRenderBuffers.SceneDepthStencilView");
+	viewbuilder.setImage(SceneDepthStencil.Image.get(), SceneDepthStencilFormat, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
+	SceneDepthStencil.View = viewbuilder.create(fb->device);
+	SceneDepthStencil.View->SetDebugName("VkRenderBuffers.SceneDepthStencilView");
 
-	viewbuilder.setImage(SceneDepthStencil.get(), SceneDepthStencilFormat, VK_IMAGE_ASPECT_DEPTH_BIT);
-	SceneDepthView = viewbuilder.create(fb->device);
-	SceneDepthView->SetDebugName("VkRenderBuffers.SceneDepthView");
+	viewbuilder.setImage(SceneDepthStencil.Image.get(), SceneDepthStencilFormat, VK_IMAGE_ASPECT_DEPTH_BIT);
+	SceneDepthStencil.DepthOnlyView = viewbuilder.create(fb->device);
+	SceneDepthStencil.DepthOnlyView->SetDebugName("VkRenderBuffers.SceneDepthView");
 }
 
 void VkRenderBuffers::CreateSceneFog(int width, int height, VkSampleCountFlagBits samples)
@@ -182,13 +174,13 @@ void VkRenderBuffers::CreateSceneFog(int width, int height, VkSampleCountFlagBit
 	builder.setSamples(samples);
 	builder.setFormat(VK_FORMAT_R8G8B8A8_UNORM);
 	builder.setUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
-	SceneFog = builder.create(fb->device);
-	SceneFog->SetDebugName("VkRenderBuffers.SceneFog");
+	SceneFog.Image = builder.create(fb->device);
+	SceneFog.Image->SetDebugName("VkRenderBuffers.SceneFog");
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(SceneFog.get(), VK_FORMAT_R8G8B8A8_UNORM);
-	SceneFogView = viewbuilder.create(fb->device);
-	SceneFogView->SetDebugName("VkRenderBuffers.SceneFogView");
+	viewbuilder.setImage(SceneFog.Image.get(), VK_FORMAT_R8G8B8A8_UNORM);
+	SceneFog.View = viewbuilder.create(fb->device);
+	SceneFog.View->SetDebugName("VkRenderBuffers.SceneFogView");
 }
 
 void VkRenderBuffers::CreateSceneNormal(int width, int height, VkSampleCountFlagBits samples)
@@ -200,22 +192,21 @@ void VkRenderBuffers::CreateSceneNormal(int width, int height, VkSampleCountFlag
 	builder.setSamples(samples);
 	builder.setFormat(VK_FORMAT_A2R10G10B10_UNORM_PACK32);
 	builder.setUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
-	SceneNormal = builder.create(fb->device);
-	SceneNormal->SetDebugName("VkRenderBuffers.SceneNormal");
+	SceneNormal.Image = builder.create(fb->device);
+	SceneNormal.Image->SetDebugName("VkRenderBuffers.SceneNormal");
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(SceneNormal.get(), VK_FORMAT_A2R10G10B10_UNORM_PACK32);
-	SceneNormalView = viewbuilder.create(fb->device);
-	SceneNormalView->SetDebugName("VkRenderBuffers.SceneNormalView");
+	viewbuilder.setImage(SceneNormal.Image.get(), VK_FORMAT_A2R10G10B10_UNORM_PACK32);
+	SceneNormal.View = viewbuilder.create(fb->device);
+	SceneNormal.View->SetDebugName("VkRenderBuffers.SceneNormalView");
 }
 
 void VkRenderBuffers::CreateShadowmap()
 {
-	if (Shadowmap && Shadowmap->width == gl_shadowmap_quality)
+	if (Shadowmap.Image && Shadowmap.Image->width == gl_shadowmap_quality)
 		return;
 
 	Shadowmap.reset();
-	ShadowmapView.reset();
 
 	auto fb = GetVulkanFrameBuffer();
 
@@ -223,17 +214,17 @@ void VkRenderBuffers::CreateShadowmap()
 	builder.setSize(gl_shadowmap_quality, 1024);
 	builder.setFormat(VK_FORMAT_R32_SFLOAT);
 	builder.setUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
-	Shadowmap = builder.create(fb->device);
-	Shadowmap->SetDebugName("VkRenderBuffers.Shadowmap");
+	Shadowmap.Image = builder.create(fb->device);
+	Shadowmap.Image->SetDebugName("VkRenderBuffers.Shadowmap");
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(Shadowmap.get(), VK_FORMAT_R32_SFLOAT);
-	ShadowmapView = viewbuilder.create(fb->device);
-	ShadowmapView->SetDebugName("VkRenderBuffers.ShadowmapView");
+	viewbuilder.setImage(Shadowmap.Image.get(), VK_FORMAT_R32_SFLOAT);
+	Shadowmap.View = viewbuilder.create(fb->device);
+	Shadowmap.View->SetDebugName("VkRenderBuffers.ShadowmapView");
 
-	PipelineBarrier barrier;
-	barrier.addImage(Shadowmap.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 0, VK_ACCESS_SHADER_READ_BIT);
-	barrier.execute(fb->GetDrawCommands(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+	VkImageTransition barrier;
+	barrier.addImage(&Shadowmap, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, true);
+	barrier.execute(fb->GetDrawCommands());
 
 	if (!ShadowmapSampler)
 	{
diff --git a/src/rendering/vulkan/renderer/vk_renderbuffers.h b/src/rendering/vulkan/renderer/vk_renderbuffers.h
index 63b84f996..b1df96b02 100644
--- a/src/rendering/vulkan/renderer/vk_renderbuffers.h
+++ b/src/rendering/vulkan/renderer/vk_renderbuffers.h
@@ -2,6 +2,7 @@
 #pragma once
 
 #include "vulkan/system/vk_objects.h"
+#include "vulkan/textures/vk_imagetransition.h"
 
 class VkRenderBuffers
 {
@@ -17,30 +18,18 @@ public:
 	int GetSceneHeight() const { return mSceneHeight; }
 	VkSampleCountFlagBits GetSceneSamples() const { return mSamples; }
 
-	std::unique_ptr<VulkanImage> SceneColor;
-	std::unique_ptr<VulkanImage> SceneDepthStencil;
-	std::unique_ptr<VulkanImage> SceneNormal;
-	std::unique_ptr<VulkanImage> SceneFog;
-	std::unique_ptr<VulkanImageView> SceneColorView;
-	std::unique_ptr<VulkanImageView> SceneDepthStencilView;
-	std::unique_ptr<VulkanImageView> SceneDepthView;
-	std::unique_ptr<VulkanImageView> SceneNormalView;
-	std::unique_ptr<VulkanImageView> SceneFogView;
+	VkTextureImage SceneColor;
+	VkTextureImage SceneDepthStencil;
+	VkTextureImage SceneNormal;
+	VkTextureImage SceneFog;
+
 	VkFormat SceneDepthStencilFormat = VK_FORMAT_D24_UNORM_S8_UINT;
-	VkImageLayout SceneColorLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
-	VkImageLayout SceneDepthStencilLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
-	VkImageLayout SceneNormalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
-	VkImageLayout SceneFogLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
 
 	static const int NumPipelineImages = 2;
-	std::unique_ptr<VulkanImage> PipelineImage[NumPipelineImages];
-	std::unique_ptr<VulkanImageView> PipelineView[NumPipelineImages];
-	VkImageLayout PipelineLayout[NumPipelineImages] = { VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL };
+	VkTextureImage PipelineImage[NumPipelineImages];
 
-	std::unique_ptr<VulkanImage> Shadowmap;
-	std::unique_ptr<VulkanImageView> ShadowmapView;
+	VkTextureImage Shadowmap;
 	std::unique_ptr<VulkanSampler> ShadowmapSampler;
-	VkImageLayout ShadowmapLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
 
 private:
 	void CreatePipeline(int width, int height);
diff --git a/src/rendering/vulkan/renderer/vk_renderpass.cpp b/src/rendering/vulkan/renderer/vk_renderpass.cpp
index fc0669419..9fc5b9ab6 100644
--- a/src/rendering/vulkan/renderer/vk_renderpass.cpp
+++ b/src/rendering/vulkan/renderer/vk_renderpass.cpp
@@ -175,7 +175,7 @@ void VkRenderPassManager::UpdateDynamicSet()
 	update.addBuffer(DynamicSet.get(), 1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, fb->LightBufferSSO->mBuffer.get());
 	update.addBuffer(DynamicSet.get(), 2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, fb->MatricesUBO->mBuffer.get(), 0, sizeof(MatricesUBO));
 	update.addBuffer(DynamicSet.get(), 3, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, fb->StreamUBO->mBuffer.get(), 0, sizeof(StreamUBO));
-	update.addCombinedImageSampler(DynamicSet.get(), 4, fb->GetBuffers()->ShadowmapView.get(), fb->GetBuffers()->ShadowmapSampler.get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
+	update.addCombinedImageSampler(DynamicSet.get(), 4, fb->GetBuffers()->Shadowmap.View.get(), fb->GetBuffers()->ShadowmapSampler.get(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
 	update.updateSets(fb->device);
 }
 
diff --git a/src/rendering/vulkan/renderer/vk_renderstate.cpp b/src/rendering/vulkan/renderer/vk_renderstate.cpp
index dab0d64b3..e9474f092 100644
--- a/src/rendering/vulkan/renderer/vk_renderstate.cpp
+++ b/src/rendering/vulkan/renderer/vk_renderstate.cpp
@@ -568,9 +568,9 @@ void VkRenderState::BeginRenderPass(const VkRenderPassKey &key, VulkanCommandBuf
 		builder.setSize(mRenderTarget.Width, mRenderTarget.Height);
 		builder.addAttachment(mRenderTarget.View);
 		if (key.DrawBuffers > 1)
-			builder.addAttachment(buffers->SceneFogView.get());
+			builder.addAttachment(buffers->SceneFog.View.get());
 		if (key.DrawBuffers > 2)
-			builder.addAttachment(buffers->SceneNormalView.get());
+			builder.addAttachment(buffers->SceneNormal.View.get());
 		if (key.UsesDepthStencil())
 			builder.addAttachment(mRenderTarget.DepthStencil);
 		framebuffer = builder.create(GetVulkanFrameBuffer()->device);
diff --git a/src/rendering/vulkan/system/vk_framebuffer.cpp b/src/rendering/vulkan/system/vk_framebuffer.cpp
index 2deff5223..173d5408b 100644
--- a/src/rendering/vulkan/system/vk_framebuffer.cpp
+++ b/src/rendering/vulkan/system/vk_framebuffer.cpp
@@ -444,7 +444,7 @@ sector_t *VulkanFrameBuffer::RenderViewpoint(FRenderViewpoint &mainvp, AActor *
 
 		if (mainview) // Bind the scene frame buffer and turn on draw buffers used by ssao
 		{
-			mRenderState->SetRenderTarget(GetBuffers()->SceneColorView.get(), GetBuffers()->SceneDepthStencilView.get(), GetBuffers()->GetWidth(), GetBuffers()->GetHeight(), VK_FORMAT_R16G16B16A16_SFLOAT, GetBuffers()->GetSceneSamples());
+			mRenderState->SetRenderTarget(GetBuffers()->SceneColor.View.get(), GetBuffers()->SceneDepthStencil.View.get(), GetBuffers()->GetWidth(), GetBuffers()->GetHeight(), VK_FORMAT_R16G16B16A16_SFLOAT, GetBuffers()->GetSceneSamples());
 			bool useSSAO = (gl_ssao != 0);
 			GetRenderState()->SetPassType(useSSAO ? GBUFFER_PASS : NORMAL_PASS);
 			GetRenderState()->EnableDrawBuffers(GetRenderState()->GetPassDrawBufferCount());
@@ -505,33 +505,32 @@ void VulkanFrameBuffer::RenderTextureView(FCanvasTexture *tex, AActor *Viewpoint
 
 	int width = mat->TextureWidth();
 	int height = mat->TextureHeight();
-	VulkanImage *image = BaseLayer->GetImage(tex, 0, 0);
-	VulkanImageView *view = BaseLayer->GetImageView(tex, 0, 0);
-	VulkanImageView *depthStencilView = BaseLayer->GetDepthStencilView(tex);
+	VkTextureImage *image = BaseLayer->GetImage(tex, 0, 0);
+	VkTextureImage *depthStencil = BaseLayer->GetDepthStencil(tex);
 
 	mRenderState->EndRenderPass();
 
-	PipelineBarrier barrier0;
-	barrier0.addImage(image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
-	barrier0.execute(GetDrawCommands(), VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
+	VkImageTransition barrier0;
+	barrier0.addImage(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true);
+	barrier0.execute(GetDrawCommands());
 
-	mRenderState->SetRenderTarget(view, depthStencilView, image->width, image->height, VK_FORMAT_R8G8B8A8_UNORM, VK_SAMPLE_COUNT_1_BIT);
+	mRenderState->SetRenderTarget(image->View.get(), depthStencil->View.get(), image->Image->width, image->Image->height, VK_FORMAT_R8G8B8A8_UNORM, VK_SAMPLE_COUNT_1_BIT);
 
 	IntRect bounds;
 	bounds.left = bounds.top = 0;
-	bounds.width = MIN(mat->GetWidth(), image->width);
-	bounds.height = MIN(mat->GetHeight(), image->height);
+	bounds.width = MIN(mat->GetWidth(), image->Image->width);
+	bounds.height = MIN(mat->GetHeight(), image->Image->height);
 
 	FRenderViewpoint texvp;
 	RenderViewpoint(texvp, Viewpoint, &bounds, FOV, (float)width / height, (float)width / height, false, false);
 
 	mRenderState->EndRenderPass();
 
-	PipelineBarrier barrier1;
-	barrier1.addImage(image, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
-	barrier1.execute(GetDrawCommands(), VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+	VkImageTransition barrier1;
+	barrier1.addImage(image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, false);
+	barrier1.execute(GetDrawCommands());
 
-	mRenderState->SetRenderTarget(GetBuffers()->SceneColorView.get(), GetBuffers()->SceneDepthStencilView.get(), GetBuffers()->GetWidth(), GetBuffers()->GetHeight(), VK_FORMAT_R16G16B16A16_SFLOAT, GetBuffers()->GetSceneSamples());
+	mRenderState->SetRenderTarget(GetBuffers()->SceneColor.View.get(), GetBuffers()->SceneDepthStencil.View.get(), GetBuffers()->GetWidth(), GetBuffers()->GetHeight(), VK_FORMAT_R16G16B16A16_SFLOAT, GetBuffers()->GetSceneSamples());
 
 	tex->SetUpdated(true);
 }
@@ -736,14 +735,15 @@ FTexture *VulkanFrameBuffer::WipeEndScreen()
 
 void VulkanFrameBuffer::CopyScreenToBuffer(int w, int h, void *data)
 {
+	VkTextureImage image;
+
 	// Convert from rgba16f to rgba8 using the GPU:
 	ImageBuilder imgbuilder;
 	imgbuilder.setFormat(VK_FORMAT_R8G8B8A8_UNORM);
 	imgbuilder.setUsage(VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT);
 	imgbuilder.setSize(w, h);
-	auto image = imgbuilder.create(device);
-	VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
-	GetPostprocess()->BlitCurrentToImage(image.get(), &layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
+	image.Image = imgbuilder.create(device);
+	GetPostprocess()->BlitCurrentToImage(&image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
 
 	// Staging buffer for download
 	BufferBuilder bufbuilder;
@@ -758,7 +758,7 @@ void VulkanFrameBuffer::CopyScreenToBuffer(int w, int h, void *data)
 	region.imageExtent.depth = 1;
 	region.imageSubresource.layerCount = 1;
 	region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
-	GetDrawCommands()->copyImageToBuffer(image->image, layout, staging->buffer, 1, &region);
+	GetDrawCommands()->copyImageToBuffer(image.Image->image, image.Layout, staging->buffer, 1, &region);
 
 	// Submit command buffers and wait for device to finish the work
 	WaitForCommands(false);
diff --git a/src/rendering/vulkan/textures/vk_hwtexture.cpp b/src/rendering/vulkan/textures/vk_hwtexture.cpp
index 755e3a8dc..57928d2b1 100644
--- a/src/rendering/vulkan/textures/vk_hwtexture.cpp
+++ b/src/rendering/vulkan/textures/vk_hwtexture.cpp
@@ -70,10 +70,12 @@ void VkHardwareTexture::Reset()
 		ResetDescriptors();
 
 		auto &deleteList = fb->FrameDeleteList;
-		if (mImage) deleteList.Images.push_back(std::move(mImage));
-		if (mImageView) deleteList.ImageViews.push_back(std::move(mImageView));
-		if (mDepthStencil) deleteList.Images.push_back(std::move(mDepthStencil));
-		if (mDepthStencilView) deleteList.ImageViews.push_back(std::move(mDepthStencilView));
+		if (mImage.Image) deleteList.Images.push_back(std::move(mImage.Image));
+		if (mImage.View) deleteList.ImageViews.push_back(std::move(mImage.View));
+		if (mDepthStencil.Image) deleteList.Images.push_back(std::move(mDepthStencil.Image));
+		if (mDepthStencil.View) deleteList.ImageViews.push_back(std::move(mDepthStencil.View));
+		mImage.reset();
+		mDepthStencil.reset();
 	}
 }
 
@@ -105,12 +107,12 @@ void VkHardwareTexture::ResetAllDescriptors()
 void VkHardwareTexture::Precache(FMaterial *mat, int translation, int flags)
 {
 	int numLayers = mat->GetLayers();
-	GetImageView(mat->tex, translation, flags);
+	GetImage(mat->tex, translation, flags);
 	for (int i = 1; i < numLayers; i++)
 	{
 		FTexture *layer;
 		auto systex = static_cast<VkHardwareTexture*>(mat->GetLayer(i, 0, &layer));
-		systex->GetImageView(layer, 0, mat->isExpanded() ? CTF_Expand : 0);
+		systex->GetImage(layer, 0, mat->isExpanded() ? CTF_Expand : 0);
 	}
 }
 
@@ -144,42 +146,31 @@ VulkanDescriptorSet *VkHardwareTexture::GetDescriptorSet(const FMaterialState &s
 
 	VulkanSampler *sampler = fb->GetSamplerManager()->Get(clampmode);
 
-	auto baseView = GetImageView(tex, translation, flags);
-
 	WriteDescriptors update;
-	update.addCombinedImageSampler(descriptor.get(), 0, baseView, sampler, mImageLayout);
+	update.addCombinedImageSampler(descriptor.get(), 0, GetImage(tex, translation, flags)->View.get(), sampler, mImage.Layout);
 	for (int i = 1; i < numLayers; i++)
 	{
 		FTexture *layer;
 		auto systex = static_cast<VkHardwareTexture*>(mat->GetLayer(i, 0, &layer));
-		update.addCombinedImageSampler(descriptor.get(), i, systex->GetImageView(layer, 0, mat->isExpanded() ? CTF_Expand : 0), sampler, systex->mImageLayout);
+		update.addCombinedImageSampler(descriptor.get(), i, systex->GetImage(layer, 0, mat->isExpanded() ? CTF_Expand : 0)->View.get(), sampler, systex->mImage.Layout);
 	}
 	update.updateSets(fb->device);
 	mDescriptorSets.emplace_back(clampmode, flags, std::move(descriptor));
 	return mDescriptorSets.back().descriptor.get();
 }
 
-VulkanImage *VkHardwareTexture::GetImage(FTexture *tex, int translation, int flags)
+VkTextureImage *VkHardwareTexture::GetImage(FTexture *tex, int translation, int flags)
 {
-	if (!mImage)
+	if (!mImage.Image)
 	{
 		CreateImage(tex, translation, flags);
 	}
-	return mImage.get();
+	return &mImage;
 }
 
-VulkanImageView *VkHardwareTexture::GetImageView(FTexture *tex, int translation, int flags)
+VkTextureImage *VkHardwareTexture::GetDepthStencil(FTexture *tex)
 {
-	if (!mImageView)
-	{
-		CreateImage(tex, translation, flags);
-	}
-	return mImageView.get();
-}
-
-VulkanImageView *VkHardwareTexture::GetDepthStencilView(FTexture *tex)
-{
-	if (!mDepthStencilView)
+	if (!mDepthStencil.View)
 	{
 		auto fb = GetVulkanFrameBuffer();
 
@@ -192,19 +183,20 @@ VulkanImageView *VkHardwareTexture::GetDepthStencilView(FTexture *tex)
 		builder.setSamples(VK_SAMPLE_COUNT_1_BIT);
 		builder.setFormat(format);
 		builder.setUsage(VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT);
-		mDepthStencil = builder.create(fb->device);
-		mDepthStencil->SetDebugName("VkHardwareTexture.DepthStencil");
+		mDepthStencil.Image = builder.create(fb->device);
+		mDepthStencil.Image->SetDebugName("VkHardwareTexture.DepthStencil");
+		mDepthStencil.AspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT;
 
 		ImageViewBuilder viewbuilder;
-		viewbuilder.setImage(mDepthStencil.get(), format, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
-		mDepthStencilView = viewbuilder.create(fb->device);
-		mDepthStencilView->SetDebugName("VkHardwareTexture.DepthStencilView");
+		viewbuilder.setImage(mDepthStencil.Image.get(), format, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
+		mDepthStencil.View = viewbuilder.create(fb->device);
+		mDepthStencil.View->SetDebugName("VkHardwareTexture.DepthStencilView");
 
-		PipelineBarrier barrier;
-		barrier.addImage(mDepthStencil.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 0, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT);
-		barrier.execute(fb->GetTransferCommands(), VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT);
+		VkImageTransition barrier;
+		barrier.addImage(&mDepthStencil, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, true);
+		barrier.execute(fb->GetTransferCommands());
 	}
-	return mDepthStencilView.get();
+	return &mDepthStencil;
 }
 
 void VkHardwareTexture::CreateImage(FTexture *tex, int translation, int flags)
@@ -236,19 +228,19 @@ void VkHardwareTexture::CreateImage(FTexture *tex, int translation, int flags)
 		imgbuilder.setFormat(format);
 		imgbuilder.setSize(w, h);
 		imgbuilder.setUsage(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
-		mImage = imgbuilder.create(fb->device);
-		mImage->SetDebugName("VkHardwareTexture.mImage");
+		mImage.Image = imgbuilder.create(fb->device);
+		mImage.Image->SetDebugName("VkHardwareTexture.mImage");
 
 		ImageViewBuilder viewbuilder;
-		viewbuilder.setImage(mImage.get(), format);
-		mImageView = viewbuilder.create(fb->device);
-		mImageView->SetDebugName("VkHardwareTexture.mImageView");
+		viewbuilder.setImage(mImage.Image.get(), format);
+		mImage.View = viewbuilder.create(fb->device);
+		mImage.View->SetDebugName("VkHardwareTexture.mImageView");
 
 		auto cmdbuffer = fb->GetTransferCommands();
 
-		PipelineBarrier imageTransition;
-		imageTransition.addImage(mImage.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, 0, VK_ACCESS_SHADER_READ_BIT);
-		imageTransition.execute(cmdbuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+		VkImageTransition imageTransition;
+		imageTransition.addImage(&mImage, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, true);
+		imageTransition.execute(cmdbuffer);
 	}
 }
 
@@ -272,19 +264,19 @@ void VkHardwareTexture::CreateTexture(int w, int h, int pixelsize, VkFormat form
 	imgbuilder.setFormat(format);
 	imgbuilder.setSize(w, h, GetMipLevels(w, h));
 	imgbuilder.setUsage(VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT);
-	mImage = imgbuilder.create(fb->device);
-	mImage->SetDebugName("VkHardwareTexture.mImage");
+	mImage.Image = imgbuilder.create(fb->device);
+	mImage.Image->SetDebugName("VkHardwareTexture.mImage");
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(mImage.get(), format);
-	mImageView = viewbuilder.create(fb->device);
-	mImageView->SetDebugName("VkHardwareTexture.mImageView");
+	viewbuilder.setImage(mImage.Image.get(), format);
+	mImage.View = viewbuilder.create(fb->device);
+	mImage.View->SetDebugName("VkHardwareTexture.mImageView");
 
 	auto cmdbuffer = fb->GetTransferCommands();
 
-	PipelineBarrier imageTransition0;
-	imageTransition0.addImage(mImage.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, VK_ACCESS_TRANSFER_WRITE_BIT);
-	imageTransition0.execute(cmdbuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+	VkImageTransition imageTransition;
+	imageTransition.addImage(&mImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, true);
+	imageTransition.execute(cmdbuffer);
 
 	VkBufferImageCopy region = {};
 	region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
@@ -292,54 +284,11 @@ void VkHardwareTexture::CreateTexture(int w, int h, int pixelsize, VkFormat form
 	region.imageExtent.depth = 1;
 	region.imageExtent.width = w;
 	region.imageExtent.height = h;
-	cmdbuffer->copyBufferToImage(stagingBuffer->buffer, mImage->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
+	cmdbuffer->copyBufferToImage(stagingBuffer->buffer, mImage.Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
 
 	fb->FrameDeleteList.Buffers.push_back(std::move(stagingBuffer));
 
-	GenerateMipmaps(mImage.get(), cmdbuffer);
-}
-
-void VkHardwareTexture::GenerateMipmaps(VulkanImage *image, VulkanCommandBuffer *cmdbuffer)
-{
-	int mipWidth = image->width;
-	int mipHeight = image->height;
-	int i;
-	for (i = 1; mipWidth > 1 || mipHeight > 1; i++)
-	{
-		PipelineBarrier barrier0;
-		barrier0.addImage(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i - 1);
-		barrier0.addImage(image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i);
-		barrier0.execute(cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
-
-		int nextWidth = std::max(mipWidth >> 1, 1);
-		int nextHeight = std::max(mipHeight >> 1, 1);
-
-		VkImageBlit blit = {};
-		blit.srcOffsets[0] = { 0, 0, 0 };
-		blit.srcOffsets[1] = { mipWidth, mipHeight, 1 };
-		blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
-		blit.srcSubresource.mipLevel = i - 1;
-		blit.srcSubresource.baseArrayLayer = 0;
-		blit.srcSubresource.layerCount = 1;
-		blit.dstOffsets[0] = { 0, 0, 0 };
-		blit.dstOffsets[1] = { nextWidth, nextHeight, 1 };
-		blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
-		blit.dstSubresource.mipLevel = i;
-		blit.dstSubresource.baseArrayLayer = 0;
-		blit.dstSubresource.layerCount = 1;
-		cmdbuffer->blitImage(image->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
-
-		PipelineBarrier barrier1;
-		barrier1.addImage(image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i - 1);
-		barrier1.execute(cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
-
-		mipWidth = nextWidth;
-		mipHeight = nextHeight;
-	}
-
-	PipelineBarrier barrier2;
-	barrier2.addImage(image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i - 1);
-	barrier2.execute(cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+	mImage.GenerateMipmaps(cmdbuffer);
 }
 
 int VkHardwareTexture::GetMipLevels(int w, int h)
@@ -356,12 +305,12 @@ int VkHardwareTexture::GetMipLevels(int w, int h)
 
 void VkHardwareTexture::AllocateBuffer(int w, int h, int texelsize)
 {
-	if (mImage && (mImage->width != w || mImage->height != h || mTexelsize != texelsize))
+	if (mImage.Image && (mImage.Image->width != w || mImage.Image->height != h || mTexelsize != texelsize))
 	{
 		Reset();
 	}
 
-	if (!mImage)
+	if (!mImage.Image)
 	{
 		auto fb = GetVulkanFrameBuffer();
 
@@ -376,21 +325,20 @@ void VkHardwareTexture::AllocateBuffer(int w, int h, int texelsize)
 		imgbuilder.setMemoryType(
 			VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
 			VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
-		mImage = imgbuilder.create(fb->device, &allocatedBytes);
-		mImage->SetDebugName("VkHardwareTexture.mImage");
-		mImageLayout = VK_IMAGE_LAYOUT_GENERAL;
+		mImage.Image = imgbuilder.create(fb->device, &allocatedBytes);
+		mImage.Image->SetDebugName("VkHardwareTexture.mImage");
 		mTexelsize = texelsize;
 
 		ImageViewBuilder viewbuilder;
-		viewbuilder.setImage(mImage.get(), format);
-		mImageView = viewbuilder.create(fb->device);
-		mImageView->SetDebugName("VkHardwareTexture.mImageView");
+		viewbuilder.setImage(mImage.Image.get(), format);
+		mImage.View = viewbuilder.create(fb->device);
+		mImage.View->SetDebugName("VkHardwareTexture.mImageView");
 
 		auto cmdbuffer = fb->GetTransferCommands();
 
-		PipelineBarrier imageTransition;
-		imageTransition.addImage(mImage.get(), VK_IMAGE_LAYOUT_UNDEFINED, mImageLayout, 0, VK_ACCESS_SHADER_READ_BIT);
-		imageTransition.execute(cmdbuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+		VkImageTransition imageTransition;
+		imageTransition.addImage(&mImage, VK_IMAGE_LAYOUT_GENERAL, true);
+		imageTransition.execute(cmdbuffer);
 
 		bufferpitch = int(allocatedBytes / h / texelsize);
 	}
@@ -398,12 +346,12 @@ void VkHardwareTexture::AllocateBuffer(int w, int h, int texelsize)
 
 uint8_t *VkHardwareTexture::MapBuffer()
 {
-	return (uint8_t*)mImage->Map(0, mImage->width * mImage->height * mTexelsize);
+	return (uint8_t*)mImage.Image->Map(0, mImage.Image->width * mImage.Image->height * mTexelsize);
 }
 
 unsigned int VkHardwareTexture::CreateTexture(unsigned char * buffer, int w, int h, int texunit, bool mipmap, int translation, const char *name)
 {
-	mImage->Unmap();
+	mImage.Image->Unmap();
 	return 0;
 }
 
@@ -417,15 +365,14 @@ void VkHardwareTexture::CreateWipeTexture(int w, int h, const char *name)
 	imgbuilder.setFormat(format);
 	imgbuilder.setSize(w, h);
 	imgbuilder.setUsage(VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VMA_MEMORY_USAGE_GPU_ONLY);
-	mImage = imgbuilder.create(fb->device);
-	mImage->SetDebugName(name);
-	mImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+	mImage.Image = imgbuilder.create(fb->device);
+	mImage.Image->SetDebugName(name);
 	mTexelsize = 4;
 
 	ImageViewBuilder viewbuilder;
-	viewbuilder.setImage(mImage.get(), format);
-	mImageView = viewbuilder.create(fb->device);
-	mImageView->SetDebugName(name);
+	viewbuilder.setImage(mImage.Image.get(), format);
+	mImage.View = viewbuilder.create(fb->device);
+	mImage.View->SetDebugName(name);
 
-	fb->GetPostprocess()->BlitCurrentToImage(mImage.get(), &mImageLayout);
+	fb->GetPostprocess()->BlitCurrentToImage(&mImage, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
 }
diff --git a/src/rendering/vulkan/textures/vk_hwtexture.h b/src/rendering/vulkan/textures/vk_hwtexture.h
index 2cf63b622..6d461184f 100644
--- a/src/rendering/vulkan/textures/vk_hwtexture.h
+++ b/src/rendering/vulkan/textures/vk_hwtexture.h
@@ -10,6 +10,7 @@
 #include "tarray.h"
 #include "hwrenderer/textures/hw_ihwtexture.h"
 #include "volk/volk.h"
+#include "vk_imagetransition.h"
 
 struct FMaterialState;
 class VulkanDescriptorSet;
@@ -40,9 +41,8 @@ public:
 
 	void DeleteDescriptors() override { ResetDescriptors(); }
 
-	VulkanImage *GetImage(FTexture *tex, int translation, int flags);
-	VulkanImageView *GetImageView(FTexture *tex, int translation, int flags);
-	VulkanImageView *GetDepthStencilView(FTexture *tex);
+	VkTextureImage *GetImage(FTexture *tex, int translation, int flags);
+	VkTextureImage *GetDepthStencil(FTexture *tex);
 
 	static void ResetAllDescriptors();
 
@@ -50,7 +50,6 @@ private:
 	void CreateImage(FTexture *tex, int translation, int flags);
 
 	void CreateTexture(int w, int h, int pixelsize, VkFormat format, const void *pixels);
-	void GenerateMipmaps(VulkanImage *image, VulkanCommandBuffer *cmdbuffer);
 	static int GetMipLevels(int w, int h);
 
 	void ResetDescriptors();
@@ -74,11 +73,8 @@ private:
 	};
 
 	std::vector<DescriptorEntry> mDescriptorSets;
-	std::unique_ptr<VulkanImage> mImage;
-	std::unique_ptr<VulkanImageView> mImageView;
-	VkImageLayout mImageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+	VkTextureImage mImage;
 	int mTexelsize = 4;
 
-	std::unique_ptr<VulkanImage> mDepthStencil;
-	std::unique_ptr<VulkanImageView> mDepthStencilView;
+	VkTextureImage mDepthStencil;
 };
diff --git a/src/rendering/vulkan/textures/vk_imagetransition.cpp b/src/rendering/vulkan/textures/vk_imagetransition.cpp
new file mode 100644
index 000000000..be47576d7
--- /dev/null
+++ b/src/rendering/vulkan/textures/vk_imagetransition.cpp
@@ -0,0 +1,130 @@
+
+#include "vk_imagetransition.h"
+
+void VkImageTransition::addImage(VkTextureImage *image, VkImageLayout targetLayout, bool undefinedSrcLayout)
+{
+	if (image->Layout == targetLayout)
+		return;
+
+	VkAccessFlags srcAccess = 0;
+	VkAccessFlags dstAccess = 0;
+	VkImageAspectFlags aspectMask = image->AspectMask;
+
+	switch (image->Layout)
+	{
+	case VK_IMAGE_LAYOUT_UNDEFINED:
+		srcAccess = 0;
+		srcStageMask |= VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
+		srcAccess = VK_ACCESS_TRANSFER_READ_BIT;
+		srcStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
+		srcAccess = VK_ACCESS_SHADER_READ_BIT;
+		srcStageMask |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
+		srcAccess = VK_ACCESS_TRANSFER_WRITE_BIT;
+		srcStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
+		srcAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+		srcStageMask |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
+		srcAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+		srcStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
+		break;
+	default:
+		I_FatalError("Unimplemented src image layout transition\n");
+	}
+
+	switch (targetLayout)
+	{
+	case VK_IMAGE_LAYOUT_GENERAL:
+		dstAccess = 0;
+		dstStageMask |= VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
+		dstAccess = VK_ACCESS_TRANSFER_READ_BIT;
+		dstStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
+		dstAccess = VK_ACCESS_SHADER_READ_BIT;
+		dstStageMask |= VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:
+		dstAccess = VK_ACCESS_TRANSFER_WRITE_BIT;
+		dstStageMask |= VK_PIPELINE_STAGE_TRANSFER_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:
+		dstAccess = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+		dstStageMask |= VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
+		break;
+	case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:
+		srcAccess = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
+		srcStageMask |= VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
+		break;
+	default:
+		I_FatalError("Unimplemented dst image layout transition\n");
+	}
+
+	barrier.addImage(image->Image.get(), undefinedSrcLayout ? VK_IMAGE_LAYOUT_UNDEFINED : image->Layout, targetLayout, srcAccess, dstAccess, aspectMask);
+	needbarrier = true;
+	image->Layout = targetLayout;
+}
+
+void VkImageTransition::execute(VulkanCommandBuffer *cmdbuffer)
+{
+	if (needbarrier)
+		barrier.execute(cmdbuffer, srcStageMask, dstStageMask);
+}
+
+/////////////////////////////////////////////////////////////////////////////
+
+void VkTextureImage::GenerateMipmaps(VulkanCommandBuffer *cmdbuffer)
+{
+	int mipWidth = Image->width;
+	int mipHeight = Image->height;
+	int i;
+	for (i = 1; mipWidth > 1 || mipHeight > 1; i++)
+	{
+		PipelineBarrier barrier0;
+		barrier0.addImage(Image.get(), Layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i - 1);
+		barrier0.addImage(Image.get(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i);
+		barrier0.execute(cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
+		Layout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
+
+		int nextWidth = std::max(mipWidth >> 1, 1);
+		int nextHeight = std::max(mipHeight >> 1, 1);
+
+		VkImageBlit blit = {};
+		blit.srcOffsets[0] = { 0, 0, 0 };
+		blit.srcOffsets[1] = { mipWidth, mipHeight, 1 };
+		blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+		blit.srcSubresource.mipLevel = i - 1;
+		blit.srcSubresource.baseArrayLayer = 0;
+		blit.srcSubresource.layerCount = 1;
+		blit.dstOffsets[0] = { 0, 0, 0 };
+		blit.dstOffsets[1] = { nextWidth, nextHeight, 1 };
+		blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+		blit.dstSubresource.mipLevel = i;
+		blit.dstSubresource.baseArrayLayer = 0;
+		blit.dstSubresource.layerCount = 1;
+		cmdbuffer->blitImage(Image->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, Image->image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &blit, VK_FILTER_LINEAR);
+
+		PipelineBarrier barrier1;
+		barrier1.addImage(Image.get(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i - 1);
+		barrier1.execute(cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+
+		mipWidth = nextWidth;
+		mipHeight = nextHeight;
+	}
+
+	PipelineBarrier barrier2;
+	barrier2.addImage(Image.get(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_ASPECT_COLOR_BIT, i - 1);
+	barrier2.execute(cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
+
+	Layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+}
diff --git a/src/rendering/vulkan/textures/vk_imagetransition.h b/src/rendering/vulkan/textures/vk_imagetransition.h
new file mode 100644
index 000000000..c5b32dd50
--- /dev/null
+++ b/src/rendering/vulkan/textures/vk_imagetransition.h
@@ -0,0 +1,39 @@
+
+#pragma once
+
+#include "vulkan/system/vk_objects.h"
+#include "vulkan/system/vk_builders.h"
+
+class VkTextureImage
+{
+public:
+	void reset()
+	{
+		AspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+		Layout = VK_IMAGE_LAYOUT_UNDEFINED;
+		DepthOnlyView.reset();
+		View.reset();
+		Image.reset();
+	}
+
+	void GenerateMipmaps(VulkanCommandBuffer *cmdbuffer);
+
+	std::unique_ptr<VulkanImage> Image;
+	std::unique_ptr<VulkanImageView> View;
+	std::unique_ptr<VulkanImageView> DepthOnlyView;
+	VkImageLayout Layout = VK_IMAGE_LAYOUT_UNDEFINED;
+	VkImageAspectFlags AspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
+};
+
+class VkImageTransition
+{
+public:
+	void addImage(VkTextureImage *image, VkImageLayout targetLayout, bool undefinedSrcLayout);
+	void execute(VulkanCommandBuffer *cmdbuffer);
+
+private:
+	PipelineBarrier barrier;
+	VkPipelineStageFlags srcStageMask = 0;
+	VkPipelineStageFlags dstStageMask = 0;
+	bool needbarrier = false;
+};