/* Copyright (C) 2025 Wildfire Games.
 * This file is part of 0 A.D.
 *
 * 0 A.D. is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * 0 A.D. is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "precompiled.h"

#include "DeviceCommandContext.h"

#include "graphics/Color.h"
#include "lib/bits.h"
#include "lib/debug.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/containers/StaticVector.h"
#include "renderer/backend/Format.h"
#include "renderer/backend/IBuffer.h"
#include "renderer/backend/IFramebuffer.h"
#include "renderer/backend/IShaderProgram.h"
#include "renderer/backend/ITexture.h"
#include "renderer/backend/PipelineState.h"
#include "renderer/backend/Sampler.h"
#include "renderer/backend/vulkan/Buffer.h"
#include "renderer/backend/vulkan/DescriptorManager.h"
#include "renderer/backend/vulkan/Device.h"
#include "renderer/backend/vulkan/DeviceSelection.h"
#include "renderer/backend/vulkan/Framebuffer.h"
#include "renderer/backend/vulkan/Mapping.h"
#include "renderer/backend/vulkan/PipelineState.h"
#include "renderer/backend/vulkan/RingCommandContext.h"
#include "renderer/backend/vulkan/ShaderProgram.h"
#include "renderer/backend/vulkan/Texture.h"
#include "renderer/backend/vulkan/Utilities.h"

#include <algorithm>
#include <cstddef>
#include <cstring>
#include <iterator>
#include <tuple>
#include <utility>

namespace Renderer
{

namespace Backend
{

namespace Vulkan
{

namespace
{

constexpr uint32_t UNIFORM_BUFFER_INITIAL_SIZE = 1024 * 1024 * 32;
constexpr uint32_t FRAME_INPLACE_BUFFER_INITIAL_SIZE = 128 * 1024;

struct SBaseImageState
{
	VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
	VkAccessFlags accessMask = 0;
	VkPipelineStageFlags stageMask = 0;
};

SBaseImageState GetBaseImageState(CTexture* texture)
{
	if (texture->GetUsage() & ITexture::Usage::SAMPLED)
	{
		return {
			VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
			VK_ACCESS_SHADER_READ_BIT,
			VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT};
	}
	else if (texture->GetUsage() & ITexture::Usage::COLOR_ATTACHMENT)
	{
		return {
			VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
			VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
			VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
	}
	else if (texture->GetUsage() & ITexture::Usage::DEPTH_STENCIL_ATTACHMENT)
	{
		return {
			VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
			VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
			VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT};
	}
	// The only TRANSFER_DST usage means we can do only readbacks.
	else if (texture->GetUsage() == ITexture::Usage::TRANSFER_DST)
	{
		return {
			VK_IMAGE_LAYOUT_GENERAL,
			VK_ACCESS_HOST_READ_BIT,
			VK_PIPELINE_STAGE_HOST_BIT};
	}
	return {};
}

class ScopedImageLayoutTransition
{
public:
	ScopedImageLayoutTransition(
		CRingCommandContext& commandContext, const std::span<CTexture* const> textures,
		const VkImageLayout layout, const VkAccessFlags accessMask, const VkPipelineStageFlags stageMask)
		: m_CommandContext(commandContext), m_Textures(textures), m_Layout(layout),
		m_AccessMask(accessMask), m_StageMask(stageMask)
	{
		for (CTexture* const texture : m_Textures)
		{
			const auto state = GetBaseImageState(texture);

			VkImageLayout oldLayout = state.layout;
			if (!texture->IsInitialized())
				oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;

			Utilities::SetTextureLayout(
				m_CommandContext.GetCommandBuffer(), texture,
				oldLayout, m_Layout,
				state.accessMask, m_AccessMask, state.stageMask, m_StageMask);
		}
	}

	~ScopedImageLayoutTransition()
	{
		for (CTexture* const texture : m_Textures)
		{
			const auto state = GetBaseImageState(texture);

			Utilities::SetTextureLayout(
				m_CommandContext.GetCommandBuffer(), texture,
				m_Layout, state.layout,
				m_AccessMask, state.accessMask, m_StageMask, state.stageMask);
		}
	}

private:
	CRingCommandContext& m_CommandContext;
	const std::span<CTexture* const> m_Textures;
	const VkImageLayout m_Layout = VK_IMAGE_LAYOUT_UNDEFINED;
	const VkAccessFlags m_AccessMask = 0;
	const VkPipelineStageFlags m_StageMask = 0;
};

template<typename TransferOp>
void TransferForEachFramebufferAttachmentPair(
	CRingCommandContext& commandContext,
	CFramebuffer* sourceFramebuffer, CFramebuffer* destinationFramebuffer,
	TransferOp transferOp)
{
	const auto& sourceColorAttachments =
		sourceFramebuffer->GetColorAttachments();
	const auto& destinationColorAttachments =
		destinationFramebuffer->GetColorAttachments();
	ENSURE(sourceColorAttachments.size() == destinationColorAttachments.size());

	for (CTexture* sourceColorAttachment : sourceColorAttachments)
		ENSURE(sourceColorAttachment->GetUsage() & ITexture::Usage::TRANSFER_SRC);
	for (CTexture* destinationColorAttachment : destinationColorAttachments)
		ENSURE(destinationColorAttachment->GetUsage() & ITexture::Usage::TRANSFER_DST);

	// TODO: combine barriers, reduce duplication, add depth.
	ScopedImageLayoutTransition scopedColorAttachmentsTransition{
		commandContext,
		{sourceColorAttachments.begin(), sourceColorAttachments.end()},
		VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
		VK_ACCESS_TRANSFER_READ_BIT,
		VK_PIPELINE_STAGE_TRANSFER_BIT};
	ScopedImageLayoutTransition destinationColorAttachmentsTransition{
		commandContext,
		{destinationColorAttachments.begin(), destinationColorAttachments.end()},
		VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
		VK_ACCESS_TRANSFER_WRITE_BIT,
		VK_PIPELINE_STAGE_TRANSFER_BIT};

	for (CFramebuffer::ColorAttachments::size_type index = 0; index < destinationColorAttachments.size(); ++index)
	{
		CTexture* sourceColorAttachment = sourceColorAttachments[index];
		CTexture* destinationColorAttachment = destinationColorAttachments[index];

		transferOp(commandContext.GetCommandBuffer(), sourceColorAttachment, destinationColorAttachment);
	}

	if (sourceFramebuffer->GetDepthStencilAttachment() && destinationFramebuffer->GetDepthStencilAttachment())
	{
		transferOp(
			commandContext.GetCommandBuffer(),
			sourceFramebuffer->GetDepthStencilAttachment(),
			destinationFramebuffer->GetDepthStencilAttachment());
	}
}

} // anonymous namespace

// A helper class to store consequent uploads to avoid many copy functions.
// We use a buffer in the device memory and NUMBER_OF_FRAMES_IN_FLIGHT
// times bigger buffer in the host memory.
class CDeviceCommandContext::CUploadRing
{
public:
	CUploadRing(
		CDevice* device, const IBuffer::Type type, const uint32_t initialCapacity);

	CBuffer* GetBuffer() { return m_Buffer.get(); }

	uint32_t ScheduleUpload(
		VkCommandBuffer commandBuffer, const std::span<const std::byte> data,
		const uint32_t alignment);

	void ExecuteUploads(VkCommandBuffer commandBuffer);

private:
	void ResizeIfNeeded(
		VkCommandBuffer commandBuffer, const uint32_t dataSize);

	CDevice* m_Device = nullptr;
	IBuffer::Type m_Type = IBuffer::Type::VERTEX;
	uint32_t m_Capacity = 0;
	uint32_t m_BlockIndex = 0, m_BlockOffset = 0;
	std::unique_ptr<CBuffer> m_Buffer, m_StagingBuffer;
	std::byte* m_StagingBufferMappedData = nullptr;
};

CDeviceCommandContext::CUploadRing::CUploadRing(
	CDevice* device, const IBuffer::Type type, const uint32_t initialCapacity)
	: m_Device(device), m_Type(type)
{
	ResizeIfNeeded(VK_NULL_HANDLE, initialCapacity);
}

void CDeviceCommandContext::CUploadRing::ResizeIfNeeded(
	VkCommandBuffer commandBuffer, const uint32_t dataSize)
{
	// We need to pad the data size for uniforms because we use dynamic offsets
	// with a fixed range.
	const uint32_t paddedDataSize{
		m_Type == IBuffer::Type::UNIFORM ? round_up_to_pow2(dataSize) : dataSize};
	const bool resizeNeeded = !m_Buffer || m_BlockOffset + paddedDataSize > m_Capacity;
	if (!resizeNeeded)
		return;

	if (m_Buffer && m_BlockOffset > 0)
	{
		ENSURE(commandBuffer != VK_NULL_HANDLE);
		ExecuteUploads(commandBuffer);
	}

	m_Capacity = std::max(m_Capacity * 2, round_up_to_pow2(dataSize));

	m_Buffer = m_Device->CreateCBuffer(
		"UploadRingBuffer", m_Type, m_Capacity, IBuffer::Usage::DYNAMIC | IBuffer::Usage::TRANSFER_DST);
	m_StagingBuffer = m_Device->CreateCBuffer(
		"UploadRingStagingBuffer", IBuffer::Type::UPLOAD, NUMBER_OF_FRAMES_IN_FLIGHT * m_Capacity, IBuffer::Usage::DYNAMIC | IBuffer::Usage::TRANSFER_SRC);
	ENSURE(m_Buffer && m_StagingBuffer);
	m_StagingBufferMappedData = static_cast<std::byte*>(m_StagingBuffer->GetMappedData());
	ENSURE(m_StagingBufferMappedData);

	m_BlockIndex = 0;
	m_BlockOffset = 0;
}

uint32_t CDeviceCommandContext::CUploadRing::ScheduleUpload(
	VkCommandBuffer commandBuffer, const std::span<const std::byte> data,
	const uint32_t alignment)
{
	ENSURE(data.size() > 0);
	ENSURE(is_pow2(alignment));

	m_BlockOffset = (m_BlockOffset + alignment - 1) & ~(alignment - 1);

	ResizeIfNeeded(commandBuffer, data.size());

	const uint32_t destination = m_BlockIndex * m_Capacity + m_BlockOffset;
	const uint32_t offset = m_BlockOffset;
	m_BlockOffset += data.size();
	std::memcpy(m_StagingBufferMappedData + destination, data.data(), data.size());
	return offset;
}

void CDeviceCommandContext::CUploadRing::ExecuteUploads(
	VkCommandBuffer commandBuffer)
{
	if (m_BlockOffset == 0)
		return;

	const VkPipelineStageFlags stageMask =
		m_Type == IBuffer::Type::UNIFORM
			? VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
			: VK_PIPELINE_STAGE_VERTEX_INPUT_BIT;

	Utilities::SubmitBufferMemoryBarrier(
		commandBuffer, m_Buffer.get(), 0, VK_WHOLE_SIZE,
		VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
		stageMask, VK_PIPELINE_STAGE_TRANSFER_BIT);

	VkBufferCopy region{};
	region.srcOffset = m_BlockIndex * m_Capacity;
	region.dstOffset = 0;
	region.size = m_BlockOffset;

	vkCmdCopyBuffer(
		commandBuffer,
		m_StagingBuffer->GetVkBuffer(), m_Buffer->GetVkBuffer(),
		1, &region);

	Utilities::SubmitBufferMemoryBarrier(
		commandBuffer, m_Buffer.get(), 0, VK_WHOLE_SIZE,
		VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT,
		VK_PIPELINE_STAGE_TRANSFER_BIT, stageMask);

	m_BlockIndex = (m_BlockIndex + 1) % NUMBER_OF_FRAMES_IN_FLIGHT;
	m_BlockOffset = 0;
}

// static
std::unique_ptr<IDeviceCommandContext> CDeviceCommandContext::Create(CDevice* device)
{
	std::unique_ptr<CDeviceCommandContext> deviceCommandContext(new CDeviceCommandContext());
	deviceCommandContext->m_Device = device;
	deviceCommandContext->m_DebugScopedLabels = device->GetCapabilities().debugScopedLabels;
	deviceCommandContext->m_PrependCommandContext =
		device->CreateRingCommandContext(NUMBER_OF_FRAMES_IN_FLIGHT);
	ENSURE(deviceCommandContext->m_PrependCommandContext);
	deviceCommandContext->m_CommandContext =
		device->CreateRingCommandContext(NUMBER_OF_FRAMES_IN_FLIGHT);
	ENSURE(deviceCommandContext->m_CommandContext);

	deviceCommandContext->m_VertexUploadRing = std::make_unique<CUploadRing>(
		device, IBuffer::Type::VERTEX, FRAME_INPLACE_BUFFER_INITIAL_SIZE);
	deviceCommandContext->m_IndexUploadRing = std::make_unique<CUploadRing>(
		device, IBuffer::Type::INDEX, FRAME_INPLACE_BUFFER_INITIAL_SIZE);
	deviceCommandContext->m_UniformUploadRing = std::make_unique<CUploadRing>(
		device, IBuffer::Type::UNIFORM, UNIFORM_BUFFER_INITIAL_SIZE);

	deviceCommandContext->m_DebugBarrierAfterFramebufferPass = g_ConfigDB.Get(
		"renderer.backend.vulkan.debugbarrierafterframebufferpass",
		deviceCommandContext->m_DebugBarrierAfterFramebufferPass);

	return deviceCommandContext;
}

CDeviceCommandContext::CDeviceCommandContext() = default;

CDeviceCommandContext::~CDeviceCommandContext()
{
	VkDevice device = m_Device->GetVkDevice();

	vkDeviceWaitIdle(device);

	if (m_UniformDescriptorPool != VK_NULL_HANDLE)
		vkDestroyDescriptorPool(device, m_UniformDescriptorPool, nullptr);
}

IDevice* CDeviceCommandContext::GetDevice()
{
	return m_Device;
}

void CDeviceCommandContext::SetGraphicsPipelineState(
	IGraphicsPipelineState* pipelineState)
{
	ENSURE(pipelineState);
	if (m_GraphicsPipelineState && m_GraphicsPipelineState->GetUID() == pipelineState->As<CGraphicsPipelineState>()->GetUID())
		return;
	m_GraphicsPipelineState = pipelineState->As<CGraphicsPipelineState>();

	CShaderProgram* shaderProgram = m_GraphicsPipelineState->GetShaderProgram()->As<CShaderProgram>();
	if (m_ShaderProgram != shaderProgram)
	{
		if (m_ShaderProgram)
			m_ShaderProgram->Unbind();
		m_ShaderProgram = shaderProgram;
	}
	m_IsPipelineStateDirty = true;
}

void CDeviceCommandContext::SetComputePipelineState(
	IComputePipelineState* pipelineState)
{
	ENSURE(pipelineState);
	if (m_ComputePipelineState && m_ComputePipelineState->GetUID() == pipelineState->As<CComputePipelineState>()->GetUID())
		return;
	m_ComputePipelineState = pipelineState->As<CComputePipelineState>();

	CShaderProgram* shaderProgram = m_ComputePipelineState->GetShaderProgram()->As<CShaderProgram>();
	if (m_ShaderProgram != shaderProgram)
	{
		if (m_ShaderProgram)
			m_ShaderProgram->Unbind();
		m_ShaderProgram = shaderProgram;
	}
	m_IsPipelineStateDirty = true;
}

void CDeviceCommandContext::BlitFramebuffer(
	IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer,
	const Rect& sourceRegion, const Rect& destinationRegion,
	const Sampler::Filter filter)
{
	ENSURE(!m_InsideFramebufferPass);
	ENSURE(sourceRegion.x >= 0 && sourceRegion.x + sourceRegion.width <= static_cast<int32_t>(sourceFramebuffer->GetWidth()));
	ENSURE(sourceRegion.y >= 0 && sourceRegion.y + sourceRegion.height <= static_cast<int32_t>(sourceFramebuffer->GetHeight()));
	ENSURE(destinationRegion.x >= 0 && destinationRegion.x + destinationRegion.width <= static_cast<int32_t>(destinationFramebuffer->GetWidth()));
	ENSURE(destinationRegion.y >= 0 && destinationRegion.y + destinationRegion.height <= static_cast<int32_t>(destinationFramebuffer->GetHeight()));

	TransferForEachFramebufferAttachmentPair(
		*m_CommandContext,
		sourceFramebuffer->As<CFramebuffer>(), destinationFramebuffer->As<CFramebuffer>(),
		[&sourceRegion, &destinationRegion, filter](
			VkCommandBuffer commandBuffer, CTexture* sourceColorAttachment, CTexture* destinationColorAttachment)
		{
			// TODO: we need to check for VK_FORMAT_FEATURE_BLIT_*_BIT for used formats.

			const bool isDepth = IsDepthFormat(sourceColorAttachment->GetFormat());
			const VkImageAspectFlags aspectMask = isDepth ? VK_IMAGE_ASPECT_DEPTH_BIT : VK_IMAGE_ASPECT_COLOR_BIT;

			VkImageBlit region{};
			// Currently (0, 0) is the left-bottom corner (legacy from GL), so
			// we need to adjust the regions.
			const uint32_t sourceHeight = sourceColorAttachment->GetHeight();
			const uint32_t destinationHeight = destinationColorAttachment->GetHeight();
			region.srcOffsets[0].x = sourceRegion.x;
			region.srcOffsets[0].y = sourceHeight - sourceRegion.y - sourceRegion.height;
			region.dstOffsets[0].x = destinationRegion.x;
			region.dstOffsets[0].y = destinationHeight - destinationRegion.y - destinationRegion.height;
			region.srcOffsets[1].x = sourceRegion.x + sourceRegion.width;
			region.srcOffsets[1].y = sourceHeight - sourceRegion.y;
			region.srcOffsets[1].z = 1;
			region.dstOffsets[1].x = destinationRegion.x + destinationRegion.width;
			region.dstOffsets[1].y = destinationHeight - destinationRegion.y;
			region.dstOffsets[1].z = 1;
			region.srcSubresource.aspectMask = aspectMask;
			region.srcSubresource.mipLevel = 0;
			region.srcSubresource.baseArrayLayer = 0;
			region.srcSubresource.layerCount = 1;
			region.dstSubresource.aspectMask = aspectMask;
			region.dstSubresource.mipLevel = 0;
			region.dstSubresource.baseArrayLayer = 0;
			region.dstSubresource.layerCount = 1;

			vkCmdBlitImage(
				commandBuffer,
				sourceColorAttachment->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
				destinationColorAttachment->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
				1, &region, filter == Sampler::Filter::LINEAR ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
		});
}

void CDeviceCommandContext::ResolveFramebuffer(
	IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer)
{
	ENSURE(!m_InsideFramebufferPass);
	ENSURE(sourceFramebuffer->As<CFramebuffer>()->GetSampleCount() > 1);
	ENSURE(destinationFramebuffer->As<CFramebuffer>()->GetSampleCount() == 1);
	ENSURE(sourceFramebuffer->As<CFramebuffer>()->GetWidth() == destinationFramebuffer->As<CFramebuffer>()->GetWidth());
	ENSURE(sourceFramebuffer->As<CFramebuffer>()->GetHeight() == destinationFramebuffer->As<CFramebuffer>()->GetHeight());

	TransferForEachFramebufferAttachmentPair(
		*m_CommandContext,
		sourceFramebuffer->As<CFramebuffer>(), destinationFramebuffer->As<CFramebuffer>(),
		[](VkCommandBuffer commandBuffer, CTexture* sourceColorAttachment, CTexture* destinationColorAttachment)
		{
			ENSURE(sourceColorAttachment->GetFormat() == destinationColorAttachment->GetFormat());
			ENSURE(!IsDepthFormat(sourceColorAttachment->GetFormat()));
			const VkImageAspectFlags aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;

			VkImageResolve region{};
			region.extent.width = sourceColorAttachment->GetWidth();
			region.extent.height = sourceColorAttachment->GetHeight();
			region.extent.depth = 1;
			region.srcSubresource.aspectMask = aspectMask;
			region.srcSubresource.mipLevel = 0;
			region.srcSubresource.baseArrayLayer = 0;
			region.srcSubresource.layerCount = 1;
			region.dstSubresource.aspectMask = aspectMask;
			region.dstSubresource.mipLevel = 0;
			region.dstSubresource.baseArrayLayer = 0;
			region.dstSubresource.layerCount = 1;

			vkCmdResolveImage(
				commandBuffer,
				sourceColorAttachment->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
				destinationColorAttachment->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
				1, &region);
		});
}

void CDeviceCommandContext::ClearFramebuffer(const bool color, const bool depth, const bool stencil)
{
	ENSURE(m_InsideFramebufferPass);
	ENSURE(m_Framebuffer);
	PS::StaticVector<VkClearAttachment, 4> clearAttachments;
	if (color)
	{
		ENSURE(!m_Framebuffer->GetColorAttachments().empty());
		for (size_t index = 0; index < m_Framebuffer->GetColorAttachments().size(); ++index)
		{
			VkClearAttachment clearAttachment{};
			clearAttachment.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
			const CColor& clearColor = m_Framebuffer->GetClearColor();
			clearAttachment.clearValue.color.float32[0] = clearColor.r;
			clearAttachment.clearValue.color.float32[1] = clearColor.g;
			clearAttachment.clearValue.color.float32[2] = clearColor.b;
			clearAttachment.clearValue.color.float32[3] = clearColor.a;
			clearAttachment.colorAttachment = index;
			clearAttachments.emplace_back(std::move(clearAttachment));
		}
	}
	if (depth || stencil)
	{
		ENSURE(m_Framebuffer->GetDepthStencilAttachment());
		if (stencil)
		{
			const Format depthStencilFormat =
				m_Framebuffer->GetDepthStencilAttachment()->GetFormat();
			ENSURE(depthStencilFormat == Format::D24_UNORM_S8_UINT ||
				depthStencilFormat == Format::D32_SFLOAT_S8_UINT);
		}
		VkClearAttachment clearAttachment{};
		if (depth)
			clearAttachment.aspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT;
		if (stencil)
			clearAttachment.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
		clearAttachment.clearValue.depthStencil.depth = 1.0f;
		clearAttachment.clearValue.depthStencil.stencil = 0;
		clearAttachments.emplace_back(std::move(clearAttachment));
	}
	VkClearRect clearRect{};
	clearRect.layerCount = 1;
	clearRect.rect.offset.x = 0;
	clearRect.rect.offset.y = 0;
	clearRect.rect.extent.width = m_Framebuffer->GetWidth();
	clearRect.rect.extent.height = m_Framebuffer->GetHeight();
	vkCmdClearAttachments(
		m_CommandContext->GetCommandBuffer(),
		clearAttachments.size(), clearAttachments.data(),
		1, &clearRect);
}

void CDeviceCommandContext::BeginFramebufferPass(IFramebuffer* framebuffer)
{
	ENSURE(!m_InsideFramebufferPass);
	ENSURE(!m_InsideComputePass);
	ENSURE(framebuffer);
	m_IsPipelineStateDirty = true;
	m_Framebuffer = framebuffer->As<CFramebuffer>();
	m_GraphicsPipelineState = nullptr;
	m_VertexInputLayout = nullptr;

	SetScissors(0, nullptr);

	for (CTexture* colorAttachment : m_Framebuffer->GetColorAttachments())
	{
		if (!(colorAttachment->GetUsage() & ITexture::Usage::SAMPLED) && colorAttachment->IsInitialized())
			continue;
		VkImageLayout oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
		if (!colorAttachment->IsInitialized())
			oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
		Utilities::SetTextureLayout(
			m_CommandContext->GetCommandBuffer(), colorAttachment,
			oldLayout,
			VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
			VK_ACCESS_SHADER_READ_BIT,
			VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
			VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT);
	}

	CTexture* depthStencilAttachment = m_Framebuffer->GetDepthStencilAttachment();
	if (depthStencilAttachment && ((depthStencilAttachment->GetUsage() & ITexture::Usage::SAMPLED) || !depthStencilAttachment->IsInitialized()))
	{
		VkImageLayout oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
		if (!depthStencilAttachment->IsInitialized())
			oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
		Utilities::SetTextureLayout(
			m_CommandContext->GetCommandBuffer(), depthStencilAttachment, oldLayout,
			VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
			VK_ACCESS_SHADER_READ_BIT,
			VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
			VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
			VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT);
	}

	m_InsideFramebufferPass = true;

	VkRenderPassBeginInfo renderPassBeginInfo{};
	renderPassBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
	renderPassBeginInfo.renderPass = m_Framebuffer->GetRenderPass();
	renderPassBeginInfo.framebuffer = m_Framebuffer->GetFramebuffer();
	renderPassBeginInfo.renderArea.offset = { 0, 0 };
	renderPassBeginInfo.renderArea.extent = { m_Framebuffer->GetWidth(), m_Framebuffer->GetHeight() };

	PS::StaticVector<VkClearValue, 4> clearValues;
	const bool needsClearValues =
		m_Framebuffer->GetColorAttachmentLoadOp() == AttachmentLoadOp::CLEAR ||
		(m_Framebuffer->GetDepthStencilAttachment() &&
			m_Framebuffer->GetDepthStencilAttachmentLoadOp() == AttachmentLoadOp::CLEAR);
	if (needsClearValues)
	{
		const CFramebuffer::ColorAttachments& colorAttachments{m_Framebuffer->GetColorAttachments()};
		std::for_each(colorAttachments.begin(), colorAttachments.end(), [&](CTexture*)
		{
			const CColor& clearColor = m_Framebuffer->GetClearColor();
			// The four array elements of the clear color map to R, G, B, and A
			// components of image formats, in order.
			clearValues.emplace_back();
			clearValues.back().color.float32[0] = clearColor.r;
			clearValues.back().color.float32[1] = clearColor.g;
			clearValues.back().color.float32[2] = clearColor.b;
			clearValues.back().color.float32[3] = clearColor.a;
		});
		if (m_Framebuffer->GetDepthStencilAttachment())
		{
			clearValues.emplace_back();
			clearValues.back().depthStencil.depth = 1.0f;
			clearValues.back().depthStencil.stencil = 0;
		}
		renderPassBeginInfo.clearValueCount = clearValues.size();
		renderPassBeginInfo.pClearValues = clearValues.data();
	}

	vkCmdBeginRenderPass(m_CommandContext->GetCommandBuffer(), &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
}

void CDeviceCommandContext::EndFramebufferPass()
{
	ENSURE(m_InsideFramebufferPass);
	vkCmdEndRenderPass(m_CommandContext->GetCommandBuffer());

	m_InsideFramebufferPass = false;
	m_BoundIndexBuffer = nullptr;

	ENSURE(m_Framebuffer);
	for (CTexture* colorAttachment : m_Framebuffer->GetColorAttachments())
	{
		if (!(colorAttachment->GetUsage() & ITexture::Usage::SAMPLED))
			continue;
		Utilities::SetTextureLayout(
			m_CommandContext->GetCommandBuffer(), colorAttachment,
			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,
			VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
	}

	CTexture* depthStencilAttachment = m_Framebuffer->GetDepthStencilAttachment();
	if (depthStencilAttachment && (depthStencilAttachment->GetUsage() & ITexture::Usage::SAMPLED))
	{
		Utilities::SetTextureLayout(
			m_CommandContext->GetCommandBuffer(), depthStencilAttachment,
			VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
			VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
			VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
			VK_ACCESS_SHADER_READ_BIT,
			VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
			VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
	}

	m_LastBoundPipeline = VK_NULL_HANDLE;
	if (m_ShaderProgram)
		m_ShaderProgram->Unbind();
	m_ShaderProgram = nullptr;
	m_GraphicsPipelineState = nullptr;

	if (m_DebugBarrierAfterFramebufferPass)
		Utilities::SubmitDebugSyncMemoryBarrier(m_CommandContext->GetCommandBuffer());
}

void CDeviceCommandContext::ReadbackFramebufferSync(
	const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height,
	void* data)
{
	CTexture* texture = m_Device->GetCurrentBackbufferTexture();
	if (!texture)
	{
		LOGERROR("Vulkan: backbuffer is unavailable.");
		return;
	}
	if (!(texture->GetUsage() & ITexture::Usage::TRANSFER_SRC))
	{
		LOGERROR("Vulkan: backbuffer doesn't support readback.");
		return;
	}
	m_QueuedReadbacks.emplace_back(x, y, width, height, data);
}

void CDeviceCommandContext::UploadTexture(ITexture* texture, const Format dataFormat,
	const void* data, const size_t dataSize,
	const uint32_t level, const uint32_t layer)
{
	(m_InsideFramebufferPass ? m_PrependCommandContext : m_CommandContext)->ScheduleUpload(
		texture->As<CTexture>(), dataFormat, data, dataSize, level, layer);
}

void CDeviceCommandContext::UploadTextureRegion(ITexture* texture, const Format dataFormat,
	const void* data, const size_t dataSize,
	const uint32_t xOffset, const uint32_t yOffset,
	const uint32_t width, const uint32_t height,
	const uint32_t level, const uint32_t layer)
{
	(m_InsideFramebufferPass ? m_PrependCommandContext : m_CommandContext)->ScheduleUpload(
		texture->As<CTexture>(), dataFormat, data, dataSize, xOffset, yOffset, width, height, level, layer);
}

void CDeviceCommandContext::UploadBuffer(IBuffer* buffer, const void* data, const uint32_t dataSize)
{
	ENSURE(!m_InsideFramebufferPass);
	m_CommandContext->ScheduleUpload(
		buffer->As<CBuffer>(), data, 0, dataSize);
}

void CDeviceCommandContext::UploadBuffer(IBuffer* buffer, const UploadBufferFunction& uploadFunction)
{
	ENSURE(!m_InsideFramebufferPass);
	m_CommandContext->ScheduleUpload(
		buffer->As<CBuffer>(), 0, buffer->As<CBuffer>()->GetSize(), uploadFunction);
}

void CDeviceCommandContext::UploadBufferRegion(
	IBuffer* buffer, const void* data, const uint32_t dataOffset, const uint32_t dataSize)
{
	ENSURE(!m_InsideFramebufferPass);
	m_CommandContext->ScheduleUpload(
		buffer->As<CBuffer>(), data, dataOffset, dataSize);
}

void CDeviceCommandContext::UploadBufferRegion(
	IBuffer* buffer, const uint32_t dataOffset, const uint32_t dataSize,
	const UploadBufferFunction& uploadFunction)
{
	m_CommandContext->ScheduleUpload(
		buffer->As<CBuffer>(), dataOffset, dataSize, uploadFunction);
}

void CDeviceCommandContext::SetScissors(const uint32_t scissorCount, const Rect* scissors)
{
	ENSURE(m_Framebuffer);
	ENSURE(scissorCount <= 1);
	VkRect2D scissor{};
	if (scissorCount == 1)
	{
		// the x and y members of offset member of any element of pScissors must be
		// greater than or equal to 0.
		int32_t x = scissors[0].x;
		int32_t y = m_Framebuffer->GetHeight() - scissors[0].y - scissors[0].height;
		int32_t width = scissors[0].width;
		int32_t height = scissors[0].height;
		if (x < 0)
		{
			width = std::max(0, width + x);
			x = 0;
		}
		if (y < 0)
		{
			height = std::max(0, height + y);
			y = 0;
		}
		scissor.offset.x = x;
		scissor.offset.y = y;
		scissor.extent.width = width;
		scissor.extent.height = height;
	}
	else
	{
		scissor.extent.width = m_Framebuffer->GetWidth();
		scissor.extent.height = m_Framebuffer->GetHeight();
	}
	vkCmdSetScissor(m_CommandContext->GetCommandBuffer(), 0, 1, &scissor);
}

void CDeviceCommandContext::SetViewports(const uint32_t viewportCount, const Rect* viewports)
{
	ENSURE(m_Framebuffer);
	ENSURE(viewportCount == 1);

	VkViewport viewport{};
	viewport.minDepth = 0.0f;
	viewport.maxDepth = 1.0f;
	viewport.x = static_cast<float>(viewports[0].x);
	viewport.y = static_cast<float>(static_cast<int32_t>(m_Framebuffer->GetHeight()) - viewports[0].y - viewports[0].height);
	viewport.width = static_cast<float>(viewports[0].width);
	viewport.height = static_cast<float>(viewports[0].height);

	vkCmdSetViewport(m_CommandContext->GetCommandBuffer(), 0, 1, &viewport);
}

void CDeviceCommandContext::SetVertexInputLayout(
	IVertexInputLayout* vertexInputLayout)
{
	ENSURE(vertexInputLayout);
	if (m_VertexInputLayout != vertexInputLayout->As<CVertexInputLayout>())
	{
		m_IsPipelineStateDirty = true;
		m_VertexInputLayout = vertexInputLayout->As<CVertexInputLayout>();
	}
}

void CDeviceCommandContext::SetVertexBuffer(
	const uint32_t bindingSlot, IBuffer* buffer, const uint32_t offset)
{
	BindVertexBuffer(bindingSlot, buffer->As<CBuffer>(), offset);
}

void CDeviceCommandContext::SetVertexBufferData(
	const uint32_t bindingSlot, const void* data, const uint32_t dataSize)
{
	// TODO: check vertex buffer alignment.
	const uint32_t ALIGNMENT = 32;

	const uint32_t offset = m_VertexUploadRing->ScheduleUpload(
		m_PrependCommandContext->GetCommandBuffer(),
		std::span<const std::byte>{static_cast<const std::byte*>(data), dataSize}, ALIGNMENT);
	BindVertexBuffer(bindingSlot, m_VertexUploadRing->GetBuffer(), offset);
}

void CDeviceCommandContext::SetIndexBuffer(IBuffer* buffer)
{
	BindIndexBuffer(buffer->As<CBuffer>(), 0);
}

void CDeviceCommandContext::SetIndexBufferData(
	const void* data, const uint32_t dataSize)
{
	// TODO: check index buffer alignment.
	const uint32_t ALIGNMENT = 32;

	const uint32_t offset = m_IndexUploadRing->ScheduleUpload(
		m_PrependCommandContext->GetCommandBuffer(),
		std::span<const std::byte>{static_cast<const std::byte*>(data), dataSize}, ALIGNMENT);
	BindIndexBuffer(m_IndexUploadRing->GetBuffer(), offset);
}

void CDeviceCommandContext::BeginPass()
{
	ENSURE(m_InsideFramebufferPass);
	m_InsidePass = true;
}

void CDeviceCommandContext::EndPass()
{
	ENSURE(m_InsidePass);
	m_InsidePass = false;
}

void CDeviceCommandContext::Draw(const uint32_t firstVertex, const uint32_t vertexCount)
{
	PreDraw();
	vkCmdDraw(m_CommandContext->GetCommandBuffer(), vertexCount, 1, firstVertex, 0);
}

void CDeviceCommandContext::DrawIndexed(
	const uint32_t firstIndex, const uint32_t indexCount, const int32_t vertexOffset)
{
	ENSURE(vertexOffset == 0);
	PreDraw();
	vkCmdDrawIndexed(m_CommandContext->GetCommandBuffer(), indexCount, 1, firstIndex, 0, 0);
}

void CDeviceCommandContext::DrawInstanced(
	const uint32_t firstVertex, const uint32_t vertexCount,
	const uint32_t firstInstance, const uint32_t instanceCount)
{
	PreDraw();
	vkCmdDraw(
		m_CommandContext->GetCommandBuffer(), vertexCount, instanceCount, firstVertex, firstInstance);
}

void CDeviceCommandContext::DrawIndexedInstanced(
	const uint32_t firstIndex, const uint32_t indexCount,
	const uint32_t firstInstance, const uint32_t instanceCount,
	const int32_t vertexOffset)
{
	PreDraw();
	vkCmdDrawIndexed(
		m_CommandContext->GetCommandBuffer(), indexCount, instanceCount, firstIndex, vertexOffset, firstInstance);
}

void CDeviceCommandContext::DrawIndexedInRange(
	const uint32_t firstIndex, const uint32_t indexCount,
	const uint32_t /*start*/, const uint32_t /*end*/)
{
	DrawIndexed(firstIndex, indexCount, 0);
}

