mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
Add Vulkan + Wayland init logic for OpenXR (#7767)
* Add Vulkan + Wayland init logic for OpenXR * typo * clang tidy
This commit is contained in:
1 parent
3ac8be273e
commit
a73e2565c8
11 files changed
+929
-74
No files matched your search
@@ -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
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user