Implements framebuffer readback for Vulkan to allow screenshots.

Differential Revision: https://code.wildfiregames.com/D4940
This was SVN commit r27552.
This commit is contained in:
Vladislav Belov 2023-02-17 17:36:10 +00:00
parent 11c9e33d0f
commit 4355c8675b
20 changed files with 468 additions and 100 deletions

View File

@ -404,9 +404,17 @@ void CPostprocManager::BlitOutputFramebuffer(
GPU_SCOPED_LABEL(deviceCommandContext, "Copy postproc to backbuffer");
Renderer::Backend::IFramebuffer* source =
(m_WhichBuffer ? m_PingFramebuffer : m_PongFramebuffer).get();
// We blit to the backbuffer from the previous active buffer.
// We'll have upscaling/downscaling separately.
Renderer::Backend::IDeviceCommandContext::Rect region{};
region.width = std::min(source->GetWidth(), destination->GetWidth());
region.height = std::min(source->GetHeight(), destination->GetHeight());
deviceCommandContext->BlitFramebuffer(
destination, (m_WhichBuffer ? m_PingFramebuffer : m_PongFramebuffer).get());
source, destination, region, region,
Renderer::Backend::Sampler::Filter::NEAREST);
}
void CPostprocManager::ApplyEffect(
@ -690,6 +698,6 @@ void CPostprocManager::ResolveMultisampleFramebuffer(
return;
GPU_SCOPED_LABEL(deviceCommandContext, "Resolve postproc multisample");
deviceCommandContext->BlitFramebuffer(
m_PingFramebuffer.get(), m_MultisampleFramebuffer.get());
deviceCommandContext->ResolveFramebuffer(
m_MultisampleFramebuffer.get(), m_PingFramebuffer.get());
}

View File

@ -488,7 +488,7 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger)
Renderer::Backend::IFramebuffer* framebuffer = nullptr;
CPostprocManager& postprocManager = g_Renderer.GetPostprocManager();
CPostprocManager& postprocManager = GetPostprocManager();
if (postprocManager.IsEnabled())
{
// We have to update the post process manager with real near/far planes
@ -515,8 +515,8 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger)
m->deviceCommandContext->BeginFramebufferPass(framebuffer);
Renderer::Backend::IDeviceCommandContext::Rect viewportRect{};
viewportRect.width = framebuffer->GetWidth();
viewportRect.height = framebuffer->GetHeight();
viewportRect.width = m_Width;
viewportRect.height = m_Height;
m->deviceCommandContext->SetViewports(1, &viewportRect);
g_Game->GetView()->Render(m->deviceCommandContext.get());
@ -542,8 +542,8 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger)
m->deviceCommandContext->BeginFramebufferPass(backbuffer);
Renderer::Backend::IDeviceCommandContext::Rect viewportRect{};
viewportRect.width = backbuffer->GetWidth();
viewportRect.height = backbuffer->GetHeight();
viewportRect.width = m_Width;
viewportRect.height = m_Height;
m->deviceCommandContext->SetViewports(1, &viewportRect);
}
@ -571,8 +571,8 @@ void CRenderer::RenderFrameImpl(const bool renderGUI, const bool renderLogger)
m->deviceCommandContext->BeginFramebufferPass(backbuffer);
Renderer::Backend::IDeviceCommandContext::Rect viewportRect{};
viewportRect.width = backbuffer->GetWidth();
viewportRect.height = backbuffer->GetHeight();
viewportRect.width = m_Width;
viewportRect.height = m_Height;
m->deviceCommandContext->SetViewports(1, &viewportRect);
}
@ -757,7 +757,9 @@ void CRenderer::RenderBigScreenShot(const bool needsPresent)
// Adjust the camera to render the appropriate region
if (oldCamera.GetProjectionType() == CCamera::ProjectionType::PERSPECTIVE)
{
projection.SetPerspectiveTile(oldCamera.GetFOV(), aspectRatio, oldCamera.GetNearPlane(), oldCamera.GetFarPlane(), tiles, tileX, tileY);
projection.SetPerspectiveTile(
oldCamera.GetFOV(), aspectRatio, oldCamera.GetNearPlane(), oldCamera.GetFarPlane(),
tiles, tileX, tileY);
}
g_Game->GetView()->GetCamera()->SetProjection(projection);

