Add Vulkan + Wayland init logic for OpenXR (#7767)

* Add Vulkan + Wayland init logic for OpenXR

* typo

* clang tidy
This commit is contained in:
BMagnu authored and GitHub committed 2026-09-08 10:48:33 -04:00
1 parent 3ac8be273e
commit a73e2565c8
11 files changed
+929 -74

No files matched your search

+47 -36
View File
@@ -99,54 +99,65 @@ bool gr_opengl_openxr_create_session() {
}
#elif defined SCP_UNIX
bool gr_opengl_openxr_create_session() {
std::variant<std::monostate, XrGraphicsBindingOpenGLXlibKHR> drawingSurface{};
// for X11 version
// if !SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11")
auto xdisplay = reinterpret_cast<Display *>(SDL_GetPointerProperty(SDL_GetWindowProperties(os::getSDLMainWindow()),
SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
nullptr));
auto xwindow = static_cast<Window>(SDL_GetNumberProperty(SDL_GetWindowProperties(os::getSDLMainWindow()),
SDL_PROP_WINDOW_X11_WINDOW_NUMBER,
0));
if (!SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11")) {
auto xdisplay = reinterpret_cast<Display *>(SDL_GetPointerProperty(SDL_GetWindowProperties(os::getSDLMainWindow()),
SDL_PROP_WINDOW_X11_DISPLAY_POINTER,
nullptr));
auto xwindow = static_cast<Window>(SDL_GetNumberProperty(SDL_GetWindowProperties(os::getSDLMainWindow()),
SDL_PROP_WINDOW_X11_WINDOW_NUMBER,
0));
// TODO: a wayland version
// if !SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland")
// auto display = reinterpret_cast<struct wl_display *>(SDL_GetPointerProperty(SDL_GetWindowProperties(os::getSDLMainWindow()),
// SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER,
// nullptr));
// auto surface = reinterpret_cast<struct wl_surface *>(SDL_GetPointerProperty(SDL_GetWindowProperties(os::getSDLMainWindow()),
// SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER,
// nullptr));
XWindowAttributes wa;
XGetWindowAttributes(xdisplay, xwindow, &wa);
XWindowAttributes wa;
XGetWindowAttributes(xdisplay, xwindow, &wa);
auto glxcontext = (GLXContext) SDL_GL_GetCurrentContext(); //uuuuuugly, and not technically allowed by the standard, but this "opaque" SDL_GLContext type is just the GLXContext on X11
GLXContext glxcontext = (GLXContext) SDL_GL_GetCurrentContext(); //uuuuuugly, and not technically allowed by the standard, but this "opaque" SDL_GLContext type is just the GLXContext on X11
int glxfbconfigid, glxscreenid, nfbconfigs;
glXQueryContext(xdisplay, glxcontext, GLX_FBCONFIG_ID, &glxfbconfigid);
glXQueryContext(xdisplay, glxcontext, GLX_SCREEN, &glxscreenid);
int glxfbconfigid, glxscreenid, nfbconfigs;
glXQueryContext(xdisplay, glxcontext, GLX_FBCONFIG_ID, &glxfbconfigid);
glXQueryContext(xdisplay, glxcontext, GLX_SCREEN, &glxscreenid);
const int fbconfigattrs[] = {GLX_FBCONFIG_ID, glxfbconfigid, None};
GLXFBConfig* fbconfigs = glXChooseFBConfig(xdisplay, glxscreenid, fbconfigattrs, &nfbconfigs);
const int fbconfigattrs[] = {GLX_FBCONFIG_ID, glxfbconfigid, None};
GLXFBConfig* fbconfigs = glXChooseFBConfig(xdisplay, glxscreenid, fbconfigattrs, &nfbconfigs);
if (nfbconfigs < 1) {
mprintf(("Unable to find Linux FBConfig for OpenXR!\n"));
return false;
}
if (nfbconfigs < 1) {
mprintf(("Unable to find Linux FBConfig for OpenXR!\n"));
drawingSurface = XrGraphicsBindingOpenGLXlibKHR {
XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR,
nullptr,
xdisplay,
static_cast<uint32_t>(XVisualIDFromVisual(wa.visual)),
fbconfigs[0],
glXGetCurrentDrawable(),
glxcontext
};
}
else if (!SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland")) {
//Unfortunately, XrGraphicsBindingOpenGLWaylandKHR is deprecated and effectively not implemented anywhere, so we are limited to X11 on OpenGL
Error("VR with OpenGL cannot use wayland! Use X11 (SDL_VIDEO_DRIVER=x11) or the Vulkan renderer.");
return false;
}
else {
Error("Unsupported video driver for OpenXR on Linux! Must be X11 or wayland!");
return false;
}
XrGraphicsBindingOpenGLXlibKHR graphicsBinding {
XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR,
nullptr,
xdisplay,
static_cast<uint32_t>(XVisualIDFromVisual(wa.visual)),
fbconfigs[0],
glXGetCurrentDrawable(),
glxcontext
};
XrSessionCreateInfo sessionCreateInfo {
XR_TYPE_SESSION_CREATE_INFO,
&graphicsBinding,
std::visit([] (auto& surface) -> void* {
if constexpr(std::is_same_v<std::decay_t<decltype(surface)>, std::monostate>) {
Assertion(false, "No drawing surface found!");
return nullptr;
}
else {
return &surface;
}
}, drawingSurface),
0,
xr_system
};
+5 -1
View File
@@ -6,6 +6,7 @@
#include "mod_table/mod_table.h"
#include "render/3d.h"
#include "starfield/starfield.h"
#include "hud/hudparse.h"
std::unique_ptr<star[]> Stars_XRBuffer;
@@ -184,7 +185,10 @@ static bool openxr_init_swapchains() {
XR_TYPE_SWAPCHAIN_CREATE_INFO,
nullptr,
0,
XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT,
//Vulkan runtimes hand back images they expect to receive again in
//VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, which requires the color
//attachment usage. Inert for OpenGL.
XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT | XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT,
chosenFormat,
1,
xr_configurationviews[i].recommendedImageRectWidth,
+605
View File
@@ -0,0 +1,605 @@
#include "VulkanOpenXR.h"
#include "VulkanBarrier.h"
#include "VulkanRenderer.h"
#include "gr_vulkan.h"
#include "graphics/2d.h"
#include "graphics/openxr.h"
#ifdef FS_OPENXR
#include "io/cursor.h"
#include "io/mouse.h"
#include <SDL3/SDL_vulkan.h>
#define XR_USE_GRAPHICS_API_VULKAN
#include "graphics/openxr_internal.h"
#endif
namespace graphics::vulkan {
namespace {
#ifdef FS_OPENXR
// Set while creating the Vulkan instance (the only point at which the runtime's
// graphics requirements can be checked, since they must be queried *before*
// instance creation) and read back later by test_capabilities().
bool Xr_graphics_requirements_met = false;
std::array<SCP_vector<XrSwapchainImageVulkan2KHR>, 2> Xr_swapchain_images;
// Which eyes hold an acquired swapchain image for the frame currently being
// built. A stereo frame spans two gr_flip() calls, so the left eye's
// acquisition has to survive until the right eye's call submits and releases
// both -- the release cannot happen before the work is submitted.
std::array<bool, 2> Xr_eye_acquired = {false, false};
/**
* @brief Copy the frame the engine just rendered into one eye's swapchain image
*
* The source is the renderer's composition image, which is the Vulkan
* equivalent of the backbuffer the OpenGL path blits from: the whole frame --
* 3D scene, HUD, menus -- has landed there by the time gr_flip() runs. Note
* that it is sampled before the output-encode pass, so the user's gamma slider
* (Gr_gamma) applies to the desktop mirror but not to the HMD image.
*
* Must be called with no render pass active on the frame command buffer.
*/
void blit_to_swapchain_image(VulkanRenderer* renderer, vk::Image dst, uint32_t dstWidth, uint32_t dstHeight)
{
const vk::CommandBuffer cmd = renderer->getCurrentCommandBuffer();
const vk::Image composition = renderer->getCurrentCompositionImage();
const vk::Extent2D srcExtent = renderer->getRenderExtent();
if (!cmd || !composition || !dst) {
return;
}
// The composition image is left in eShaderReadOnlyOptimal by the render
// pass that just ended, and must be put back that way afterwards: it is
// what both m_renderPassLoad's initialLayout and the output-encode pass
// expect to find.
std::array<ImageBarrier2, 2> preBarriers;
preBarriers[0].image = composition;
preBarriers[0].oldLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
preBarriers[0].newLayout = vk::ImageLayout::eTransferSrcOptimal;
preBarriers[0].srcStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput;
preBarriers[0].srcAccess = vk::AccessFlagBits2::eColorAttachmentWrite;
preBarriers[0].dstStage = vk::PipelineStageFlagBits2::eBlit;
preBarriers[0].dstAccess = vk::AccessFlagBits2::eTransferRead;
// eUndefined discards whatever the runtime last left in the image, which is
// exactly right: the blit overwrites every pixel of it.
preBarriers[1].image = dst;
preBarriers[1].oldLayout = vk::ImageLayout::eUndefined;
preBarriers[1].newLayout = vk::ImageLayout::eTransferDstOptimal;
preBarriers[1].srcStage = vk::PipelineStageFlagBits2::eNone;
preBarriers[1].srcAccess = {};
preBarriers[1].dstStage = vk::PipelineStageFlagBits2::eBlit;
preBarriers[1].dstAccess = vk::AccessFlagBits2::eTransferWrite;
cmdImageBarriers(cmd, ArrayView<const ImageBarrier2>(preBarriers.data(), preBarriers.size()));
vk::ImageBlit blit;
blit.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
blit.srcSubresource.mipLevel = 0;
blit.srcSubresource.baseArrayLayer = 0;
blit.srcSubresource.layerCount = 1;
blit.srcOffsets[0] = vk::Offset3D(0, 0, 0);
blit.srcOffsets[1] = vk::Offset3D(static_cast<int32_t>(srcExtent.width), static_cast<int32_t>(srcExtent.height), 1);
blit.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
blit.dstSubresource.mipLevel = 0;
blit.dstSubresource.baseArrayLayer = 0;
blit.dstSubresource.layerCount = 1;
blit.dstOffsets[0] = vk::Offset3D(0, 0, 0);
blit.dstOffsets[1] = vk::Offset3D(static_cast<int32_t>(dstWidth), static_cast<int32_t>(dstHeight), 1);
// eLinear because the eye resolution rarely matches the window resolution,
// matching the OpenGL path's GL_LINEAR blit into the swapchain.
cmd.blitImage(composition,
vk::ImageLayout::eTransferSrcOptimal,
dst,
vk::ImageLayout::eTransferDstOptimal,
1,
&blit,
vk::Filter::eLinear);
std::array<ImageBarrier2, 2> postBarriers;
// Runtimes expect a released swapchain image in eColorAttachmentOptimal.
postBarriers[0].image = dst;
postBarriers[0].oldLayout = vk::ImageLayout::eTransferDstOptimal;
postBarriers[0].newLayout = vk::ImageLayout::eColorAttachmentOptimal;
postBarriers[0].srcStage = vk::PipelineStageFlagBits2::eBlit;
postBarriers[0].srcAccess = vk::AccessFlagBits2::eTransferWrite;
postBarriers[0].dstStage = vk::PipelineStageFlagBits2::eColorAttachmentOutput;
postBarriers[0].dstAccess = vk::AccessFlagBits2::eColorAttachmentWrite;
postBarriers[1].image = composition;
postBarriers[1].oldLayout = vk::ImageLayout::eTransferSrcOptimal;
postBarriers[1].newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
postBarriers[1].srcStage = vk::PipelineStageFlagBits2::eBlit;
postBarriers[1].srcAccess = vk::AccessFlagBits2::eTransferRead;
postBarriers[1].dstStage = vk::PipelineStageFlagBits2::eFragmentShader;
postBarriers[1].dstAccess = vk::AccessFlagBits2::eShaderSampledRead;
cmdImageBarriers(cmd, ArrayView<const ImageBarrier2>(postBarriers.data(), postBarriers.size()));
}
/**
* @brief Acquire one eye's swapchain image and blit the current frame into it
*
* Ends the in-flight render pass, since a blit cannot be recorded inside one.
*/
bool render_eye(VulkanRenderer* renderer, uint32_t eye)
{
renderer->endCurrentRenderPass();
XrSwapchain swapchain = xr_swapchains[eye]->swapchain;
XrSwapchainImageAcquireInfo acquireImageInfo{
XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO,
nullptr
};
uint32_t activeIndex = 0;
if (xrAcquireSwapchainImage(swapchain, &acquireImageInfo, &activeIndex) != XR_SUCCESS) {
return false;
}
XrSwapchainImageWaitInfo waitImageInfo{
XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO,
nullptr,
std::numeric_limits<int64_t>::max()
};
xrWaitSwapchainImage(swapchain, &waitImageInfo);
Xr_eye_acquired[eye] = true;
blit_to_swapchain_image(renderer,
vk::Image(Xr_swapchain_images[eye][activeIndex].image),
xr_swapchains[eye]->width,
xr_swapchains[eye]->height);
return true;
}
void release_acquired_images()
{
for (uint32_t i = 0; i < 2; i++) {
if (!Xr_eye_acquired[i]) {
continue;
}
XrSwapchainImageReleaseInfo releaseImageInfo{
XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO,
nullptr
};
xrReleaseSwapchainImage(xr_swapchains[i]->swapchain, &releaseImageInfo);
Xr_eye_acquired[i] = false;
}
}
/**
* @brief Draw the mouse cursor into the frame
*
* The cursor is an OS cursor, so it is drawn by the compositor over the window
* and never appears in anything we render. On a flat screen inside the headset
* that would leave the player clicking blind, so it has to be stamped into the
* image by hand -- as the OpenGL path also does. Must run while a render pass
* is still active.
*/
void draw_cursor()
{
auto* cursorManager = io::mouse::CursorManager::get();
if (cursorManager == nullptr || !cursorManager->isCursorShown()) {
return;
}
auto* cursor = cursorManager->getCurrentCursor();
if (cursor == nullptr) {
return;
}
const int cursorBitmap = cursor->getBitmapHandle();
if (cursorBitmap < 0) {
return;
}
int mouseX, mouseY;
mouse_get_pos(&mouseX, &mouseY);
gr_set_bitmap(cursorBitmap);
gr_bitmap(mouseX, mouseY, GR_RESIZE_NONE);
}
/**
* @brief Submit the composed frame to both the HMD and the desktop mirror
*/
void end_frame(VulkanRenderer* renderer, bool stereo)
{
// Submit first: OpenXR requires every command that writes a swapchain image
// to have been submitted to the queue the session was created with before
// xrEndFrame is called. This also presents the desktop mirror and acquires
// the next swap chain image.
renderer->flip();
release_acquired_images();
XrCompositionLayerProjectionView projectedViews[2];
XrCompositionLayerProjection projectionLayer{};
XrCompositionLayerQuad quadLayer{};
const XrCompositionLayerBaseHeader* pLayer = nullptr;
if (stereo) {
for (uint32_t i = 0; i < 2; i++) {
projectedViews[i].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW;
projectedViews[i].next = nullptr;
projectedViews[i].pose = xr_views[i].pose;
projectedViews[i].fov = xr_views[i].fov;
projectedViews[i].subImage = {
xr_swapchains[i]->swapchain,
{
{ 0, 0 },
{ (int32_t)xr_swapchains[i]->width, (int32_t)xr_swapchains[i]->height }
},
0
};
}
projectionLayer.type = XR_TYPE_COMPOSITION_LAYER_PROJECTION;
projectionLayer.next = nullptr;
projectionLayer.layerFlags = 0;
projectionLayer.space = xr_space;
projectionLayer.viewCount = 2;
projectionLayer.views = projectedViews;
pLayer = (const XrCompositionLayerBaseHeader*)&projectionLayer;
}
else {
// A flat frame (menus, briefings, anything not rendered per-eye) becomes
// a quad layer rather than a projection layer, letting the runtime place
// and filter a single 2D image instead of us reprojecting it per eye.
// Position and size reproduce the proportions of the OpenGL path's quad:
// 4 units ahead of the origin, 4 units tall, widened by the aspect ratio.
// OpenXR's forward axis is -Z, FSO's is +Z.
quadLayer.type = XR_TYPE_COMPOSITION_LAYER_QUAD;
quadLayer.next = nullptr;
quadLayer.layerFlags = 0;
quadLayer.space = xr_space;
quadLayer.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
quadLayer.subImage = {
xr_swapchains[0]->swapchain,
{
{ 0, 0 },
{ (int32_t)xr_swapchains[0]->width, (int32_t)xr_swapchains[0]->height }
},
0
};
quadLayer.pose = XrPosef{ XrQuaternionf{ 0, 0, 0, 1 }, XrVector3f{ 0, 0, -4.0f } };
quadLayer.size = XrExtent2Df{ 4.0f * gr_screen.clip_aspect, 4.0f };
pLayer = (const XrCompositionLayerBaseHeader*)&quadLayer;
}
XrFrameEndInfo frameEndInfo{
XR_TYPE_FRAME_END_INFO,
nullptr,
xr_state.predictedDisplayTime,
XR_ENVIRONMENT_BLEND_MODE_OPAQUE,
0,
nullptr
};
if (xr_state.shouldRender) {
frameEndInfo.layerCount = 1;
frameEndInfo.layers = &pLayer;
}
xrEndFrame(xr_session, &frameEndInfo);
}
#endif
} // namespace
// SETUP FUNCTIONS VULKAN
#ifdef FS_OPENXR
SCP_vector<const char*> vulkan_openxr_get_extensions()
{
return { XR_KHR_VULKAN_ENABLE2_EXTENSION_NAME };
}
bool vulkan_openxr_test_capabilities()
{
// The real check happened back when the Vulkan instance was created -- the
// runtime requires its graphics requirements to be queried before that
// point, which is far earlier than this function is called.
return Xr_graphics_requirements_met;
}
bool vulkan_openxr_create_session()
{
auto* renderer = getRendererInstance();
XrGraphicsBindingVulkan2KHR graphicsBinding {
XR_TYPE_GRAPHICS_BINDING_VULKAN2_KHR,
nullptr,
renderer->getVkInstance(),
renderer->getPhysicalDevice(),
renderer->getDevice(),
renderer->getGraphicsQueueFamilyIndex(),
0
};
XrSessionCreateInfo sessionCreateInfo {
XR_TYPE_SESSION_CREATE_INFO,
&graphicsBinding,
0,
xr_system
};
XrResult sessionInit = xrCreateSession(xr_instance, &sessionCreateInfo, &xr_session);
if (sessionInit != XR_SUCCESS) {
mprintf(("Failed to create OpenXR session with code %d\n", static_cast<int>(sessionInit)));
return false;
}
return true;
}
int64_t vulkan_openxr_get_swapchain_format(const SCP_vector<int64_t>& allowed)
{
// Prefer the format of our own composition image, which makes the blit into
// the swapchain a straight copy with no conversion.
const int64_t preferred[] = {
static_cast<int64_t>(VK_FORMAT_R16G16B16A16_SFLOAT),
static_cast<int64_t>(VK_FORMAT_B8G8R8A8_UNORM),
};
for (int64_t format : preferred) {
if (std::find(allowed.cbegin(), allowed.cend(), format) != allowed.cend()) {
return format;
}
}
return allowed.front();
}
bool vulkan_openxr_acquire_swapchain_buffers()
{
for (uint32_t i = 0; i < 2; i++) {
uint32_t imageCount = 0;
XrResult swapchainAcq = xrEnumerateSwapchainImages(xr_swapchains[i]->swapchain, 0, &imageCount, nullptr);
if (swapchainAcq != XR_SUCCESS) {
mprintf(("Failed to acquire OpenXR swapchain %d with code %d\n", i, static_cast<int>(swapchainAcq)));
return false;
}
Xr_swapchain_images[i] = SCP_vector<XrSwapchainImageVulkan2KHR>(imageCount,
{ XR_TYPE_SWAPCHAIN_IMAGE_VULKAN2_KHR, nullptr, VK_NULL_HANDLE });
swapchainAcq = xrEnumerateSwapchainImages(xr_swapchains[i]->swapchain,
imageCount,
&imageCount,
(XrSwapchainImageBaseHeader*)Xr_swapchain_images[i].data());
if (swapchainAcq != XR_SUCCESS) {
mprintf(("Failed to acquire OpenXR swapchain %d with code %d\n", i, static_cast<int>(swapchainAcq)));
return false;
}
}
return true;
}
bool vulkan_openxr_flip()
{
if (!openxr_enabled()) {
return false;
}
auto* renderer = getRendererInstance();
static bool was_multiframe = false;
switch (xr_stage) {
case OpenXRFBStage::FIRST:
// openxr_start_frame() already ran in openxr_start_stereo_frame().
was_multiframe = true;
render_eye(renderer, 0);
// The right eye is rendered into the same composition image over the
// next gr_flip() cycle, so start it from a clean slate rather than on
// top of the left eye's picture.
renderer->restartCompositionPass();
xr_stage = OpenXRFBStage::SECOND;
return true;
case OpenXRFBStage::SECOND:
render_eye(renderer, 1);
end_frame(renderer, true);
break;
case OpenXRFBStage::NONE:
// A flat frame that nothing has told OpenXR about yet.
openxr_start_frame();
if (was_multiframe) {
openxr_reset_offset();
was_multiframe = false;
}
draw_cursor();
render_eye(renderer, 0);
end_frame(renderer, false);
break;
}
xr_stage = OpenXRFBStage::NONE;
// gr_flip() skips its own page flip when we return true, so the new frame
// has to be opened here instead.
gr_setup_frame();
return true;
}
#else
// Stubs for builds without OpenXR support.
SCP_vector<const char*> vulkan_openxr_get_extensions() { return SCP_vector<const char*>{}; }
bool vulkan_openxr_test_capabilities() { return false; }
bool vulkan_openxr_create_session() { return false; }
int64_t vulkan_openxr_get_swapchain_format(const SCP_vector<int64_t>& /*allowed*/) { return 0; }
bool vulkan_openxr_acquire_swapchain_buffers() { return false; }
bool vulkan_openxr_flip() { return false; }
#endif
vk::UniqueInstance vulkan_openxr_create_instance(const vk::InstanceCreateInfo& createInfo)
{
#ifdef FS_OPENXR
// Must be queried before the instance is created, per the extension.
XrGraphicsRequirementsVulkan2KHR requirements{
XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR,
nullptr,
0,
0
};
auto reqResult = openxr_callExtensionFunction<PFN_xrGetVulkanGraphicsRequirements2KHR>(
"xrGetVulkanGraphicsRequirements2KHR", xr_instance, xr_system, &requirements);
if (!reqResult.has_value() || *reqResult != XR_SUCCESS) {
mprintf(("Failed to query OpenXR graphics requirements!\n"));
return vk::createInstanceUnique(createInfo, nullptr);
}
const XrVersion ourVersion = XR_MAKE_VERSION(VK_API_VERSION_MAJOR(VulkanApiVersion),
VK_API_VERSION_MINOR(VulkanApiVersion),
0);
if (ourVersion < requirements.minApiVersionSupported || ourVersion > requirements.maxApiVersionSupported) {
mprintf(("System doesn't meet OpenXR graphics requirements (min %" PRIu64 ", max %" PRIu64 ", using %" PRIu64 ")!\n",
requirements.minApiVersionSupported,
requirements.maxApiVersionSupported,
ourVersion));
return vk::createInstanceUnique(createInfo, nullptr);
}
XrVulkanInstanceCreateInfoKHR xrCreateInfo{
XR_TYPE_VULKAN_INSTANCE_CREATE_INFO_KHR,
nullptr,
xr_system,
0,
reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr()),
reinterpret_cast<const VkInstanceCreateInfo*>(&createInfo),
nullptr
};
VkInstance rawInstance = VK_NULL_HANDLE;
VkResult vulkanResult = VK_SUCCESS;
auto created = openxr_callExtensionFunction<PFN_xrCreateVulkanInstanceKHR>(
"xrCreateVulkanInstanceKHR", xr_instance, &xrCreateInfo, &rawInstance, &vulkanResult);
if (!created.has_value() || *created != XR_SUCCESS || vulkanResult != VK_SUCCESS) {
mprintf(("OpenXR failed to create the Vulkan instance (xr %d, vk %d)!\n",
created.has_value() ? static_cast<int>(*created) : -1,
static_cast<int>(vulkanResult)));
return vk::createInstanceUnique(createInfo, nullptr);
}
Xr_graphics_requirements_met = true;
const vk::detail::ObjectDestroy<vk::detail::NoParent, VULKAN_HPP_DEFAULT_DISPATCHER_TYPE> deleter(nullptr,
VULKAN_HPP_DEFAULT_DISPATCHER);
return vk::UniqueInstance(vk::Instance(rawInstance), deleter);
#else
return vk::createInstanceUnique(createInfo, nullptr);
#endif
}
vk::PhysicalDevice vulkan_openxr_required_physical_device([[maybe_unused]] vk::Instance instance)
{
#ifdef FS_OPENXR
if (openxr_requested() && Xr_graphics_requirements_met) {
XrVulkanGraphicsDeviceGetInfoKHR getInfo{
XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR,
nullptr,
xr_system,
instance
};
VkPhysicalDevice rawDevice = VK_NULL_HANDLE;
auto result = openxr_callExtensionFunction<PFN_xrGetVulkanGraphicsDevice2KHR>(
"xrGetVulkanGraphicsDevice2KHR", xr_instance, &getInfo, &rawDevice);
if (result.has_value() && *result == XR_SUCCESS && rawDevice != VK_NULL_HANDLE) {
vk::PhysicalDevice device(rawDevice);
mprintf(("OpenXR requires Vulkan device %s.\n", device.getProperties().deviceName.data()));
return device;
}
mprintf(("Failed to query the Vulkan device required by OpenXR!\n"));
}
#endif
return {};
}
vk::UniqueDevice vulkan_openxr_create_device(vk::PhysicalDevice physicalDevice, const vk::DeviceCreateInfo& createInfo)
{
#ifdef FS_OPENXR
if (Xr_graphics_requirements_met) {
XrVulkanDeviceCreateInfoKHR xrCreateInfo{
XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR,
nullptr,
xr_system,
0,
reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr()),
physicalDevice,
reinterpret_cast<const VkDeviceCreateInfo*>(&createInfo),
nullptr
};
VkDevice rawDevice = VK_NULL_HANDLE;
VkResult vulkanResult = VK_SUCCESS;
auto created = openxr_callExtensionFunction<PFN_xrCreateVulkanDeviceKHR>(
"xrCreateVulkanDeviceKHR", xr_instance, &xrCreateInfo, &rawDevice, &vulkanResult);
if (created.has_value() && *created == XR_SUCCESS && vulkanResult == VK_SUCCESS) {
const vk::detail::ObjectDestroy<vk::detail::NoParent, VULKAN_HPP_DEFAULT_DISPATCHER_TYPE> deleter(nullptr, VULKAN_HPP_DEFAULT_DISPATCHER);
return vk::UniqueDevice(vk::Device(rawDevice), deleter);
}
// Falling back to a device the runtime did not create means no VR, but
// it is better than failing to start the game at all.
mprintf(("OpenXR failed to create the Vulkan device (xr %d, vk %d)!\n",
created.has_value() ? static_cast<int>(*created) : -1,
static_cast<int>(vulkanResult)));
Xr_graphics_requirements_met = false;
}
#endif
return physicalDevice.createDeviceUnique(createInfo);
}
} // namespace graphics::vulkan
+53
View File
@@ -0,0 +1,53 @@
#pragma once
#include <vulkan/vulkan.hpp>
namespace graphics::vulkan {
/**
* @brief OpenXR support for the Vulkan backend
*
* Every line of Vulkan code that knows about OpenXR lives in VulkanOpenXR.cpp.
* The rest of the backend only sees the four entry points below, none of which
* mention OpenXR in their signatures.
*
* The three creation helpers exist because XR_KHR_vulkan_enable2 does not let
* the application create its own instance and device: the runtime has to create
* them (xrCreateVulkanInstanceKHR / xrCreateVulkanDeviceKHR) so it can inject
* the extensions it needs, and it dictates which physical device to use. That
* happens during VulkanRenderer::initialize(), long before openxr_init() runs,
* so the renderer's normal creation calls are routed through here instead of
* being duplicated behind an "is VR on?" branch at each site.
*
* When VR was not requested -- or the build has no OpenXR support at all --
* each helper performs exactly the plain Vulkan call it replaced.
*/
/**
* @brief Create the Vulkan instance, via OpenXR when a VR session is coming
*/
vk::UniqueInstance vulkan_openxr_create_instance(const vk::InstanceCreateInfo& createInfo);
/**
* @brief The physical device a VR session requires, or a null handle if any device will do
*/
vk::PhysicalDevice vulkan_openxr_required_physical_device(vk::Instance instance);
/**
* @brief Create the logical device, via OpenXR when a VR session is coming
*/
vk::UniqueDevice vulkan_openxr_create_device(vk::PhysicalDevice physicalDevice, const vk::DeviceCreateInfo& createInfo);
SCP_vector<const char*> vulkan_openxr_get_extensions();
bool vulkan_openxr_test_capabilities();
bool vulkan_openxr_create_session();
int64_t vulkan_openxr_get_swapchain_format(const SCP_vector<int64_t>& allowed);
bool vulkan_openxr_acquire_swapchain_buffers();
bool vulkan_openxr_flip();
} // namespace graphics::vulkan
@@ -10,6 +10,7 @@
#include "VulkanDescriptorManager.h"
#include "graphics/grinternal.h"
#include "graphics/2d.h"
#include "graphics/openxr.h"
#include "lighting/lighting_profiles.h"
#include "lighting/lighting.h"
#include "nebula/neb.h"
@@ -27,7 +28,7 @@ namespace graphics::vulkan {
bool PostProcessContext::initScratchUBO()
{
return scratchRing.init(device, memoryManager, SCRATCH_UBO_MAX_SLOTS, SCRATCH_UBO_SLOT_SIZE);
return scratchRing.init(device, memoryManager, (openxr_requested() ? 2 : 1) * SCRATCH_UBO_MAX_SLOTS, SCRATCH_UBO_SLOT_SIZE);
}
void PostProcessContext::shutdownScratchUBO()
+16 -8
View File
@@ -50,7 +50,7 @@ void VulkanRenderer::createCompositionResources()
vk::ImageCreateInfo imageInfo;
imageInfo.imageType = vk::ImageType::e2D;
imageInfo.format = HDR_COLOR_FORMAT;
imageInfo.extent = vk::Extent3D(m_swapChainExtent.width, m_swapChainExtent.height, 1);
imageInfo.extent = vk::Extent3D(m_renderExtent.width, m_renderExtent.height, 1);
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.samples = vk::SampleCountFlagBits::e1;
@@ -155,8 +155,8 @@ void VulkanRenderer::createFrameBuffers()
framebufferInfo.renderPass = m_renderPass.get();
framebufferInfo.attachmentCount = 2;
framebufferInfo.pAttachments = attachments;
framebufferInfo.width = m_swapChainExtent.width;
framebufferInfo.height = m_swapChainExtent.height;
framebufferInfo.width = m_renderExtent.width;
framebufferInfo.height = m_renderExtent.height;
framebufferInfo.layers = 1;
m_swapChainFramebuffers.push_back(m_device->createFramebufferUnique(framebufferInfo));
@@ -179,6 +179,14 @@ void VulkanRenderer::createFrameBuffers()
}
}
vk::Image VulkanRenderer::getCurrentCompositionImage() const
{
if (m_currentSwapChainImage >= m_compositionImages.size()) {
return {};
}
return m_compositionImages[m_currentSwapChainImage].get();
}
void VulkanRenderer::encodeToSwapChain()
{
if (!m_postProcessor || m_currentSwapChainImage >= m_swapChainImages.size()) {
@@ -257,8 +265,8 @@ void VulkanRenderer::createDepthResources()
vk::ImageCreateInfo imageInfo;
imageInfo.imageType = vk::ImageType::e2D;
imageInfo.format = m_depthFormat;
imageInfo.extent.width = m_swapChainExtent.width;
imageInfo.extent.height = m_swapChainExtent.height;
imageInfo.extent.width = m_renderExtent.width;
imageInfo.extent.height = m_renderExtent.height;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
@@ -287,7 +295,7 @@ void VulkanRenderer::createDepthResources()
m_depthImageView = m_device->createImageViewUnique(viewInfo);
nprintf(("vulkan", "Vulkan: Created depth buffer (%dx%d, format %d)\n",
m_swapChainExtent.width, m_swapChainExtent.height, static_cast<int>(m_depthFormat)));
m_renderExtent.width, m_renderExtent.height, static_cast<int>(m_depthFormat)));
}
void VulkanRenderer::destroyDepthResources()
{
@@ -454,8 +462,8 @@ bool VulkanRenderer::readbackFramebuffer(ubyte** outPixels, uint32_t* outWidth,
const vk::PipelineStageFlags2 srcStageMask = vk::PipelineStageFlagBits2::eFragmentShader;
const vk::AccessFlags2 srcAccessMask = vk::AccessFlagBits2::eShaderSampledRead;
const uint32_t bytesPerSrcPixel = 8; // fp16 RGBA = 4 x 2 bytes
uint32_t w = m_swapChainExtent.width;
uint32_t h = m_swapChainExtent.height;
uint32_t w = m_renderExtent.width;
uint32_t h = m_renderExtent.height;
vk::DeviceSize bufferSize = static_cast<vk::DeviceSize>(w) * h * bytesPerSrcPixel;
// End the current render pass so we can record transfer commands
+65
View File
@@ -172,6 +172,44 @@ class VulkanRenderer {
*/
VkCommandBuffer getVkCurrentCommandBuffer() const;
/**
* @brief Get the current frame command buffer (valid between setupFrame and flip)
*/
vk::CommandBuffer getCurrentCommandBuffer() const { return m_currentCommandBuffer; }
vk::Instance getVkInstance() const { return m_vkInstance.get(); }
vk::PhysicalDevice getPhysicalDevice() const { return m_physicalDevice; }
vk::Device getDevice() const { return m_device.get(); }
vk::Queue getGraphicsQueue() const { return m_graphicsQueue; }
uint32_t getGraphicsQueueFamilyIndex() const { return m_graphicsQueueFamilyIndex; }
/**
* @brief Size of the actual window / swap chain images
*/
vk::Extent2D getSwapChainExtent() const { return m_swapChainExtent; }
/**
* @brief Size everything is rendered at, which is not always the window size
*
* The engine positions every draw in terms of gr_screen.max_w/max_h, which
* may differ from the window (see Cmdline_window_res and
* gr_window_to_render_pos). Everything upstream of the output-encode pass --
* composition image, depth, and the post-processor's scene and G-buffer
* targets -- is sized to this, and the encode pass scales it into the swap
* chain image. OpenGL does the same thing with Back_framebuffer.
*/
vk::Extent2D getRenderExtent() const { return m_renderExtent; }
/**
* @brief Get the composition image the frame is currently being rendered into
*
* This is the fp16 image behind m_swapChainFramebuffers -- the whole frame
* (3D scene, HUD and menus alike) lands here, and encodeToSwapChain() later
* converts it into the actual swap chain image. Returns a null handle if
* composition resources do not exist yet.
*/
vk::Image getCurrentCompositionImage() const;
/**
* @brief Check if VK_EXT_debug_utils is enabled
*/
@@ -285,6 +323,25 @@ class VulkanRenderer {
*/
void resumeSwapChainPass();
/**
* @brief Begin the composition render pass afresh, clearing color and depth
*
* The loadOp=eClear counterpart to resumeSwapChainPass(): discards whatever
* the composition image held and starts a clean frame into the same swap
* chain image, without submitting anything.
*/
void restartCompositionPass();
/**
* @brief End the in-flight render pass, if one is active
*
* Recording a transfer/build command requires being outside a render pass
* instance, but callers cannot generally know whether the frame currently
* has one open. This ends it and clears the state tracker's record of it,
* so a second call (or a later flip()) is a no-op rather than an error.
*/
void endCurrentRenderPass();
/**
* @brief Resume rendering into a render target with loadOp=eLoad (preserving content)
*
@@ -393,6 +450,14 @@ class VulkanRenderer {
bool m_hdrActive = false; // True when an HDR10 (PQ/BT.2020) swap chain was negotiated
bool m_hdrMetadataSupported = false; // VK_EXT_hdr_metadata device extension enabled
vk::Extent2D m_swapChainExtent;
// The resolution the engine draws at (gr_screen.max_w/max_h), which is not
// always the window size: when Cmdline_window_res is set,
// SDLGraphicsOperations::createViewport sizes the window from that instead,
// and the frame is scaled into it by the output-encode pass. -vr forces
// exactly that split (Cmdline_window_res 1000x1000 vs. the VR resolution
// option), so treating the two as one is what put whole frames in the
// upper-left corner of the screen.
vk::Extent2D m_renderExtent;
SCP_vector<vk::Image> m_swapChainImages;
SCP_vector<vk::UniqueImageView> m_swapChainImageViews;
SCP_vector<vk::UniqueFramebuffer> m_swapChainFramebuffers;
+33 -7
View File
@@ -152,7 +152,7 @@ void VulkanRenderer::setupFrame()
PassBeginDesc pass;
pass.renderPass = m_renderPass.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_swapChainExtent;
pass.extent = m_renderExtent;
pass.clearValues = clearValues;
beginTrackedRenderPass(pass);
@@ -173,7 +173,9 @@ void VulkanRenderer::flip()
// End the composition render pass (composition image is now in
// eShaderReadOnlyOptimal) and run the final output-encode pass that writes
// the actual swap chain image (SDR passthrough or HDR10 PQ/BT.2020).
m_currentCommandBuffer.endRenderPass();
// Tolerates the pass having already been ended: the OpenXR flip has to end
// it itself to record its swapchain blit before handing off to us.
endCurrentRenderPass();
#ifdef __APPLE__
// MoltenVK: the encode render pass's VK_SUBPASS_EXTERNAL dependency (see
@@ -345,7 +347,7 @@ void VulkanRenderer::endSceneRendering()
PassBeginDesc pass;
pass.renderPass = m_renderPassLoad.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_swapChainExtent;
pass.extent = m_renderExtent;
pass.clearValues = clearValues;
pass.viewport = PassViewport::NoFlip;
beginTrackedRenderPass(pass);
@@ -355,9 +357,9 @@ void VulkanRenderer::endSceneRendering()
// Restore Y-flipped viewport for HUD rendering
m_stateTracker->setViewport(0.0f,
static_cast<float>(m_swapChainExtent.height),
static_cast<float>(m_swapChainExtent.width),
-static_cast<float>(m_swapChainExtent.height));
static_cast<float>(m_renderExtent.height),
static_cast<float>(m_renderExtent.width),
-static_cast<float>(m_renderExtent.height));
m_sceneRendering = false;
m_useGbufRenderPass = false;
@@ -539,11 +541,35 @@ void VulkanRenderer::resumeSwapChainPass()
PassBeginDesc pass;
pass.renderPass = m_renderPassLoad.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_swapChainExtent;
pass.extent = m_renderExtent;
pass.clearValues = clearValues;
beginTrackedRenderPass(pass);
}
void VulkanRenderer::restartCompositionPass()
{
std::array<vk::ClearValue, 2> clearValues;
clearValues[0].color.setFloat32({0.0f, 0.0f, 0.0f, 1.0f});
clearValues[1].depthStencil = vk::ClearDepthStencilValue(1.0f, 0);
PassBeginDesc pass;
pass.renderPass = m_renderPass.get();
pass.framebuffer = m_swapChainFramebuffers[m_currentSwapChainImage].get();
pass.extent = m_renderExtent;
pass.clearValues = clearValues;
beginTrackedRenderPass(pass);
}
void VulkanRenderer::endCurrentRenderPass()
{
if (!m_stateTracker->getCurrentRenderPass()) {
return;
}
m_currentCommandBuffer.endRenderPass();
m_stateTracker->setRenderPass(vk::RenderPass());
}
void VulkanRenderer::resumeRenderTargetPass(tcache_slot_vulkan* ts)
{
// loadOp=eLoad on color keeps whatever was already drawn into the target; depth (if
+94 -9
View File
@@ -2,6 +2,7 @@
#include "VulkanRenderer.h"
#include "VulkanMemory.h"
#include "VulkanBuffer.h"
#include "VulkanOpenXR.h"
#include "VulkanTexture.h"
#include "cmdline/cmdline.h"
@@ -18,6 +19,8 @@
#include <SDL3/SDL_vulkan.h>
#include <cstdint>
#include "graphics/openxr.h"
extern float flFrametime;
@@ -299,6 +302,30 @@ vk::PresentModeKHR choosePresentMode(const PhysicalDeviceValues& values)
return chosen;
}
/**
* @brief The size of the actual window, which is not the render resolution
*
* Only used where the surface declines to tell us its extent (Wayland reports
* UINT32_MAX and lets the client pick). gr_screen.max_w/max_h is the wrong
* answer there: with -window_res set -- which -vr forces -- the window is
* smaller than the resolution the engine draws at.
*/
vk::Extent2D windowExtent()
{
if (auto* viewport = os::getMainViewport()) {
const auto size = viewport->getSize();
if (size.first > 0 && size.second > 0) {
return {size.first, size.second};
}
}
if (Cmdline_window_res) {
return {Cmdline_window_res->first, Cmdline_window_res->second};
}
return {static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h)};
}
vk::Extent2D chooseSwapChainExtent(const PhysicalDeviceValues& values, uint32_t width, uint32_t height)
{
if (values.surfaceCapabilities.currentExtent.width != UINT32_MAX) {
@@ -326,6 +353,13 @@ bool VulkanRenderer::initialize()
// Load the RenderDoc API if available before doing anything with OpenGL
renderdoc::loadApi();
// Capture the render resolution BEFORE the window exists. Creating it can
// queue an SDL resize event, and osapi's handler answers those with an
// unconditional gr_screen_resize() to the *window* size -- which would
// quietly collapse the render resolution onto the window resolution and
// erase the very distinction this extent exists to preserve.
m_renderExtent = vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h));
if (!initDisplayDevice()) {
return false;
}
@@ -495,7 +529,7 @@ bool VulkanRenderer::initialize()
// Initialize post-processing
m_postProcessor = std::make_unique<VulkanPostProcessor>();
if (!m_postProcessor->init(m_device.get(), m_physicalDevice, m_memoryManager.get(),
m_swapChainExtent, m_depthFormat, m_hdrActive)) {
m_renderExtent, m_depthFormat, m_hdrActive)) {
mprintf(("Warning: Failed to initialize Vulkan post-processor, post-processing will be disabled\n"));
m_postProcessor.reset();
} else {
@@ -541,15 +575,29 @@ bool VulkanRenderer::initDisplayDevice() const
attrs.enable_vulkan = true;
attrs.display = gr_get_preferred_display();
attrs.width = static_cast<uint32_t>(gr_screen.max_w);
attrs.height = static_cast<uint32_t>(gr_screen.max_h);
// The window is the window resolution, not the render resolution -- with
// -window_res (which -vr forces on) they differ, and the frame is scaled
// into the window by the output-encode pass. Asking for max_w/max_h here
// would be a lie that SDLGraphicsOperations::createViewport silently
// corrects anyway, since it applies Cmdline_window_res itself.
if (Cmdline_window_res) {
attrs.width = Cmdline_window_res->first;
attrs.height = Cmdline_window_res->second;
} else {
attrs.width = static_cast<uint32_t>(gr_screen.max_w);
attrs.height = static_cast<uint32_t>(gr_screen.max_h);
}
attrs.title = Osreg_title;
if (!Window_title.empty()) {
attrs.title = Window_title;
}
if (Using_in_game_options) {
if (Cmdline_enable_vr) {
// Must use windowed mode here
}
else if (Using_in_game_options) {
switch (Gr_configured_window_state) {
case os::ViewportState::Windowed:
// That's the default
@@ -740,7 +788,13 @@ bool VulkanRenderer::initializeInstance()
createInstanceChain.unlink<vk::ValidationFeaturesEXT>();
}
vk::UniqueInstance instance = vk::createInstanceUnique(createInstanceChain.get<vk::InstanceCreateInfo>(), nullptr);
vk::UniqueInstance instance;
if (openxr_requested())
instance = vulkan_openxr_create_instance(createInstanceChain.get<vk::InstanceCreateInfo>());
else
instance = vk::createInstanceUnique(createInfo, nullptr);
if (!instance) {
return false;
}
@@ -820,6 +874,17 @@ bool VulkanRenderer::pickPhysicalDevice(PhysicalDeviceValues& deviceValues)
mprintf(("Physical Vulkan devices:\n"));
std::for_each(values.cbegin(), values.cend(), printPhysicalDevice);
// A VR session dictates which GPU we must render on -- the one the headset is
// attached to. Drop everything else before scoring, so a discrete card can
// never outrank the device the runtime actually requires.
const auto requiredDevice = vulkan_openxr_required_physical_device(m_vkInstance.get());
if (requiredDevice) {
values.erase(std::remove_if(values.begin(),
values.end(),
[&requiredDevice](const PhysicalDeviceValues& value) { return value.device != requiredDevice; }),
values.end());
}
// Remove devices that do not have the features we need
values.erase(std::remove_if(values.begin(),
values.end(),
@@ -979,7 +1044,10 @@ bool VulkanRenderer::createLogicalDevice(const PhysicalDeviceValues& deviceValue
deviceCreateChain.unlink<vk::PhysicalDeviceBufferDeviceAddressFeatures>();
}
m_device = deviceValues.device.createDeviceUnique(deviceCreateChain.get<vk::DeviceCreateInfo>());
if (openxr_requested())
m_device = vulkan_openxr_create_device(deviceValues.device, deviceCreateChain.get<vk::DeviceCreateInfo>());
else
m_device = deviceValues.device.createDeviceUnique(deviceCreateChain.get<vk::DeviceCreateInfo>());
// Load device-level function pointers for the dynamic dispatcher
VULKAN_HPP_DEFAULT_DISPATCHER.init(m_device.get());
@@ -1034,12 +1102,14 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v
const auto surfaceFormat = chooseSurfaceFormat(deviceValues);
const vk::Extent2D window = windowExtent();
vk::SwapchainCreateInfoKHR createInfo;
createInfo.surface = m_vkSurface.get();
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = chooseSwapChainExtent(deviceValues, gr_screen.max_w, gr_screen.max_h);
createInfo.imageExtent = chooseSwapChainExtent(deviceValues, window.width, window.height);
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment
| vk::ImageUsageFlagBits::eTransferSrc
@@ -1075,7 +1145,21 @@ bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, v
m_hdrActive = (surfaceFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT);
Gr_hdr_output_active = m_hdrActive;
m_swapChainExtent = createInfo.imageExtent;
// Without -window_res the render resolution simply is the window, so follow
// gr_screen (which gr_screen_resize() has already updated for this
// recreation). With it, the two are deliberately independent -- the window
// is whatever Cmdline_window_res says (SDLGraphicsOperations::createViewport
// forces it) while the engine keeps drawing at gr_screen.max_w/max_h -- so
// the value captured in initialize() must survive untouched.
if (!Cmdline_window_res) {
m_renderExtent = vk::Extent2D(static_cast<uint32_t>(gr_screen.max_w), static_cast<uint32_t>(gr_screen.max_h));
}
mprintf(("Vulkan: Swap chain output mode: %s\n", m_hdrActive ? "HDR10 (PQ/BT.2020)" : "SDR (sRGB)"));
mprintf(("Vulkan: Render resolution %ux%u, window/swap chain %ux%u\n",
m_renderExtent.width, m_renderExtent.height,
m_swapChainExtent.width, m_swapChainExtent.height));
m_swapChainImageViews.reserve(m_swapChainImages.size());
for (const auto& image : m_swapChainImages) {
@@ -1144,7 +1228,8 @@ bool VulkanRenderer::recreateSwapChain()
freshValues.presentQueueIndex = {true, m_presentQueueFamilyIndex};
// Check for 0x0 extent (minimized window) — caller should retry later
auto extent = chooseSwapChainExtent(freshValues, gr_screen.max_w, gr_screen.max_h);
const vk::Extent2D window = windowExtent();
auto extent = chooseSwapChainExtent(freshValues, window.width, window.height);
if (extent.width == 0 || extent.height == 0) {
nprintf(("vulkan", "Vulkan: Surface extent is 0x0 (minimized), deferring swap chain recreation\n"));
return false;
@@ -1178,7 +1263,7 @@ bool VulkanRenderer::recreateSwapChain()
// Recreate the post-processor's extent-sized targets (scene color/depth,
// G-buffer, bloom chains, LDR/SMAA targets, ...). Its render passes and
// samplers are extent-independent and stay alive, keeping pipelines valid.
if (m_postProcessor && !m_postProcessor->resize(m_swapChainExtent)) {
if (m_postProcessor && !m_postProcessor->resize(m_renderExtent)) {
mprintf(("Vulkan: post-processor resize failed, disabling post-processing!\n"));
setPostProcessor(nullptr);
m_postProcessor->shutdown();
+7 -12
View File
@@ -10,6 +10,7 @@
#include "VulkanState.h"
#include "VulkanDraw.h"
#include "VulkanDeferred.h"
#include "VulkanOpenXR.h"
#include "VulkanPostProcessing.h"
#include "backends/imgui_impl_sdl3.h"
@@ -406,12 +407,6 @@ std::unique_ptr<os::Viewport> stub_create_viewport(const os::ViewPortProperties&
return {};
}
void stub_use_viewport(os::Viewport* /*view*/) {}
SCP_vector<const char*> stub_openxr_get_extensions() { return {}; }
bool stub_openxr_test_capabilities() { return false; }
bool stub_openxr_create_session() { return false; }
int64_t stub_openxr_get_swapchain_format(const SCP_vector<int64_t>& /*allowed*/) { return 0; }
bool stub_openxr_acquire_swapchain_buffers() { return false; }
bool stub_openxr_flip() { return false; }
// ========== Function pointer table ==========
// Implementations are defined in their respective files:
@@ -555,12 +550,12 @@ void init_function_pointers()
gr_screen.gf_set_viewport = vulkan_set_viewport;
gr_screen.gf_openxr_get_extensions = stub_openxr_get_extensions;
gr_screen.gf_openxr_test_capabilities = stub_openxr_test_capabilities;
gr_screen.gf_openxr_create_session = stub_openxr_create_session;
gr_screen.gf_openxr_get_swapchain_format = stub_openxr_get_swapchain_format;
gr_screen.gf_openxr_acquire_swapchain_buffers = stub_openxr_acquire_swapchain_buffers;
gr_screen.gf_openxr_flip = stub_openxr_flip;
gr_screen.gf_openxr_get_extensions = vulkan_openxr_get_extensions;
gr_screen.gf_openxr_test_capabilities = vulkan_openxr_test_capabilities;
gr_screen.gf_openxr_create_session = vulkan_openxr_create_session;
gr_screen.gf_openxr_get_swapchain_format = vulkan_openxr_get_swapchain_format;
gr_screen.gf_openxr_acquire_swapchain_buffers = vulkan_openxr_acquire_swapchain_buffers;
gr_screen.gf_openxr_flip = vulkan_openxr_flip;
}
} // anonymous namespace
+2
View File
@@ -610,6 +610,8 @@ if (FSO_BUILD_WITH_VULKAN)
graphics/vulkan/VulkanDrawAPI.cpp
graphics/vulkan/VulkanMemory.cpp
graphics/vulkan/VulkanMemory.h
graphics/vulkan/VulkanOpenXR.cpp
graphics/vulkan/VulkanOpenXR.h
graphics/vulkan/VulkanPerFrameUbo.cpp
graphics/vulkan/VulkanPerFrameUbo.h
graphics/vulkan/VulkanPipeline.cpp