mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
Vulkan: Add an offscreen mode to VulkanGraphicsContext
For when there's nothing to present to. Instead of a surface and swapchain, it renders into images of its own through the VulkanPresentation interface libretro uses, picking the graphics queue without a surface. Acquiring and presenting signal and wait on the frame's semaphores with empty submits. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
a50fb6071f
commit
0b778d6b83
2 files changed
+143
-18
No files matched your search
@@ -55,6 +55,7 @@
|
||||
#include "Core/FrameTiming.h"
|
||||
#include "Common/GPU/Vulkan/VulkanLoader.h"
|
||||
#include "Common/GPU/Vulkan/VulkanContext.h"
|
||||
#include "Common/GPU/Vulkan/VulkanPresentation.h"
|
||||
|
||||
#include "Common/GPU/thin3d.h"
|
||||
#include "Common/GPU/thin3d_create.h"
|
||||
@@ -70,6 +71,94 @@ static const bool g_validate_ = true;
|
||||
static const bool g_validate_ = false;
|
||||
#endif
|
||||
|
||||
using namespace PPSSPP_VK;
|
||||
|
||||
// Stands in for the swapchain in offscreen mode: a few images that frames are rendered into and left
|
||||
// in. Acquiring and presenting only signal and wait on the frame's semaphores, with empty submits, so
|
||||
// the rest of the frame synchronization works as with a real swapchain.
|
||||
class VulkanOffscreenPresentation : public VulkanPresentation {
|
||||
public:
|
||||
VulkanOffscreenPresentation(VkFormat format, VkExtent2D extent) : format_(format), extent_(extent) {}
|
||||
|
||||
bool Create(VulkanContext *vulkan) {
|
||||
VkDevice device = vulkan->GetDevice();
|
||||
for (Image &img : images_) {
|
||||
VkImageCreateInfo info{ VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO };
|
||||
info.imageType = VK_IMAGE_TYPE_2D;
|
||||
info.format = format_;
|
||||
info.extent = { extent_.width, extent_.height, 1 };
|
||||
info.mipLevels = 1;
|
||||
info.arrayLayers = 1;
|
||||
info.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
info.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
info.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
if (vkCreateImage(device, &info, nullptr, &img.image) != VK_SUCCESS)
|
||||
return false;
|
||||
|
||||
VkMemoryRequirements memreq;
|
||||
vkGetImageMemoryRequirements(device, img.image, &memreq);
|
||||
VkMemoryAllocateInfo alloc{ VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO };
|
||||
alloc.allocationSize = memreq.size;
|
||||
if (!vulkan->MemoryTypeFromProperties(memreq.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, &alloc.memoryTypeIndex))
|
||||
return false;
|
||||
if (vkAllocateMemory(device, &alloc, nullptr, &img.memory) != VK_SUCCESS)
|
||||
return false;
|
||||
if (vkBindImageMemory(device, img.image, img.memory, 0) != VK_SUCCESS)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Destroy(VulkanContext *vulkan) override {
|
||||
VkDevice device = vulkan->GetDevice();
|
||||
for (Image &img : images_) {
|
||||
if (img.image)
|
||||
vkDestroyImage(device, img.image, nullptr);
|
||||
if (img.memory)
|
||||
vkFreeMemory(device, img.memory, nullptr);
|
||||
img = {};
|
||||
}
|
||||
}
|
||||
|
||||
VkResult AcquireNextImage(VulkanContext *vulkan, VkSemaphore signalSemaphore, uint32_t *imageIndex) override {
|
||||
// The queue runs in order, so an image is free again by the time later work reaches it.
|
||||
*imageIndex = next_;
|
||||
next_ = (next_ + 1) % IMAGE_COUNT;
|
||||
VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO };
|
||||
submit.signalSemaphoreCount = 1;
|
||||
submit.pSignalSemaphores = &signalSemaphore;
|
||||
return vkQueueSubmit(vulkan->GetGraphicsQueue(), 1, &submit, VK_NULL_HANDLE);
|
||||
}
|
||||
|
||||
VkResult QueuePresent(VulkanContext *vulkan, VkQueue queue, uint32_t imageIndex, VkSemaphore waitSemaphore) override {
|
||||
// Nothing to show it on, so just consume the semaphore.
|
||||
VkPipelineStageFlags stage = VK_PIPELINE_STAGE_ALL_COMMANDS_BIT;
|
||||
VkSubmitInfo submit{ VK_STRUCTURE_TYPE_SUBMIT_INFO };
|
||||
submit.waitSemaphoreCount = 1;
|
||||
submit.pWaitSemaphores = &waitSemaphore;
|
||||
submit.pWaitDstStageMask = &stage;
|
||||
return vkQueueSubmit(queue, 1, &submit, VK_NULL_HANDLE);
|
||||
}
|
||||
|
||||
uint32_t GetImageCount() const override { return IMAGE_COUNT; }
|
||||
VkImage GetImage(uint32_t index) const override { return images_[index].image; }
|
||||
VkExtent2D GetExtent() const override { return extent_; }
|
||||
VkFormat GetFormat() const override { return format_; }
|
||||
VkImageLayout GetPresentLayout() const override { return VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; }
|
||||
|
||||
private:
|
||||
static constexpr int IMAGE_COUNT = 2;
|
||||
struct Image {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
};
|
||||
Image images_[IMAGE_COUNT];
|
||||
VkFormat format_;
|
||||
VkExtent2D extent_;
|
||||
uint32_t next_ = 0;
|
||||
};
|
||||
|
||||
bool VulkanGraphicsContext::InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) {
|
||||
*errorMessage = "N/A";
|
||||
_dbg_assert_(deviceName);
|
||||
@@ -116,20 +205,35 @@ bool VulkanGraphicsContext::InitAPI(void *wnd, std::string *deviceName, std::str
|
||||
vulkan_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
// Normally the queue is picked along with the surface, which must be able to present from it.
|
||||
if (offscreenWidth_ > 0 && !vulkan_->ChooseGraphicsQueueWithoutSurface()) {
|
||||
*errorMessage = "No graphics queue";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VulkanGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) {
|
||||
// Don't proceed on failure - without a surface there's no present mode, no swapchain and no queue,
|
||||
// so everything below would just fail in more confusing ways further down (it used to assert deep
|
||||
// inside the thin3d context constructor). Let the caller fall back to another backend instead.
|
||||
VkResult res = vulkan_->InitSurface(winsys, data1, data2);
|
||||
if (res != VK_SUCCESS) {
|
||||
*errorMessage = vulkan_->InitError();
|
||||
if (errorMessage->empty()) {
|
||||
*errorMessage = StringFromFormat("Failed to initialize Vulkan surface: %s", VulkanResultToString(res));
|
||||
if (offscreenWidth_ > 0) {
|
||||
auto presentation = std::make_unique<VulkanOffscreenPresentation>(VK_FORMAT_B8G8R8A8_UNORM, VkExtent2D{ (uint32_t)offscreenWidth_, (uint32_t)offscreenHeight_ });
|
||||
if (!presentation->Create(vulkan_)) {
|
||||
presentation->Destroy(vulkan_);
|
||||
*errorMessage = "Failed to create the offscreen images";
|
||||
return false;
|
||||
}
|
||||
vulkan_->SetPresentation(std::move(presentation));
|
||||
} else {
|
||||
// Don't proceed on failure - without a surface there's no present mode, no swapchain and no queue,
|
||||
// so everything below would just fail in more confusing ways further down (it used to assert deep
|
||||
// inside the thin3d context constructor). Let the caller fall back to another backend instead.
|
||||
VkResult res = vulkan_->InitSurface(winsys, data1, data2);
|
||||
if (res != VK_SUCCESS) {
|
||||
*errorMessage = vulkan_->InitError();
|
||||
if (errorMessage->empty()) {
|
||||
*errorMessage = StringFromFormat("Failed to initialize Vulkan surface: %s", VulkanResultToString(res));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool useMultiThreading = g_Config.bRenderMultiThreading;
|
||||
@@ -139,17 +243,19 @@ bool VulkanGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *
|
||||
|
||||
draw_ = Draw::T3DCreateVulkanContext(vulkan_, useMultiThreading);
|
||||
|
||||
VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_);
|
||||
if (offscreenWidth_ == 0) {
|
||||
VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_);
|
||||
|
||||
#ifdef VK_EXT_full_screen_exclusive
|
||||
vulkan_->SetFullScreenExclusiveMode(g_Config.bFullScreen && g_Config.bAllowFullScreenExclusive
|
||||
? VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT
|
||||
: VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT);
|
||||
vulkan_->SetFullScreenExclusiveMode(g_Config.bFullScreen && g_Config.bAllowFullScreenExclusive
|
||||
? VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT
|
||||
: VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT);
|
||||
#endif
|
||||
|
||||
if (!vulkan_->InitSwapchain(presentMode)) {
|
||||
*errorMessage = vulkan_->InitError();
|
||||
return false;
|
||||
if (!vulkan_->InitSwapchain(presentMode)) {
|
||||
*errorMessage = vulkan_->InitError();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SetGPUBackend(GPUBackend::VULKAN, vulkan_->GetPhysicalDeviceProperties().properties.deviceName);
|
||||
@@ -176,8 +282,13 @@ void VulkanGraphicsContext::ShutdownSurface() {
|
||||
draw_ = nullptr;
|
||||
|
||||
vulkan_->WaitUntilQueueIdle();
|
||||
vulkan_->DestroySwapchain();
|
||||
vulkan_->DestroySurface();
|
||||
if (VulkanPresentation *presentation = vulkan_->GetPresentation()) {
|
||||
presentation->Destroy(vulkan_);
|
||||
vulkan_->SetPresentation(nullptr);
|
||||
} else {
|
||||
vulkan_->DestroySwapchain();
|
||||
vulkan_->DestroySurface();
|
||||
}
|
||||
}
|
||||
|
||||
void VulkanGraphicsContext::ShutdownAPI() {
|
||||
@@ -192,6 +303,10 @@ void VulkanGraphicsContext::ShutdownAPI() {
|
||||
}
|
||||
|
||||
void VulkanGraphicsContext::Resize() {
|
||||
if (offscreenWidth_ > 0) {
|
||||
// The images have a fixed size.
|
||||
return;
|
||||
}
|
||||
draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, vulkan_->GetBackbufferWidth(), vulkan_->GetBackbufferHeight());
|
||||
VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_);
|
||||
|
||||
|
||||
@@ -26,6 +26,14 @@ class VulkanRenderManager;
|
||||
class VulkanGraphicsContext : public GraphicsContext {
|
||||
public:
|
||||
VulkanGraphicsContext() : draw_(nullptr) {}
|
||||
|
||||
// Renders into images of this size instead of a window's swapchain, for when there's nothing to
|
||||
// present to (headless). Call before InitAPI. InitSurface then ignores the window system.
|
||||
void SetOffscreen(int width, int height) {
|
||||
offscreenWidth_ = width;
|
||||
offscreenHeight_ = height;
|
||||
}
|
||||
|
||||
bool InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) override;
|
||||
bool InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) override;
|
||||
void ShutdownSurface() override;
|
||||
@@ -45,4 +53,6 @@ private:
|
||||
VulkanContext *vulkan_ = nullptr;
|
||||
VulkanRenderManager *renderManager_ = nullptr;
|
||||
bool windowRestored_ = false;
|
||||
int offscreenWidth_ = 0;
|
||||
int offscreenHeight_ = 0;
|
||||
};
|
||||
Reference in new issue
Block a user