View File

@ -33,6 +33,7 @@ enum class Format
R8G8B8_UNORM,
R8G8B8A8_UNORM,
R8G8B8A8_UINT,
B8G8R8A8_UNORM,
// TODO: we need to drop legacy A8 and L8 formats as soon as we have proper
// channel swizzling.

View File

@ -22,6 +22,7 @@
#include "renderer/backend/Format.h"
#include "renderer/backend/IDeviceObject.h"
#include "renderer/backend/PipelineState.h"
#include "renderer/backend/Sampler.h"
#include <cstdint>
#include <functional>
@ -46,8 +47,30 @@ public:
*/
virtual void SetGraphicsPipelineState(IGraphicsPipelineState* pipelineState) = 0;
// TODO: maybe we should add a more common type, like CRectI.
struct Rect
{
int32_t x, y;
int32_t width, height;
};
/**
* Copies source region into destination region automatically applying
* compatible format conversion and scaling using a provided filter.
* A backbuffer can't be a source.
*/
virtual void BlitFramebuffer(
IFramebuffer* destinationFramebuffer, IFramebuffer* sourceFramebuffer) = 0;
IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer,
const Rect& sourceRegion, const Rect& destinationRegion,
const Sampler::Filter filter) = 0;
/**
* Resolves multisample source framebuffer attachments to destination
* attachments. Source attachments should have a sample count > 1 and
* destination attachments should have a sample count = 1.
* A backbuffer can't be a source.
*/
virtual void ResolveFramebuffer(
IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer) = 0;
/**
* Starts a framebuffer pass, performs attachment load operations.
@ -68,6 +91,15 @@ public:
*/
virtual void ClearFramebuffer(const bool color, const bool depth, const bool stencil) = 0;
/**
* Readbacks the current backbuffer to data in R8G8B8_UNORM format somewhen
* between the function call and Flush (inclusively). Because of that the
* data pointer should be valid in that time period and have enough space
* to fit the readback result.
* @note this operation is very slow and should not be used regularly.
* TODO: ideally we should do readback on Present or even asynchronously
* but a client doesn't support that yet.
*/
virtual void ReadbackFramebufferSync(
const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height,
void* data) = 0;
@ -90,12 +122,6 @@ public:
IBuffer* buffer, const uint32_t dataOffset, const uint32_t dataSize,
const UploadBufferFunction& uploadFunction) = 0;
// TODO: maybe we should add a more common type, like CRectI.
struct Rect
{
int32_t x, y;
int32_t width, height;
};
virtual void SetScissors(const uint32_t scissorCount, const Rect* scissors) = 0;
virtual void SetViewports(const uint32_t viewportCount, const Rect* viewports) = 0;

View File

@ -99,7 +99,12 @@ void CDeviceCommandContext::Flush()
{
}
void CDeviceCommandContext::BlitFramebuffer(IFramebuffer*, IFramebuffer*)
void CDeviceCommandContext::BlitFramebuffer(
IFramebuffer*, IFramebuffer*, const Rect&, const Rect&, const Sampler::Filter)
{
}
void CDeviceCommandContext::ResolveFramebuffer(IFramebuffer*, IFramebuffer*)
{
}

View File

@ -48,7 +48,12 @@ public:
void SetGraphicsPipelineState(IGraphicsPipelineState* pipelineState) override;
void BlitFramebuffer(IFramebuffer* destinationFramebuffer, IFramebuffer* sourceFramebuffer) override;
void BlitFramebuffer(
IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer,
const Rect& sourceRegion, const Rect& destinationRegion,
const Sampler::Filter filter) override;
void ResolveFramebuffer(
IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer) override;
void ClearFramebuffer(const bool color, const bool depth, const bool stencil) override;
void BeginFramebufferPass(IFramebuffer* framebuffer) override;

View File