void CDeviceCommandContext::BeginComputePass()
{
	ENSURE(!m_InsideFramebufferPass);
	ENSURE(!m_InsideComputePass);
	m_InsideComputePass = true;
}

void CDeviceCommandContext::EndComputePass()
{
	if (m_ShaderProgram)
	{
		m_ShaderProgram->Unbind();
		m_ShaderProgram = nullptr;
	}
	m_LastBoundPipeline = VK_NULL_HANDLE;
	m_ComputePipelineState = nullptr;

	ENSURE(m_InsideComputePass);
	m_InsideComputePass = false;
}

void CDeviceCommandContext::Dispatch(
	const uint32_t groupCountX,
	const uint32_t groupCountY,
	const uint32_t groupCountZ)
{
	ENSURE(m_InsideComputePass);
	ApplyPipelineStateIfDirty();
	m_ShaderProgram->PreDispatch(*m_CommandContext);
	UpdateOutdatedConstants();
	vkCmdDispatch(
		m_CommandContext->GetCommandBuffer(), groupCountX, groupCountY, groupCountZ);
	m_ShaderProgram->PostDispatch(*m_CommandContext);
}

void CDeviceCommandContext::InsertMemoryBarrier(
	const uint32_t srcStageMask, const uint32_t dstStageMask,
	const uint32_t srcAccessMask, const uint32_t dstAccessMask)
{
	ENSURE(!m_InsideFramebufferPass);
	Utilities::SubmitMemoryBarrier(
		m_CommandContext->GetCommandBuffer(),
		Mapping::FromAccessMask(srcAccessMask), Mapping::FromAccessMask(dstAccessMask),
		Mapping::FromPipelineStageMask(srcStageMask), Mapping::FromPipelineStageMask(dstStageMask));
}

