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:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-25 09:06:56 -06:00
1 parent a50fb6071f
commit 0b778d6b83
2 files changed
+143 -18

No files matched your search

+133 -18
View File
@@ -55,6 +55,7 @@
#include "Core/FrameTiming.h" #include "Core/FrameTiming.h"
#include "Common/GPU/Vulkan/VulkanLoader.h" #include "Common/GPU/Vulkan/VulkanLoader.h"
#include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/GPU/Vulkan/VulkanContext.h"
#include "Common/GPU/Vulkan/VulkanPresentation.h"
#include "Common/GPU/thin3d.h" #include "Common/GPU/thin3d.h"
#include "Common/GPU/thin3d_create.h" #include "Common/GPU/thin3d_create.h"
@@ -70,6 +71,94 @@ static const bool g_validate_ = true;
static const bool g_validate_ = false; static const bool g_validate_ = false;
#endif #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) { bool VulkanGraphicsContext::InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) {
*errorMessage = "N/A"; *errorMessage = "N/A";
_dbg_assert_(deviceName); _dbg_assert_(deviceName);
@@ -116,20 +205,35 @@ bool VulkanGraphicsContext::InitAPI(void *wnd, std::string *deviceName, std::str
vulkan_ = nullptr; vulkan_ = nullptr;
return false; 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; return true;
} }
bool VulkanGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) { 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, if (offscreenWidth_ > 0) {
// so everything below would just fail in more confusing ways further down (it used to assert deep auto presentation = std::make_unique<VulkanOffscreenPresentation>(VK_FORMAT_B8G8R8A8_UNORM, VkExtent2D{ (uint32_t)offscreenWidth_, (uint32_t)offscreenHeight_ });
// inside the thin3d context constructor). Let the caller fall back to another backend instead. if (!presentation->Create(vulkan_)) {
VkResult res = vulkan_->InitSurface(winsys, data1, data2); presentation->Destroy(vulkan_);
if (res != VK_SUCCESS) { *errorMessage = "Failed to create the offscreen images";
*errorMessage = vulkan_->InitError(); return false;
if (errorMessage->empty()) { }
*errorMessage = StringFromFormat("Failed to initialize Vulkan surface: %s", VulkanResultToString(res)); 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; bool useMultiThreading = g_Config.bRenderMultiThreading;
@@ -139,17 +243,19 @@ bool VulkanGraphicsContext::InitSurface(WindowSystem winsys, void *data1, void *
draw_ = Draw::T3DCreateVulkanContext(vulkan_, useMultiThreading); draw_ = Draw::T3DCreateVulkanContext(vulkan_, useMultiThreading);
VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_); if (offscreenWidth_ == 0) {
VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_);
#ifdef VK_EXT_full_screen_exclusive #ifdef VK_EXT_full_screen_exclusive
vulkan_->SetFullScreenExclusiveMode(g_Config.bFullScreen && g_Config.bAllowFullScreenExclusive vulkan_->SetFullScreenExclusiveMode(g_Config.bFullScreen && g_Config.bAllowFullScreenExclusive
? VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT ? VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT
: VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT); : VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT);
#endif #endif
if (!vulkan_->InitSwapchain(presentMode)) { if (!vulkan_->InitSwapchain(presentMode)) {
*errorMessage = vulkan_->InitError(); *errorMessage = vulkan_->InitError();
return false; return false;
}
} }
SetGPUBackend(GPUBackend::VULKAN, vulkan_->GetPhysicalDeviceProperties().properties.deviceName); SetGPUBackend(GPUBackend::VULKAN, vulkan_->GetPhysicalDeviceProperties().properties.deviceName);
@@ -176,8 +282,13 @@ void VulkanGraphicsContext::ShutdownSurface() {
draw_ = nullptr; draw_ = nullptr;
vulkan_->WaitUntilQueueIdle(); vulkan_->WaitUntilQueueIdle();
vulkan_->DestroySwapchain(); if (VulkanPresentation *presentation = vulkan_->GetPresentation()) {
vulkan_->DestroySurface(); presentation->Destroy(vulkan_);
vulkan_->SetPresentation(nullptr);
} else {
vulkan_->DestroySwapchain();
vulkan_->DestroySurface();
}
} }
void VulkanGraphicsContext::ShutdownAPI() { void VulkanGraphicsContext::ShutdownAPI() {
@@ -192,6 +303,10 @@ void VulkanGraphicsContext::ShutdownAPI() {
} }
void VulkanGraphicsContext::Resize() { void VulkanGraphicsContext::Resize() {
if (offscreenWidth_ > 0) {
// The images have a fixed size.
return;
}
draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, vulkan_->GetBackbufferWidth(), vulkan_->GetBackbufferHeight()); draw_->HandleEvent(Draw::Event::LOST_BACKBUFFER, vulkan_->GetBackbufferWidth(), vulkan_->GetBackbufferHeight());
VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_); VkPresentModeKHR presentMode = ConfigPresentModeToVulkan(draw_);
+10
View File
@@ -26,6 +26,14 @@ class VulkanRenderManager;
class VulkanGraphicsContext : public GraphicsContext { class VulkanGraphicsContext : public GraphicsContext {
public: public:
VulkanGraphicsContext() : draw_(nullptr) {} 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 InitAPI(void *wnd, std::string *deviceName, std::string *errorMessage) override;
bool InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) override; bool InitSurface(WindowSystem winsys, void *data1, void *data2, std::string *errorMessage) override;
void ShutdownSurface() override; void ShutdownSurface() override;
@@ -45,4 +53,6 @@ private:
VulkanContext *vulkan_ = nullptr; VulkanContext *vulkan_ = nullptr;
VulkanRenderManager *renderManager_ = nullptr; VulkanRenderManager *renderManager_ = nullptr;
bool windowRestored_ = false; bool windowRestored_ = false;
int offscreenWidth_ = 0;
int offscreenHeight_ = 0;
}; };