Skip to content

Commit

Permalink
Merge pull request #10141 from hrydgard/vulkan-screenshots
Browse files Browse the repository at this point in the history
Windows/Vulkan: implement screenshots.
  • Loading branch information
hrydgard committed Nov 15, 2017
2 parents 656ec18 + 96d6f1c commit 143a199
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 39 deletions.
33 changes: 21 additions & 12 deletions Common/Vulkan/VulkanContext.cpp
Expand Up @@ -712,9 +712,8 @@ bool VulkanContext::InitQueue() {

bool VulkanContext::InitSwapchain() {
VkResult U_ASSERT_ONLY res;
VkSurfaceCapabilitiesKHR surfCapabilities;

res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_devices_[physical_device_], surface_, &surfCapabilities);
res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical_devices_[physical_device_], surface_, &surfCapabilities_);
assert(res == VK_SUCCESS);

uint32_t presentModeCount;
Expand All @@ -727,15 +726,15 @@ bool VulkanContext::InitSwapchain() {

VkExtent2D swapChainExtent;
// width and height are either both -1, or both not -1.
if (surfCapabilities.currentExtent.width == (uint32_t)-1) {
if (surfCapabilities_.currentExtent.width == (uint32_t)-1) {
// If the surface size is undefined, the size is set to
// the size of the images requested.
ILOG("initSwapchain: %dx%d", width_, height_);
swapChainExtent.width = width_;
swapChainExtent.height = height_;
} else {
// If the surface size is defined, the swap chain size must match
swapChainExtent = surfCapabilities.currentExtent;
swapChainExtent = surfCapabilities_.currentExtent;
}

// TODO: Find a better way to specify the prioritized present mode while being able
Expand Down Expand Up @@ -771,20 +770,20 @@ bool VulkanContext::InitSwapchain() {
// Determine the number of VkImage's to use in the swap chain (we desire to
// own only 1 image at a time, besides the images being displayed and
// queued for display):
uint32_t desiredNumberOfSwapChainImages = surfCapabilities.minImageCount + 1;
uint32_t desiredNumberOfSwapChainImages = surfCapabilities_.minImageCount + 1;
ILOG("numSwapChainImages: %d", desiredNumberOfSwapChainImages);
if ((surfCapabilities.maxImageCount > 0) &&
(desiredNumberOfSwapChainImages > surfCapabilities.maxImageCount))
if ((surfCapabilities_.maxImageCount > 0) &&
(desiredNumberOfSwapChainImages > surfCapabilities_.maxImageCount))
{
// Application must settle for fewer images than desired:
desiredNumberOfSwapChainImages = surfCapabilities.maxImageCount;
desiredNumberOfSwapChainImages = surfCapabilities_.maxImageCount;
}

VkSurfaceTransformFlagBitsKHR preTransform;
if (surfCapabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) {
if (surfCapabilities_.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) {
preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
} else {
preTransform = surfCapabilities.currentTransform;
preTransform = surfCapabilities_.currentTransform;
}

VkSwapchainCreateInfoKHR swap_chain_info = { VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
Expand All @@ -799,12 +798,22 @@ bool VulkanContext::InitSwapchain() {
swap_chain_info.presentMode = swapchainPresentMode;
swap_chain_info.oldSwapchain = VK_NULL_HANDLE;
swap_chain_info.clipped = true;
swap_chain_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
swap_chain_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
if (surfCapabilities_.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT)
swap_chain_info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT;

#ifndef ANDROID
// We don't support screenshots on Android
// Add more usage flags if they're supported.
if (surfCapabilities_.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT)
swap_chain_info.imageUsage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
#endif

swap_chain_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
swap_chain_info.queueFamilyIndexCount = 0;
swap_chain_info.pQueueFamilyIndices = NULL;
// OPAQUE is not supported everywhere.
if (surfCapabilities.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) {
if (surfCapabilities_.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) {
swap_chain_info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
} else {
// This should be supported anywhere, and is the only thing supported on the SHIELD TV, for example.
Expand Down
3 changes: 3 additions & 0 deletions Common/Vulkan/VulkanContext.h
Expand Up @@ -198,6 +198,7 @@ class VulkanContext {
const VkPhysicalDeviceFeatures &GetFeaturesAvailable() const { return featuresAvailable_; }
const VkPhysicalDeviceFeatures &GetFeaturesEnabled() const { return featuresEnabled_; }
const VulkanPhysicalDeviceInfo &GetDeviceInfo() const { return deviceInfo_; }
const VkSurfaceCapabilitiesKHR &GetSurfaceCapabilities() const { return surfCapabilities_; }

bool IsDeviceExtensionAvailable(const char *name) const {
for (auto &iter : device_extension_properties_) {
Expand Down Expand Up @@ -302,6 +303,8 @@ class VulkanContext {
VkPhysicalDeviceFeatures featuresAvailable_{};
VkPhysicalDeviceFeatures featuresEnabled_{};

VkSurfaceCapabilitiesKHR surfCapabilities_{};

std::vector<VkCommandBuffer> cmdQueue_;
};

Expand Down
12 changes: 11 additions & 1 deletion GPU/Vulkan/FramebufferVulkan.cpp
Expand Up @@ -699,4 +699,14 @@ void FramebufferManagerVulkan::CompilePostShader() {


usePostShader_ = true;
}
}

bool FramebufferManagerVulkan::GetOutputFramebuffer(GPUDebugBuffer &buffer) {
int w, h;
draw_->GetFramebufferDimensions(nullptr, &w, &h);
// I'm really not sure why I have to pass the wrong format here, it seems all the other color conversion are correct.
// But I get R/B swapped if I do what seems right... Maybe driver bug for copies from the backbuffer, who knows.
buffer.Allocate(w, h, GPU_DBG_FORMAT_8888_BGRA, false);
draw_->CopyFramebufferToMemorySync(nullptr, Draw::FB_COLOR_BIT, 0, 0, w, h, Draw::DataFormat::R8G8B8A8_UNORM, buffer.GetData(), w);
return true;
}
3 changes: 3 additions & 0 deletions GPU/Vulkan/FramebufferVulkan.h
Expand Up @@ -97,6 +97,9 @@ class FramebufferManagerVulkan : public FramebufferManagerCommon {
bool CreateDownloadTempBuffer(VirtualFramebuffer *nvfb) override;
void UpdateDownloadTempBuffer(VirtualFramebuffer *nvfb) override;

// Small tweak to the Common one.
bool GetOutputFramebuffer(GPUDebugBuffer &buffer);

private:
// The returned texture does not need to be free'd, might be returned from a pool (currently single entry)
void MakePixelTexture(const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, float &u1, float &v1) override;
Expand Down
1 change: 1 addition & 0 deletions ext/native/thin3d/DataFormat.h
Expand Up @@ -73,6 +73,7 @@ inline bool DataFormatIsColor(DataFormat fmt) {
}

void ConvertFromRGBA8888(uint8_t *dst, const uint8_t *src, uint32_t dstStride, uint32_t srcStride, uint32_t width, uint32_t height, DataFormat format);
void ConvertFromBGRA8888(uint8_t *dst, const uint8_t *src, uint32_t dstStride, uint32_t srcStride, uint32_t width, uint32_t height, DataFormat format);
void ConvertToD32F(uint8_t *dst, const uint8_t *src, uint32_t dstStride, uint32_t srcStride, uint32_t width, uint32_t height, DataFormat format);

} // namespace
66 changes: 49 additions & 17 deletions ext/native/thin3d/VulkanQueueRunner.cpp
Expand Up @@ -837,22 +837,6 @@ void VulkanQueueRunner::SetupTransitionToTransferDst(VKRImage &img, VkImageMemor
}

void VulkanQueueRunner::PerformReadback(const VKRStep &step, VkCommandBuffer cmd) {
VKRImage *srcImage;
if (step.readback.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) {
srcImage = &step.readback.src->color;
} else if (step.readback.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) {
srcImage = &step.readback.src->depth;
} else {
assert(false);
}

VkImageMemoryBarrier barrier{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
VkPipelineStageFlags stage = 0;
if (srcImage->layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
SetupTransitionToTransferSrc(*srcImage, barrier, stage, step.readback.aspectMask);
vkCmdPipelineBarrier(cmd, stage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}

ResizeReadbackBuffer(sizeof(uint32_t) * step.readback.srcRect.extent.width * step.readback.srcRect.extent.height);

VkBufferImageCopy region{};
Expand All @@ -863,9 +847,55 @@ void VulkanQueueRunner::PerformReadback(const VKRStep &step, VkCommandBuffer cmd
region.bufferOffset = 0;
region.bufferRowLength = step.readback.srcRect.extent.width;
region.bufferImageHeight = step.readback.srcRect.extent.height;
vkCmdCopyImageToBuffer(cmd, srcImage->image, srcImage->layout, readbackBuffer_, 1, &region);

VkImage image;
VkImageLayout copyLayout;
// Special case for backbuffer readbacks.
if (step.readback.src == nullptr) {
// We only take screenshots after the main render pass (anything else would be stupid) so we need to transition out of PRESENT,
// and then back into it.
TransitionImageLayout2(cmd, backbufferImage_, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, VK_ACCESS_TRANSFER_READ_BIT);
copyLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
image = backbufferImage_;
} else {
VKRImage *srcImage;
if (step.readback.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) {
srcImage = &step.readback.src->color;
}
else if (step.readback.aspectMask & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) {
srcImage = &step.readback.src->depth;
}
else {
assert(false);
}

VkImageMemoryBarrier barrier{ VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER };
VkPipelineStageFlags stage = 0;
if (srcImage->layout != VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) {
SetupTransitionToTransferSrc(*srcImage, barrier, stage, step.readback.aspectMask);
vkCmdPipelineBarrier(cmd, stage, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
}
image = srcImage->image;
copyLayout = srcImage->layout;
}

vkCmdCopyImageToBuffer(cmd, image, copyLayout, readbackBuffer_, 1, &region);

// NOTE: Can't read the buffer using the CPU here - need to sync first.

// If we copied from the backbuffer, transition it back.
if (step.readback.src == nullptr) {
// We only take screenshots after the main render pass (anything else would be stupid) so we need to transition out of PRESENT,
// and then back into it.
TransitionImageLayout2(cmd, backbufferImage_, VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_ACCESS_TRANSFER_READ_BIT, 0);
copyLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
}
}

void VulkanQueueRunner::PerformReadbackImage(const VKRStep &step, VkCommandBuffer cmd) {
Expand Down Expand Up @@ -911,6 +941,8 @@ void VulkanQueueRunner::CopyReadbackBuffer(int width, int height, Draw::DataForm
assert(res == VK_SUCCESS);
if (srcFormat == Draw::DataFormat::R8G8B8A8_UNORM) {
ConvertFromRGBA8888(pixels, (const uint8_t *)mappedData, pixelStride, width, width, height, destFormat);
} else if (srcFormat == Draw::DataFormat::B8G8R8A8_UNORM) {
ConvertFromBGRA8888(pixels, (const uint8_t *)mappedData, pixelStride, width, width, height, destFormat);
} else if (srcFormat == destFormat) {
uint8_t *dst = pixels;
const uint8_t *src = (const uint8_t *)mappedData;
Expand Down
4 changes: 3 additions & 1 deletion ext/native/thin3d/VulkanQueueRunner.h
Expand Up @@ -145,8 +145,9 @@ struct VKRStep {
class VulkanQueueRunner {
public:
VulkanQueueRunner(VulkanContext *vulkan) : vulkan_(vulkan) {}
void SetBackbuffer(VkFramebuffer fb) {
void SetBackbuffer(VkFramebuffer fb, VkImage img) {
backbuffer_ = fb;
backbufferImage_ = img;
}
void RunSteps(VkCommandBuffer cmd, const std::vector<VKRStep *> &steps);
void LogSteps(const std::vector<VKRStep *> &steps);
Expand Down Expand Up @@ -192,6 +193,7 @@ class VulkanQueueRunner {
VulkanContext *vulkan_;

VkFramebuffer backbuffer_;
VkImage backbufferImage_;
VkFramebuffer curFramebuffer_ = VK_NULL_HANDLE;

VkRenderPass backbufferRenderPass_ = VK_NULL_HANDLE;
Expand Down
28 changes: 23 additions & 5 deletions ext/native/thin3d/VulkanRenderManager.cpp
Expand Up @@ -413,7 +413,7 @@ void VulkanRenderManager::BindFramebufferAsRenderTarget(VKRFramebuffer *fb, VKRR
curHeight_ = fb ? fb->height : vulkan_->GetBackbufferHeight();
}

void VulkanRenderManager::CopyFramebufferToMemorySync(VKRFramebuffer *src, int aspectBits, int x, int y, int w, int h, Draw::DataFormat destFormat, uint8_t *pixels, int pixelStride) {
bool VulkanRenderManager::CopyFramebufferToMemorySync(VKRFramebuffer *src, int aspectBits, int x, int y, int w, int h, Draw::DataFormat destFormat, uint8_t *pixels, int pixelStride) {
VKRStep *step = new VKRStep{ VKRStepType::READBACK };
step->readback.aspectMask = aspectBits;
step->readback.src = src;
Expand All @@ -427,9 +427,26 @@ void VulkanRenderManager::CopyFramebufferToMemorySync(VKRFramebuffer *src, int a

Draw::DataFormat srcFormat;
if (aspectBits & VK_IMAGE_ASPECT_COLOR_BIT) {
switch (src->color.format) {
case VK_FORMAT_R8G8B8A8_UNORM: srcFormat = Draw::DataFormat::R8G8B8A8_UNORM; break;
default: assert(false);
if (src) {
switch (src->color.format) {
case VK_FORMAT_R8G8B8A8_UNORM: srcFormat = Draw::DataFormat::R8G8B8A8_UNORM; break;
default: assert(false);
}
}
else {
// Backbuffer.
if (!(vulkan_->GetSurfaceCapabilities().supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT)) {
ELOG("Copying from backbuffer not supported, can't take screenshots");
return false;
}
switch (vulkan_->GetSwapchainFormat()) {
case VK_FORMAT_B8G8R8A8_UNORM: srcFormat = Draw::DataFormat::B8G8R8A8_UNORM; break;
case VK_FORMAT_R8G8B8A8_UNORM: srcFormat = Draw::DataFormat::R8G8B8A8_UNORM; break;
// NOTE: If you add supported formats here, make sure to also support them in VulkanQueueRunner::CopyReadbackBuffer.
default:
ELOG("Unsupported backbuffer format for screenshots");
return false;
}
}
} else if (aspectBits & VK_IMAGE_ASPECT_STENCIL_BIT) {
// Copies from stencil are always S8.
Expand All @@ -446,6 +463,7 @@ void VulkanRenderManager::CopyFramebufferToMemorySync(VKRFramebuffer *src, int a
}
// Need to call this after FlushSync so the pixels are guaranteed to be ready in CPU-accessible VRAM.
queueRunner_.CopyReadbackBuffer(w, h, srcFormat, destFormat, pixelStride, pixels);
return true;
}

void VulkanRenderManager::CopyImageToMemorySync(VkImage image, int mipLevel, int x, int y, int w, int h, Draw::DataFormat destFormat, uint8_t *pixels, int pixelStride) {
Expand Down Expand Up @@ -724,7 +742,7 @@ void VulkanRenderManager::BeginSubmitFrame(int frame) {

assert(res == VK_SUCCESS);

queueRunner_.SetBackbuffer(framebuffers_[frameData.curSwapchainImage]);
queueRunner_.SetBackbuffer(framebuffers_[frameData.curSwapchainImage], swapchainImages_[frameData.curSwapchainImage].image);

frameData.hasBegun = true;
}
Expand Down
2 changes: 1 addition & 1 deletion ext/native/thin3d/VulkanRenderManager.h
Expand Up @@ -94,7 +94,7 @@ class VulkanRenderManager {

void BindFramebufferAsRenderTarget(VKRFramebuffer *fb, VKRRenderPassAction color, VKRRenderPassAction depth, uint32_t clearColor, float clearDepth, uint8_t clearStencil);
VkImageView BindFramebufferAsTexture(VKRFramebuffer *fb, int binding, int aspectBit, int attachment);
void CopyFramebufferToMemorySync(VKRFramebuffer *src, int aspectBits, int x, int y, int w, int h, Draw::DataFormat destFormat, uint8_t *pixels, int pixelStride);
bool CopyFramebufferToMemorySync(VKRFramebuffer *src, int aspectBits, int x, int y, int w, int h, Draw::DataFormat destFormat, uint8_t *pixels, int pixelStride);
void CopyImageToMemorySync(VkImage image, int mipLevel, int x, int y, int w, int h, Draw::DataFormat destFormat, uint8_t *pixels, int pixelStride);

void CopyFramebuffer(VKRFramebuffer *src, VkRect2D srcRect, VKRFramebuffer *dst, VkOffset2D dstPos, int aspectMask);
Expand Down
25 changes: 25 additions & 0 deletions ext/native/thin3d/thin3d.cpp
Expand Up @@ -381,6 +381,31 @@ void ConvertFromRGBA8888(uint8_t *dst, const uint8_t *src, uint32_t dstStride, u
}
}

// TODO: SSE/NEON
// Could also make C fake-simd for 64-bit, two 8888 pixels fit in a register :)
void ConvertFromBGRA8888(uint8_t *dst, const uint8_t *src, uint32_t dstStride, uint32_t srcStride, uint32_t width, uint32_t height, DataFormat format) {
// Must skip stride in the cases below. Some games pack data into the cracks, like MotoGP.
const uint32_t *src32 = (const uint32_t *)src;

if (format == Draw::DataFormat::R8G8B8A8_UNORM) {
uint32_t *dst32 = (uint32_t *)dst;
if (src == dst) {
return;
}
else {
for (uint32_t y = 0; y < height; ++y) {
ConvertBGRA8888ToRGBA8888(dst32, src32, width);
memcpy(dst32, src32, width * 4);
src32 += srcStride;
dst32 += dstStride;
}
}
}
else {
// Don't even bother with these, this path only happens in screenshots and we don't save those to 16-bit.
assert(false);
}
}
void ConvertToD32F(uint8_t *dst, const uint8_t *src, uint32_t dstStride, uint32_t srcStride, uint32_t width, uint32_t height, DataFormat format) {
if (format == Draw::DataFormat::D32F) {
const float *src32 = (const float *)src;
Expand Down
3 changes: 1 addition & 2 deletions ext/native/thin3d/thin3d_vulkan.cpp
Expand Up @@ -1337,8 +1337,7 @@ bool VKContext::CopyFramebufferToMemorySync(Framebuffer *srcfb, int channelBits,
if (channelBits & FBChannel::FB_DEPTH_BIT) aspectMask |= VK_IMAGE_ASPECT_DEPTH_BIT;
if (channelBits & FBChannel::FB_STENCIL_BIT) aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;

renderManager_.CopyFramebufferToMemorySync(src->GetFB(), aspectMask, x, y, w, h, format, (uint8_t *)pixels, pixelStride);
return true;
return renderManager_.CopyFramebufferToMemorySync(src ? src->GetFB() : nullptr, aspectMask, x, y, w, h, format, (uint8_t *)pixels, pixelStride);
}

void VKContext::BindFramebufferAsRenderTarget(Framebuffer *fbo, const RenderPassInfo &rp) {
Expand Down

0 comments on commit 143a199

Please sign in to comment.