void CDeviceCommandContext::SetTexture(const int32_t bindingSlot, ITexture* texture)
{
	if (bindingSlot < 0)
		return;

	ENSURE(m_InsidePass || m_InsideComputePass);
	ENSURE(texture);
	CTexture* textureToBind = texture->As<CTexture>();
	ENSURE(textureToBind->GetUsage() & ITexture::Usage::SAMPLED);

	if (!m_Device->GetDescriptorManager().UseDescriptorIndexing() && m_InsidePass)
	{
		// We can't bind textures which are used as attachments.
		const auto& colorAttachments = m_Framebuffer->GetColorAttachments();
		ENSURE(std::find(
			colorAttachments.begin(), colorAttachments.end(), textureToBind) == colorAttachments.end());
		ENSURE(m_Framebuffer->GetDepthStencilAttachment() != textureToBind);

		ENSURE(textureToBind->IsInitialized());
	}

	m_ShaderProgram->SetTexture(bindingSlot, textureToBind);
}

void CDeviceCommandContext::SetStorageTexture(const int32_t bindingSlot, ITexture* texture)
{
	ENSURE(m_InsidePass || m_InsideComputePass);
	ENSURE(texture);
	CTexture* textureToBind = texture->As<CTexture>();
	ENSURE(textureToBind->GetUsage() & ITexture::Usage::STORAGE);
	m_ShaderProgram->SetStorageTexture(bindingSlot, textureToBind);
}