@ -806,7 +806,9 @@ void CDeviceCommandContext::SetGraphicsPipelineStateImpl(
}
void CDeviceCommandContext::BlitFramebuffer(
IFramebuffer* dstFramebuffer, IFramebuffer* srcFramebuffer)
IFramebuffer* srcFramebuffer, IFramebuffer* dstFramebuffer,
const Rect& sourceRegion, const Rect& destinationRegion,
const Sampler::Filter filter)
{
ENSURE(!m_InsideFramebufferPass);
CFramebuffer* destinationFramebuffer = dstFramebuffer->As<CFramebuffer>();
@ -814,6 +816,9 @@ void CDeviceCommandContext::BlitFramebuffer(
#if CONFIG2_GLES
UNUSED2(destinationFramebuffer);
UNUSED2(sourceFramebuffer);
UNUSED2(destinationRegion);
UNUSED2(sourceRegion);
UNUSED2(filter);
debug_warn("CDeviceCommandContext::BlitFramebuffer is not implemented for GLES");
#else
// Source framebuffer should not be backbuffer.
@ -821,8 +826,34 @@ void CDeviceCommandContext::BlitFramebuffer(
ENSURE(destinationFramebuffer != sourceFramebuffer);
glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, sourceFramebuffer->GetHandle());
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, destinationFramebuffer->GetHandle());
// TODO: add more check for internal formats. And currently we don't support
// scaling inside blit.
// TODO: add more check for internal formats.
glBlitFramebufferEXT(
sourceRegion.x, sourceRegion.y, sourceRegion.width, sourceRegion.height,
destinationRegion.x, destinationRegion.y, destinationRegion.width, destinationRegion.height,
(sourceFramebuffer->GetAttachmentMask() & destinationFramebuffer->GetAttachmentMask()),
filter == Sampler::Filter::LINEAR ? GL_LINEAR : GL_NEAREST);
ogl_WarnIfError();
#endif
}
void CDeviceCommandContext::ResolveFramebuffer(
IFramebuffer* srcFramebuffer, IFramebuffer* dstFramebuffer)
{
ENSURE(!m_InsideFramebufferPass);
CFramebuffer* destinationFramebuffer = dstFramebuffer->As<CFramebuffer>();
CFramebuffer* sourceFramebuffer = srcFramebuffer->As<CFramebuffer>();
ENSURE(destinationFramebuffer->GetWidth() == sourceFramebuffer->GetWidth());
ENSURE(destinationFramebuffer->GetHeight() == sourceFramebuffer->GetHeight());
#if CONFIG2_GLES
UNUSED2(destinationFramebuffer);
UNUSED2(sourceFramebuffer);
debug_warn("CDeviceCommandContext::ResolveFramebuffer is not implemented for GLES");
#else
// Source framebuffer should not be backbuffer.
ENSURE(sourceFramebuffer->GetHandle() != 0);
ENSURE(destinationFramebuffer != sourceFramebuffer);
glBindFramebufferEXT(GL_READ_FRAMEBUFFER_EXT, sourceFramebuffer->GetHandle());
glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER_EXT, destinationFramebuffer->GetHandle());
glBlitFramebufferEXT(
0, 0, sourceFramebuffer->GetWidth(), sourceFramebuffer->GetHeight(),
0, 0, sourceFramebuffer->GetWidth(), sourceFramebuffer->GetHeight(),

View File

@ -57,7 +57,12 @@ public:
void SetGraphicsPipelineState(IGraphicsPipelineState* pipelineState) override;
void BlitFramebuffer(IFramebuffer* destinationFramebuffer, IFramebuffer* sourceFramebuffer) override;
void BlitFramebuffer(
IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer,
const Rect& sourceRegion, const Rect& destinationRegion,
const Sampler::Filter filter) override;
void ResolveFramebuffer(
IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer) override;
void BeginFramebufferPass(IFramebuffer* framebuffer) override;
void EndFramebufferPass() override;

View File

@ -640,6 +640,8 @@ CDevice::~CDevice()
// The order of destroying does matter to avoid use-after-free and validation
// layers complaints.
m_BackbufferReadbackTexture.reset();
m_SubmitScheduler.reset();
ProcessTextureToDestroyQueue(true);
@ -927,6 +929,7 @@ std::unique_ptr<CRingCommandContext> CDevice::CreateRingCommandContext(const siz
void CDevice::RecreateSwapChain()
{
m_BackbufferReadbackTexture.reset();
int surfaceDrawableWidth = 0, surfaceDrawableHeight = 0;
SDL_Vulkan_GetDrawableSize(m_Window, &surfaceDrawableWidth, &surfaceDrawableHeight);
m_SwapChain = CSwapChain::Create(
@ -998,6 +1001,27 @@ void CDevice::ProcessTextureToDestroyQueue(const bool ignoreFrameID)
}
}
CTexture* CDevice::GetCurrentBackbufferTexture()
{
return IsSwapChainValid() ? m_SwapChain->GetCurrentBackbufferTexture() : nullptr;
}
CTexture* CDevice::GetOrCreateBackbufferReadbackTexture()
{
if (!IsSwapChainValid())
return nullptr;
if (!m_BackbufferReadbackTexture)
{
CTexture* currentBackbufferTexture = m_SwapChain->GetCurrentBackbufferTexture();
m_BackbufferReadbackTexture = CTexture::CreateReadback(
this, "BackbufferReadback",
currentBackbufferTexture->GetFormat(),
currentBackbufferTexture->GetWidth(),
currentBackbufferTexture->GetHeight());
}
return m_BackbufferReadbackTexture.get();
}
std::unique_ptr<IDevice> CreateDevice(SDL_Window* window)
{
return Vulkan::CDevice::Create(window);

View File

@ -159,6 +159,10 @@ public:
CDescriptorManager& GetDescriptorManager() { return *m_DescriptorManager; }
CTexture* GetCurrentBackbufferTexture();
CTexture* GetOrCreateBackbufferReadbackTexture();
private:
CDevice();
@ -194,6 +198,7 @@ private:
uint32_t m_GraphicsQueueFamilyIndex = std::numeric_limits<uint32_t>::max();
std::unique_ptr<CSwapChain> m_SwapChain;
std::unique_ptr<CTexture> m_BackbufferReadbackTexture;
uint32_t m_FrameID = 0;

View File

@ -36,6 +36,7 @@
#include <algorithm>
#include <cstddef>
#include <tuple>
namespace Renderer
{
@ -82,6 +83,14 @@ SBaseImageState GetBaseImageState(CTexture* texture)
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 {};
}
@ -130,6 +139,54 @@ private:
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.
@ -344,107 +401,97 @@ void CDeviceCommandContext::SetGraphicsPipelineState(
m_IsPipelineStateDirty = true;
}
void CDeviceCommandContext::BlitFramebuffer(IFramebuffer* destinationFramebuffer, IFramebuffer* sourceFramebuffer)
void CDeviceCommandContext::BlitFramebuffer(
IFramebuffer* sourceFramebuffer, IFramebuffer* destinationFramebuffer,
const Rect& sourceRegion, const Rect& destinationRegion,
const Sampler::Filter filter)
{
ENSURE(!m_InsideFramebufferPass);
const auto& sourceColorAttachments =
sourceFramebuffer->As<CFramebuffer>()->GetColorAttachments();
const auto& destinationColorAttachments =
destinationFramebuffer->As<CFramebuffer>()->GetColorAttachments();
ENSURE(sourceColorAttachments.size() == destinationColorAttachments.size());
// TODO: account depth.
//ENSURE(
// static_cast<bool>(sourceFramebuffer->As<CFramebuffer>()->GetDepthStencilAttachment()) ==
// static_cast<bool>(destinationFramebuffer->As<CFramebuffer>()->GetDepthStencilAttachment()));
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()));
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{
TransferForEachFramebufferAttachmentPair(
*m_CommandContext,
{sourceColorAttachments.begin(), sourceColorAttachments.end()},
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_ACCESS_TRANSFER_READ_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT};
ScopedImageLayoutTransition destinationColorAttachmentsTransition{
*m_CommandContext,
{destinationColorAttachments.begin(), destinationColorAttachments.end()},
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT};
// TODO: split BlitFramebuffer into ResolveFramebuffer and BlitFramebuffer.
if (sourceFramebuffer->As<CFramebuffer>()->GetSampleCount() == 1)
{
// TODO: we need to check for VK_FORMAT_FEATURE_BLIT_*_BIT for used formats.
for (CFramebuffer::ColorAttachments::size_type index = 0; index < destinationColorAttachments.size(); ++index)
sourceFramebuffer->As<CFramebuffer>(), destinationFramebuffer->As<CFramebuffer>(),
[&sourceRegion, &destinationRegion, filter](
VkCommandBuffer commandBuffer, CTexture* sourceColorAttachment, CTexture* destinationColorAttachment)
{
CTexture* sourceColorAttachment = sourceColorAttachments[index];
CTexture* destinationColorAttachment = destinationColorAttachments[index];
// 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{};
region.srcOffsets[1].x = sourceColorAttachment->GetWidth();
region.srcOffsets[1].y = sourceColorAttachment->GetHeight();
// 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 = destinationColorAttachment->GetWidth();
region.dstOffsets[1].y = destinationColorAttachment->GetHeight();
region.dstOffsets[1].x = destinationRegion.x + destinationRegion.width;
region.dstOffsets[1].y = destinationHeight - destinationRegion.y;
region.dstOffsets[1].z = 1;
region.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.srcSubresource.aspectMask = aspectMask;
region.srcSubresource.mipLevel = 0;
region.srcSubresource.baseArrayLayer = 0;
region.srcSubresource.layerCount = 1;
region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.dstSubresource.aspectMask = aspectMask;
region.dstSubresource.mipLevel = 0;
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
ENSURE(sourceColorAttachment->GetImage() != VK_NULL_HANDLE);
ENSURE(destinationColorAttachment->GetImage() != VK_NULL_HANDLE);
vkCmdBlitImage(
m_CommandContext->GetCommandBuffer(),
commandBuffer,
sourceColorAttachment->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
destinationColorAttachment->GetImage(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &region, VK_FILTER_NEAREST);
}
}
else
{
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());
for (CFramebuffer::ColorAttachments::size_type index = 0; index < destinationColorAttachments.size(); ++index)
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)
{
CTexture* sourceColorAttachment = sourceColorAttachments[index];
CTexture* destinationColorAttachment = destinationColorAttachments[index];
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 = VK_IMAGE_ASPECT_COLOR_BIT;
region.srcSubresource.aspectMask = aspectMask;
region.srcSubresource.mipLevel = 0;
region.srcSubresource.baseArrayLayer = 0;
region.srcSubresource.layerCount = 1;
region.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.dstSubresource.aspectMask = aspectMask;
region.dstSubresource.mipLevel = 0;
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
vkCmdResolveImage(
m_CommandContext->GetCommandBuffer(),
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)
@ -626,12 +673,18 @@ void CDeviceCommandContext::ReadbackFramebufferSync(
const uint32_t x, const uint32_t y, const uint32_t width, const uint32_t height,
void* data)
{
UNUSED2(x);
UNUSED2(y);
UNUSED2(width);
UNUSED2(height);
UNUSED2(data);
LOGERROR("Vulkan: framebuffer readback is not implemented yet.");
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,
@ -922,8 +975,89 @@ void CDeviceCommandContext::Flush()
m_IsPipelineStateDirty = true;
m_PrependCommandContext->Flush();
m_CommandContext->Flush();
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()

View File

@ -18,6 +18,7 @@
#ifndef INCLUDED_RENDERER_VULKAN_DEVICECOMMANDCONTEXT
#define INCLUDED_RENDERER_VULKAN_DEVICECOMMANDCONTEXT
#include "ps/containers/StaticVector.h"
#include "renderer/backend/IBuffer.h"
#include "renderer/backend/IDeviceCommandContext.h"
@ -51,7 +52,12 @@ public:
void SetGraphicsPipelineState(IGraphicsPipelineState* pipelineState) override;
void BlitFramebuffer(IFramebuffer* destinationFramebuffer, IFramebuffer* sourceFramebuffer) override;
void BlitFramebuffer(
IFramebuffer* destinationFramebuffer, IFramebuffer* sourceFramebuffer,
const Rect& destinationRegion, const Rect& sourceRegion,
const Sampler::Filter filter) override;
void ResolveFramebuffer(
IFramebuffer* destinationFramebuffer, IFramebuffer* sourceFramebuffer) override;
void ClearFramebuffer(const bool color, const bool depth, const bool stencil) override;
void BeginFramebufferPass(IFramebuffer* framebuffer) override;
@ -170,6 +176,16 @@ private:
VkDescriptorPool m_UniformDescriptorPool = VK_NULL_HANDLE;
VkDescriptorSet m_UniformDescriptorSet = VK_NULL_HANDLE;
// Currently we support readbacks only from backbuffer.
struct QueuedReadback
{
uint32_t x = 0, y = 0;
uint32_t width = 0, height = 0;
// It's a responsibility of the caller to guarantee that data is valid.
void* data = nullptr;
};
PS::StaticVector<QueuedReadback, 2> m_QueuedReadbacks;
};
} // namespace Vulkan

View File

@ -170,6 +170,7 @@ VkFormat FromFormat(const Format format)
CASE(R8G8_UINT)
CASE(R8G8B8A8_UNORM)
CASE(R8G8B8A8_UINT)
CASE(B8G8R8A8_UNORM)
CASE(R16_UNORM)
CASE(R16_UINT)

View File

@ -122,6 +122,17 @@ void CRingCommandContext::Flush()
m_RingIndex = (m_RingIndex + 1) % m_Ring.size();
}
void CRingCommandContext::FlushAndWait()
{
RingItem& item = m_Ring[m_RingIndex];
ENSURE(item.isBegan);
End();
item.handle = m_SubmitScheduler.Submit(item.commandBuffer);
WaitUntilFree(item);
}
void CRingCommandContext::ScheduleUpload(
CTexture* texture, const Format dataFormat,
const void* data, const size_t dataSize,

View File

@ -63,6 +63,12 @@ public:
*/
void Flush();
/**
* The same as Flush but also waits until it's completed. It means it
* forces SubmitScheduler to submit all previously queued work to GPU.
*/
void FlushAndWait();
/**
* Schedules uploads until next render pass or flush.
* @note doesn't save a command buffer returned by GetCommandBuffer during

View File

@ -62,9 +62,9 @@ public:
uint32_t GetFrameID() const { return m_FrameID; }
private:
void Flush();
private:
CDevice* m_Device = nullptr;
VkQueue m_Queue = VK_NULL_HANDLE;

View File

@ -371,6 +371,12 @@ CFramebuffer* CSwapChain::GetCurrentBackbuffer(
return it->second.get();
}
CTexture* CSwapChain::GetCurrentBackbufferTexture()
{
ENSURE(m_CurrentImageIndex != std::numeric_limits<uint32_t>::max());
return m_Textures[m_CurrentImageIndex].get();
}
} // namespace Vulkan
} // namespace Backend

View File

@ -62,6 +62,8 @@ public:
const AttachmentLoadOp depthStencilAttachmentLoadOp,
const AttachmentStoreOp depthStencilAttachmentStoreOp);
CTexture* GetCurrentBackbufferTexture();
CTexture* GetDepthTexture() { return m_DepthTexture.get(); }
private:

View File

@ -227,7 +227,12 @@ std::unique_ptr<CTexture> CTexture::WrapBackbufferImage(
std::unique_ptr<CTexture> texture(new CTexture());
texture->m_Device = device;
texture->m_Format = Format::UNDEFINED;
if (format == VK_FORMAT_R8G8B8A8_UNORM)
texture->m_Format = Format::R8G8B8A8_UNORM;
else if (format == VK_FORMAT_B8G8R8A8_UNORM)
texture->m_Format = Format::B8G8R8A8_UNORM;
else
texture->m_Format = Format::UNDEFINED;
texture->m_Type = Type::TEXTURE_2D;
if (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
texture->m_Usage |= Usage::COLOR_ATTACHMENT;
@ -268,6 +273,69 @@ std::unique_ptr<CTexture> CTexture::WrapBackbufferImage(
return texture;
}
// static
std::unique_ptr<CTexture> CTexture::CreateReadback(
CDevice* device, const char* name, const Format format,
const uint32_t width, const uint32_t height)
{
std::unique_ptr<CTexture> texture(new CTexture());
texture->m_Device = device;
texture->m_Format = format;
texture->m_Type = Type::TEXTURE_2D;
texture->m_Usage = Usage::TRANSFER_DST;
texture->m_Width = width;
texture->m_Height = height;
texture->m_MIPLevelCount = 1;
texture->m_SampleCount = 1;
texture->m_LayerCount = 1;
texture->m_VkFormat = Mapping::FromFormat(texture->m_Format);
texture->m_AttachmentImageAspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
texture->m_SamplerImageAspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
VkImageCreateInfo imageCreateInfo{};
imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
imageCreateInfo.extent.width = width;
imageCreateInfo.extent.height = height;
imageCreateInfo.extent.depth = 1;
imageCreateInfo.mipLevels = 1;
imageCreateInfo.arrayLayers = 1;
imageCreateInfo.format = texture->m_VkFormat;
imageCreateInfo.samples = Mapping::FromSampleCount(1);
imageCreateInfo.tiling = VK_IMAGE_TILING_LINEAR;
imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT;
imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VmaAllocationCreateInfo allocationCreateInfo{};
allocationCreateInfo.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT;
#ifndef NDEBUG
allocationCreateInfo.flags |= VMA_ALLOCATION_CREATE_USER_DATA_COPY_STRING_BIT;
allocationCreateInfo.pUserData = const_cast<char*>(name);
#endif
allocationCreateInfo.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
allocationCreateInfo.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
const VkResult createImageResult = vmaCreateImage(
device->GetVMAAllocator(), &imageCreateInfo, &allocationCreateInfo,
&texture->m_Image, &texture->m_Allocation, &texture->m_AllocationInfo);
if (createImageResult != VK_SUCCESS)
{
LOGERROR("Failed to create VkImage: %d", static_cast<int>(createImageResult));
return nullptr;
}
if (!texture->m_AllocationInfo.pMappedData)
{
LOGERROR("Failed to map readback image.");
return nullptr;
}
device->SetObjectName(VK_OBJECT_TYPE_IMAGE, texture->m_Image, name);
return texture;
}
CTexture::CTexture()
{
static uint32_t m_LastAvailableUID = 1;

View File

@ -63,6 +63,13 @@ public:
VkImageAspectFlags GetAttachmentImageAspectMask() { return m_AttachmentImageAspectMask; }
VkImageAspectFlags GetSamplerImageAspectMask() { return m_SamplerImageAspectMask; }
/**
* @return mapped data for readback textures else returns nullptr.
*/
void* GetMappedData() { return m_AllocationInfo.pMappedData; }
VkDeviceMemory GetDeviceMemory() { return m_AllocationInfo.deviceMemory; }
bool IsInitialized() const { return m_Initialized; }
void SetInitialized() { m_Initialized = true; }
@ -91,6 +98,10 @@ private:
CDevice* device, const char* name, const VkImage image, const VkFormat format,
const VkImageUsageFlags usage, const uint32_t width, const uint32_t height);
static std::unique_ptr<CTexture> CreateReadback(
CDevice* device, const char* name, const Format format,
const uint32_t width, const uint32_t height);
Type m_Type = Type::TEXTURE_2D;
uint32_t m_Usage = 0;
Format m_Format = Format::UNDEFINED;
@ -109,6 +120,7 @@ private:
VkSampler m_Sampler = VK_NULL_HANDLE;
bool m_IsCompareEnabled = false;
VmaAllocation m_Allocation{};
VmaAllocationInfo m_AllocationInfo{};
UID m_UID = 0;