void CDeviceCommandContext::SetStorageBuffer(const int32_t bindingSlot, IBuffer* buffer)
{
	ENSURE(m_InsidePass || m_InsideComputePass);
	ENSURE(buffer);
	CBuffer* bufferToBind = buffer->As<CBuffer>();
	ENSURE(bufferToBind->GetType() == IBuffer::Type::VERTEX || bufferToBind->GetType() == IBuffer::Type::INDEX);
	ENSURE(bufferToBind->GetUsage() & IBuffer::Usage::STORAGE);
	m_ShaderProgram->SetStorageBuffer(bindingSlot, bufferToBind);
}

void CDeviceCommandContext::SetUniform(
	const int32_t bindingSlot,
	const float value)
{
	ENSURE(m_InsidePass || m_InsideComputePass);
	m_ShaderProgram->SetUniform(bindingSlot, value);
}

void CDeviceCommandContext::SetUniform(
	const int32_t bindingSlot,
	const float valueX, const float valueY)
{
	ENSURE(m_InsidePass || m_InsideComputePass);
	m_ShaderProgram->SetUniform(bindingSlot, valueX, valueY);
}

void CDeviceCommandContext::SetUniform(
	const int32_t bindingSlot,
	const float valueX, const float valueY,
	const float valueZ)
{
	ENSURE(m_InsidePass || m_InsideComputePass);
	m_ShaderProgram->SetUniform(bindingSlot, valueX, valueY, valueZ);
}

void CDeviceCommandContext::SetUniform(
	const int32_t bindingSlot,
	const float valueX, const float valueY,
	const float valueZ, const float valueW)
{
	ENSURE(m_InsidePass || m_InsideComputePass);
	m_ShaderProgram->SetUniform(bindingSlot, valueX, valueY, valueZ, valueW);
}

void CDeviceCommandContext::SetUniform(
	const int32_t bindingSlot, std::span<const float> values)
{
	ENSURE(m_InsidePass || m_InsideComputePass);
	m_ShaderProgram->SetUniform(bindingSlot, values);
}

void CDeviceCommandContext::InsertTimestampQuery(const uint32_t handle, const bool isScopeBegin)
{
	ENSURE(!m_InsideFramebufferPass);
	m_Device->InsertTimestampQuery(m_CommandContext->GetCommandBuffer(), handle, isScopeBegin);
}

void CDeviceCommandContext::BeginScopedLabel(const char* name)
{
	if (!m_DebugScopedLabels)
		return;
	VkDebugUtilsLabelEXT label{};
	label.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_LABEL_EXT;
	label.pLabelName = name;
	vkCmdBeginDebugUtilsLabelEXT(m_CommandContext->GetCommandBuffer(), &label);
}

void CDeviceCommandContext::EndScopedLabel()
{
	if (!m_DebugScopedLabels)
		return;
	vkCmdEndDebugUtilsLabelEXT(m_CommandContext->GetCommandBuffer());
}

void CDeviceCommandContext::Flush()
{
	ENSURE(!m_InsideFramebufferPass);
	// TODO: fix unsafe copying when overlaping flushes/frames.

	m_VertexUploadRing->ExecuteUploads(m_PrependCommandContext->GetCommandBuffer());
	m_IndexUploadRing->ExecuteUploads(m_PrependCommandContext->GetCommandBuffer());
	m_UniformUploadRing->ExecuteUploads(m_PrependCommandContext->GetCommandBuffer());

	m_IsPipelineStateDirty = true;

	CTexture* backbufferReadbackTexture = m_QueuedReadbacks.empty()
		? nullptr : m_Device->GetOrCreateBackbufferReadbackTexture();
	const bool needsReadback = backbufferReadbackTexture;
	if (needsReadback)
	{
		CTexture* backbufferTexture = m_Device->GetCurrentBackbufferTexture();

		{
		// We assume that the readback texture is in linear tiling.
		ScopedImageLayoutTransition scopedBackbufferTransition{
			*m_CommandContext,
			{&backbufferTexture, 1},
			VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
			VK_ACCESS_TRANSFER_READ_BIT,
			VK_PIPELINE_STAGE_TRANSFER_BIT};
		ScopedImageLayoutTransition scopedReadbackBackbufferTransition{
			*m_CommandContext,
			{&backbufferReadbackTexture, 1},
			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			VK_ACCESS_TRANSFER_WRITE_BIT,
			VK_PIPELINE_STAGE_TRANSFER_BIT};
		VkImageCopy region{};
		region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		region.srcSubresource.layerCount = 1;
		region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		region.dstSubresource.layerCount = 1;
		region.extent.width = backbufferTexture->GetWidth();
		region.extent.height = backbufferTexture->GetHeight();
		region.extent.depth = 1;
		vkCmdCopyImage(
			m_CommandContext->GetCommandBuffer(),
			backbufferTexture->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
			backbufferReadbackTexture->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			1, &region);
		}

		Utilities::SubmitMemoryBarrier(
			m_CommandContext->GetCommandBuffer(),
			VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT,
			VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT);

		m_PrependCommandContext->Flush();
		m_CommandContext->FlushAndWait();

		VkImageSubresource subresource{};
		subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		VkSubresourceLayout subresourceLayout{};
		vkGetImageSubresourceLayout(
			m_Device->GetVkDevice(), backbufferReadbackTexture->GetImage(), &subresource, &subresourceLayout);

		void* mappedData = backbufferReadbackTexture->GetMappedData();
		ENSURE(mappedData);
		const uint32_t height = backbufferReadbackTexture->GetHeight();
		const auto [redOffset, greenOffset, blueOffset] = backbufferReadbackTexture->GetFormat() == Format::B8G8R8A8_UNORM
			? std::make_tuple(2, 1, 0) : std::make_tuple(0, 1, 2);
		for (const QueuedReadback& queuedReackback : m_QueuedReadbacks)
		{
			const std::byte* data = static_cast<const std::byte*>(mappedData);
			// Currently the backbuffer (0, 0) is the left-bottom corner (legacy from GL).
			data += subresourceLayout.offset + subresourceLayout.rowPitch * (height - queuedReackback.height - queuedReackback.y);
			for (uint32_t y = 0; y < queuedReackback.height; ++y)
			{
				const std::byte* row = data;
				for (uint32_t x = 0; x < queuedReackback.width; ++x)
				{
					const uint32_t sourceIndex = (queuedReackback.x + x) * 4;
					const uint32_t destinationIndex = ((queuedReackback.height - y - 1) * queuedReackback.width + x) * 3;
					std::byte* destinationPixelData = static_cast<std::byte*>(queuedReackback.data) + destinationIndex;
					destinationPixelData[0] = row[sourceIndex + redOffset];
					destinationPixelData[1] = row[sourceIndex + greenOffset];
					destinationPixelData[2] = row[sourceIndex + blueOffset];
				}
				data += subresourceLayout.rowPitch;
			}
		}
	}
	else
	{
		m_PrependCommandContext->Flush();
		m_CommandContext->Flush();
	}

	m_QueuedReadbacks.clear();
}

void CDeviceCommandContext::PreDraw()
{
	ENSURE(m_InsidePass);
	ApplyPipelineStateIfDirty();
	m_ShaderProgram->PreDraw(*m_CommandContext);
	UpdateOutdatedConstants();
}

void CDeviceCommandContext::UpdateOutdatedConstants()
{
	if (m_ShaderProgram->IsMaterialConstantsDataOutdated())
	{
		const VkDeviceSize alignment =
			std::max(static_cast<VkDeviceSize>(16), m_Device->GetChoosenPhysicalDevice().properties.limits.minUniformBufferOffsetAlignment);
		const uint32_t offset = m_UniformUploadRing->ScheduleUpload(
			m_PrependCommandContext->GetCommandBuffer(),
			std::span<const std::byte>{
				m_ShaderProgram->GetMaterialConstantsData(),
				m_ShaderProgram->GetMaterialConstantsDataSize()}, alignment);
		m_ShaderProgram->UpdateMaterialConstantsData();

		const VkDescriptorSet uniformDescriptorSet{GetUniformDescriptorSet(
			m_UniformUploadRing->GetBuffer(), m_ShaderProgram->GetMaterialConstantsDataSize())};
		// TODO: maybe move inside shader program to reduce the # of bind calls.
		vkCmdBindDescriptorSets(
			m_CommandContext->GetCommandBuffer(), m_ShaderProgram->GetPipelineBindPoint(),
			m_ShaderProgram->GetPipelineLayout(), m_Device->GetDescriptorManager().GetUniformSet(),
			1, &uniformDescriptorSet, 1, &offset);
	}
}

void CDeviceCommandContext::ApplyPipelineStateIfDirty()
{
	if (!m_IsPipelineStateDirty)
		return;
	m_IsPipelineStateDirty = false;

	ENSURE(m_GraphicsPipelineState || m_ComputePipelineState);
	if (m_GraphicsPipelineState)
	{
		ENSURE(m_VertexInputLayout);
		ENSURE(m_Framebuffer);
	}

	VkPipeline pipeline{m_GraphicsPipelineState
		? m_GraphicsPipelineState->GetOrCreatePipeline(m_VertexInputLayout, m_Framebuffer)
		: m_ComputePipelineState->GetPipeline()};
	ENSURE(pipeline != VK_NULL_HANDLE);

	if (m_LastBoundPipeline != pipeline)
	{
		m_LastBoundPipeline = pipeline;
		vkCmdBindPipeline(m_CommandContext->GetCommandBuffer(), m_ShaderProgram->GetPipelineBindPoint(), pipeline);

		m_ShaderProgram->Bind();

		if (m_Device->GetDescriptorManager().UseDescriptorIndexing())
		{
			vkCmdBindDescriptorSets(
				m_CommandContext->GetCommandBuffer(), m_ShaderProgram->GetPipelineBindPoint(),
				m_ShaderProgram->GetPipelineLayout(), 0,
				1, &m_Device->GetDescriptorManager().GetDescriptorIndexingSet(), 0, nullptr);
		}
	}
}

void CDeviceCommandContext::BindVertexBuffer(
	const uint32_t bindingSlot, CBuffer* buffer, uint32_t offset)
{
	VkBuffer vertexBuffers[] = { buffer->GetVkBuffer() };
	VkDeviceSize offsets[] = { offset };
	vkCmdBindVertexBuffers(
		m_CommandContext->GetCommandBuffer(), bindingSlot, std::size(vertexBuffers), vertexBuffers, offsets);
}

void CDeviceCommandContext::BindIndexBuffer(CBuffer* buffer, uint32_t offset)
{
	if (buffer == m_BoundIndexBuffer && offset == m_BoundIndexBufferOffset)
		return;
	m_BoundIndexBuffer = buffer;
	m_BoundIndexBufferOffset = offset;
	vkCmdBindIndexBuffer(
		m_CommandContext->GetCommandBuffer(), buffer->GetVkBuffer(), offset, VK_INDEX_TYPE_UINT16);
}

VkDescriptorSet CDeviceCommandContext::GetUniformDescriptorSet(
	CBuffer* uniformBuffer, const uint32_t dataSize)
{
	ENSURE(uniformBuffer);

	if (m_UniformBufferUID == INVALID_DEVICE_OBJECT_UID || m_UniformBufferUID != uniformBuffer->GetUID())
	{
		if (m_UniformDescriptorPool != VK_NULL_HANDLE)
		{
			m_Device->ScheduleObjectToDestroy(
				VK_OBJECT_TYPE_DESCRIPTOR_POOL, m_UniformDescriptorPool, VK_NULL_HANDLE);
			m_UniformDescriptorPool = VK_NULL_HANDLE;
		}

		m_UniformBufferUID = uniformBuffer->GetUID();
		m_UniformDescriptorSets.clear();

		constexpr uint32_t initialSize{16};
		const uint32_t maxUniformBufferRange{m_Device->GetChoosenPhysicalDevice().properties.limits.maxUniformBufferRange};
		const uint32_t maxSize{std::min(uniformBuffer->GetSize(), 65536u)};

		uint32_t maxSets{0};
		for (uint32_t size{initialSize}; size <= maxUniformBufferRange && size <= maxSize; size *= 2)
			++maxSets;

		VkDescriptorPoolSize descriptorPoolSize{};
		descriptorPoolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
		descriptorPoolSize.descriptorCount = maxSets;

		VkDescriptorPoolCreateInfo descriptorPoolCreateInfo{};
		descriptorPoolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
		descriptorPoolCreateInfo.poolSizeCount = 1;
		descriptorPoolCreateInfo.pPoolSizes = &descriptorPoolSize;
		descriptorPoolCreateInfo.maxSets = maxSets;
		ENSURE_VK_SUCCESS(vkCreateDescriptorPool(
			m_Device->GetVkDevice(), &descriptorPoolCreateInfo, nullptr, &m_UniformDescriptorPool));

		PS::StaticVector<VkDescriptorBufferInfo, 16> descriptorBufferInfos;
		PS::StaticVector<VkWriteDescriptorSet, 16> writeDescriptorSets;
		for (uint32_t size{initialSize}; size <= maxUniformBufferRange && size <= maxSize; size *= 2)
		{
			VkDescriptorSetAllocateInfo descriptorSetAllocateInfo{};
			descriptorSetAllocateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
			descriptorSetAllocateInfo.descriptorPool = m_UniformDescriptorPool;
			descriptorSetAllocateInfo.descriptorSetCount = 1;
			descriptorSetAllocateInfo.pSetLayouts = &m_Device->GetDescriptorManager().GetUniformDescriptorSetLayout();

			VkDescriptorSet descriptorSet{VK_NULL_HANDLE};
			ENSURE_VK_SUCCESS(vkAllocateDescriptorSets(
				m_Device->GetVkDevice(), &descriptorSetAllocateInfo, &descriptorSet));

			m_UniformDescriptorSets.emplace_back(descriptorSet, size);
			descriptorBufferInfos.emplace_back(uniformBuffer->GetVkBuffer(), 0u, size);

			VkWriteDescriptorSet writeDescriptorSet{};
			writeDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
			writeDescriptorSet.dstSet = descriptorSet;
			writeDescriptorSet.dstBinding = 0;
			writeDescriptorSet.dstArrayElement = 0;
			writeDescriptorSet.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC;
			writeDescriptorSet.descriptorCount = 1;
			writeDescriptorSet.pBufferInfo = &descriptorBufferInfos.back();
			writeDescriptorSets.emplace_back(writeDescriptorSet);
		}

		vkUpdateDescriptorSets(
			m_Device->GetVkDevice(), writeDescriptorSets.size(), writeDescriptorSets.data(), 0, nullptr);
	}

	VkDescriptorSet descriptorSet{VK_NULL_HANDLE};
	for (const UniformDescriptorSet& uniformDescriptorSet : m_UniformDescriptorSets)
		if (uniformDescriptorSet.size >= dataSize)
		{
			descriptorSet = uniformDescriptorSet.descriptorSet;
			break;
		}
	return descriptorSet;
}

} // namespace Vulkan

} // namespace Backend

} // namespace Renderer
