mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
1303 lines
53 KiB
C++
1303 lines
53 KiB
C++
|
|
#include "VulkanRenderer.h"
|
|
#include "VulkanMemory.h"
|
|
#include "VulkanBuffer.h"
|
|
#include "VulkanOpenXR.h"
|
|
#include "VulkanTexture.h"
|
|
|
|
#include "cmdline/cmdline.h"
|
|
#include "globalincs/version.h"
|
|
#include "graphics/grinternal.h"
|
|
#include "graphics/post_processing.h"
|
|
|
|
#include "backends/imgui_impl_vulkan.h"
|
|
#include "graphics/2d.h"
|
|
#include "lighting/lighting.h"
|
|
#include "libs/renderdoc/renderdoc.h"
|
|
#include "mod_table/mod_table.h"
|
|
|
|
#include <SDL3/SDL_vulkan.h>
|
|
#include <cstdint>
|
|
|
|
#include "graphics/openxr.h"
|
|
|
|
|
|
extern float flFrametime;
|
|
|
|
namespace graphics::vulkan {
|
|
|
|
namespace {
|
|
|
|
const char* EngineName = "FreeSpaceOpen";
|
|
|
|
const gameversion::version MinVulkanVersion(1, 1, 0, 0);
|
|
|
|
VkBool32 VKAPI_PTR debugReportCallback(
|
|
vk::DebugReportFlagsEXT /*flags*/,
|
|
vk::DebugReportObjectTypeEXT /*objectType*/,
|
|
uint64_t /*object*/,
|
|
size_t /*location*/,
|
|
int32_t /*messageCode*/,
|
|
const char* pLayerPrefix,
|
|
const char* pMessage,
|
|
void* /*pUserData*/)
|
|
{
|
|
nprintf(("vulkan", "Vulkan message: [%s]: %s\n", pLayerPrefix, pMessage));
|
|
return VK_FALSE;
|
|
}
|
|
|
|
VkBool32 VKAPI_PTR debugUtilsMessengerCallback(
|
|
vk::DebugUtilsMessageSeverityFlagBitsEXT severity,
|
|
vk::DebugUtilsMessageTypeFlagsEXT /*types*/,
|
|
const vk::DebugUtilsMessengerCallbackDataEXT* callbackData,
|
|
void* /*pUserData*/)
|
|
{
|
|
const char* msgId = (callbackData != nullptr && callbackData->pMessageIdName != nullptr)
|
|
? callbackData->pMessageIdName : "unknown";
|
|
const char* msg = (callbackData != nullptr && callbackData->pMessage != nullptr)
|
|
? callbackData->pMessage : "";
|
|
|
|
if (severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eError ||
|
|
severity == vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
|
|
// Errors and warnings (including all -gr_sync_validation hazards) go to the
|
|
// main log so they are visible without the "vulkan" debug filter enabled.
|
|
mprintf(("Vulkan validation: [%s]: %s\n", msgId, msg));
|
|
} else {
|
|
nprintf(("vulkan", "Vulkan message: [%s]: %s\n", msgId, msg));
|
|
}
|
|
return VK_FALSE;
|
|
}
|
|
|
|
const SCP_vector<const char*> RequiredDeviceExtensions = {
|
|
VK_KHR_SWAPCHAIN_EXTENSION_NAME,
|
|
// Barrier submission across this renderer is written entirely against
|
|
// VK_KHR_synchronization2 (see VulkanBarrier.h) -- there is no legacy
|
|
// vkCmdPipelineBarrier fallback, so the extension is required rather than
|
|
// optionally negotiated.
|
|
VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME,
|
|
};
|
|
|
|
bool checkDeviceExtensionSupport(PhysicalDeviceValues& values)
|
|
{
|
|
auto exts = values.device.enumerateDeviceExtensionProperties();
|
|
values.extensions.assign(exts.begin(), exts.end());
|
|
|
|
std::set<std::string> requiredExtensions(RequiredDeviceExtensions.cbegin(), RequiredDeviceExtensions.cend());
|
|
for (const auto& extension : values.extensions) {
|
|
requiredExtensions.erase(extension.extensionName);
|
|
}
|
|
|
|
return requiredExtensions.empty();
|
|
}
|
|
|
|
bool checkSwapChainSupport(PhysicalDeviceValues& values, const vk::UniqueSurfaceKHR& surface)
|
|
{
|
|
values.surfaceCapabilities = values.device.getSurfaceCapabilitiesKHR(surface.get());
|
|
auto fmts = values.device.getSurfaceFormatsKHR(surface.get());
|
|
values.surfaceFormats.assign(fmts.begin(), fmts.end());
|
|
auto modes = values.device.getSurfacePresentModesKHR(surface.get());
|
|
values.presentModes.assign(modes.begin(), modes.end());
|
|
|
|
return !values.surfaceFormats.empty() && !values.presentModes.empty();
|
|
}
|
|
|
|
bool isDeviceUnsuitable(PhysicalDeviceValues& values, const vk::UniqueSurfaceKHR& surface)
|
|
{
|
|
// We need a GPU. Reject CPU or "other" types.
|
|
if (values.properties.deviceType != vk::PhysicalDeviceType::eDiscreteGpu &&
|
|
values.properties.deviceType != vk::PhysicalDeviceType::eIntegratedGpu &&
|
|
values.properties.deviceType != vk::PhysicalDeviceType::eVirtualGpu) {
|
|
nprintf(("vulkan", "Rejecting %s (%d) because the device type is unsuitable.\n",
|
|
values.properties.deviceName.data(),
|
|
values.properties.deviceID));
|
|
return true;
|
|
}
|
|
|
|
uint32_t i = 0;
|
|
for (const auto& queue : values.queueProperties) {
|
|
if (queue.queueFlags & vk::QueueFlagBits::eGraphics) {
|
|
if (!values.graphicsQueueIndex.initialized) {
|
|
values.graphicsQueueIndex.initialized = true;
|
|
values.graphicsQueueIndex.index = i;
|
|
}
|
|
}
|
|
// No dedicated transfer queue is selected: all uploads run on the graphics
|
|
// queue (which implicitly supports transfer). Async transfer on a separate
|
|
// queue is future work and must be reintroduced end-to-end, including
|
|
// queue-family ownership transfers -- not half-wired.
|
|
if (!values.presentQueueIndex.initialized && values.device.getSurfaceSupportKHR(i, surface.get())) {
|
|
values.presentQueueIndex.initialized = true;
|
|
values.presentQueueIndex.index = i;
|
|
}
|
|
|
|
++i;
|
|
}
|
|
|
|
if (!values.graphicsQueueIndex.initialized) {
|
|
nprintf(("vulkan", "Rejecting %s (%d) because the device does not have a graphics queue.\n",
|
|
values.properties.deviceName.data(),
|
|
values.properties.deviceID));
|
|
return true;
|
|
}
|
|
if (!values.presentQueueIndex.initialized) {
|
|
nprintf(("vulkan", "Rejecting %s (%d) because the device does not have a presentation queue.\n",
|
|
values.properties.deviceName.data(),
|
|
values.properties.deviceID));
|
|
return true;
|
|
}
|
|
|
|
if (!checkDeviceExtensionSupport(values)) {
|
|
nprintf(("vulkan", "Rejecting %s (%d) because the device does not support our required extensions.\n",
|
|
values.properties.deviceName.data(),
|
|
values.properties.deviceID));
|
|
return true;
|
|
}
|
|
|
|
// VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME is in RequiredDeviceExtensions
|
|
// above, but its feature bit still has to be checked independently -- a
|
|
// device could theoretically expose the extension without enabling the
|
|
// feature (same reasoning as bufferDeviceAddress's separate feature check).
|
|
if (!values.synchronization2FeatureSupported) {
|
|
nprintf(("vulkan", "Rejecting %s (%d) because the device does not support the synchronization2 feature.\n",
|
|
values.properties.deviceName.data(),
|
|
values.properties.deviceID));
|
|
return true;
|
|
}
|
|
|
|
if (!checkSwapChainSupport(values, surface)) {
|
|
nprintf(("vulkan", "Rejecting %s (%d) because the device swap chain was not compatible.\n",
|
|
values.properties.deviceName.data(),
|
|
values.properties.deviceID));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
uint32_t deviceTypeScore(vk::PhysicalDeviceType type)
|
|
{
|
|
switch (type) {
|
|
case vk::PhysicalDeviceType::eIntegratedGpu:
|
|
return 1;
|
|
case vk::PhysicalDeviceType::eDiscreteGpu:
|
|
return 2;
|
|
case vk::PhysicalDeviceType::eVirtualGpu:
|
|
case vk::PhysicalDeviceType::eCpu:
|
|
case vk::PhysicalDeviceType::eOther:
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
uint64_t scoreDevice(const PhysicalDeviceValues& device)
|
|
{
|
|
// Device type is the primary selection criterion (a discrete GPU must always
|
|
// beat an integrated one). The API version is only a tiebreaker between
|
|
// devices of the same type. We pack the type into the high bits and the
|
|
// (already 32-bit) API version into the low bits so the type can never be
|
|
// overwhelmed by the magnitude of the packed apiVersion value.
|
|
uint64_t score = 0;
|
|
|
|
score |= static_cast<uint64_t>(deviceTypeScore(device.properties.deviceType)) << 32;
|
|
score |= static_cast<uint64_t>(device.properties.apiVersion);
|
|
|
|
return score;
|
|
}
|
|
|
|
bool compareDevices(const PhysicalDeviceValues& left, const PhysicalDeviceValues& right)
|
|
{
|
|
return scoreDevice(left) < scoreDevice(right);
|
|
}
|
|
|
|
void printPhysicalDevice(const PhysicalDeviceValues& values)
|
|
{
|
|
mprintf((" Found %s (%d) of type %s. API version %d.%d.%d, Driver version %d.%d.%d. Scored as " UINT64_T_ARG "\n",
|
|
values.properties.deviceName.data(),
|
|
values.properties.deviceID,
|
|
to_string(values.properties.deviceType).c_str(),
|
|
VK_VERSION_MAJOR(values.properties.apiVersion),
|
|
VK_VERSION_MINOR(values.properties.apiVersion),
|
|
VK_VERSION_PATCH(values.properties.apiVersion),
|
|
VK_VERSION_MAJOR(values.properties.driverVersion),
|
|
VK_VERSION_MINOR(values.properties.driverVersion),
|
|
VK_VERSION_PATCH(values.properties.driverVersion),
|
|
scoreDevice(values)));
|
|
}
|
|
|
|
vk::SurfaceFormatKHR chooseSurfaceFormat(const PhysicalDeviceValues& values)
|
|
{
|
|
// When HDR output is requested, prefer a 10-bit HDR10 (PQ / ST.2084) surface
|
|
// using BT.2020 primaries. The final output-encode pass writes PQ-encoded
|
|
// BT.2020 values into this surface.
|
|
if (Gr_enable_hdr) {
|
|
for (const auto& availableFormat : values.surfaceFormats) {
|
|
if ((availableFormat.format == vk::Format::eA2B10G10R10UnormPack32 ||
|
|
availableFormat.format == vk::Format::eA2R10G10B10UnormPack32) &&
|
|
availableFormat.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT) {
|
|
nprintf(("vulkan", "Vulkan: Selected HDR10 surface (10-bit, ST.2084/BT.2020)\n"));
|
|
return availableFormat;
|
|
}
|
|
}
|
|
nprintf(("vulkan", "Vulkan: HDR requested but no HDR10 surface format available; falling back to SDR\n"));
|
|
}
|
|
|
|
// Use a non-sRGB (UNORM) format to match OpenGL's default framebuffer behavior.
|
|
// The FSO shaders handle gamma correction manually in the fragment shader and
|
|
// post-processing pipeline, so hardware sRGB conversion would double-correct.
|
|
for (const auto& availableFormat : values.surfaceFormats) {
|
|
if (availableFormat.format == vk::Format::eB8G8R8A8Unorm &&
|
|
availableFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear) {
|
|
return availableFormat;
|
|
}
|
|
}
|
|
|
|
// Fallback: no preferred format matched. Pick the first concrete format,
|
|
// defensively skipping any eUndefined entry (the legacy "any format allowed"
|
|
// sentinel), and log the actual choice so it's visible in the log.
|
|
for (const auto& availableFormat : values.surfaceFormats) {
|
|
if (availableFormat.format != vk::Format::eUndefined) {
|
|
nprintf(("vulkan", "Vulkan: no preferred surface format available; falling back to format=%d colorSpace=%d\n",
|
|
static_cast<int>(availableFormat.format), static_cast<int>(availableFormat.colorSpace)));
|
|
return availableFormat;
|
|
}
|
|
}
|
|
|
|
// Degenerate list (all eUndefined) — return the front entry and warn.
|
|
nprintf(("vulkan", "Vulkan: surface format list has no concrete entry; using front (format=%d)\n",
|
|
static_cast<int>(values.surfaceFormats.front().format)));
|
|
return values.surfaceFormats.front();
|
|
}
|
|
|
|
vk::PresentModeKHR choosePresentMode(const PhysicalDeviceValues& values)
|
|
{
|
|
// With vsync requested, use FIFO: it is the only spec-guaranteed mode and
|
|
// the only one that actually caps the frame rate to the display. Mailbox is
|
|
// tear-free but uncapped ("fast vsync") and must not be silently substituted
|
|
// for requested vsync. Without vsync prefer Immediate (true uncapped), then
|
|
// Mailbox (uncapped but tear-free), then the guaranteed FIFO fallback.
|
|
vk::PresentModeKHR chosen = vk::PresentModeKHR::eFifo;
|
|
|
|
if (!Gr_enable_vsync) {
|
|
for (const auto& availablePresentMode : values.presentModes) {
|
|
if (availablePresentMode == vk::PresentModeKHR::eImmediate) {
|
|
chosen = availablePresentMode;
|
|
break;
|
|
}
|
|
if (availablePresentMode == vk::PresentModeKHR::eMailbox) {
|
|
chosen = availablePresentMode;
|
|
}
|
|
}
|
|
}
|
|
|
|
const char* name = "Unknown";
|
|
switch (chosen) {
|
|
case vk::PresentModeKHR::eImmediate: name = "Immediate"; break;
|
|
case vk::PresentModeKHR::eMailbox: name = "Mailbox"; break;
|
|
case vk::PresentModeKHR::eFifo: name = "FIFO (vsync)"; break;
|
|
case vk::PresentModeKHR::eFifoRelaxed: name = "FIFO Relaxed"; break;
|
|
default: break;
|
|
}
|
|
mprintf(("Vulkan: Present mode: %s (Gr_enable_vsync=%d)\n", name, Gr_enable_vsync ? 1 : 0));
|
|
|
|
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) {
|
|
return values.surfaceCapabilities.currentExtent;
|
|
} else {
|
|
VkExtent2D actualExtent = {width, height};
|
|
|
|
actualExtent.width = std::max(values.surfaceCapabilities.minImageExtent.width,
|
|
std::min(values.surfaceCapabilities.maxImageExtent.width, actualExtent.width));
|
|
actualExtent.height = std::max(values.surfaceCapabilities.minImageExtent.height,
|
|
std::min(values.surfaceCapabilities.maxImageExtent.height, actualExtent.height));
|
|
|
|
return actualExtent;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
bool VulkanRenderer::initialize()
|
|
{
|
|
mprintf(("Initializing Vulkan graphics device at %ix%i with %i-bit color...\n",
|
|
gr_screen.max_w,
|
|
gr_screen.max_h,
|
|
gr_screen.bits_per_pixel));
|
|
|
|
// 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;
|
|
}
|
|
|
|
try {
|
|
if (!initializeInstance()) {
|
|
mprintf(("Failed to create Vulkan instance!\n"));
|
|
return false;
|
|
}
|
|
} catch(const vk::SystemError &err) {
|
|
mprintf(("Vulkan ERROR: %s\n", err.what()));
|
|
return false;
|
|
}
|
|
|
|
if (!initializeSurface()) {
|
|
nprintf(("vulkan", "Failed to create Vulkan surface!\n"));
|
|
return false;
|
|
}
|
|
|
|
PhysicalDeviceValues deviceValues;
|
|
if (!pickPhysicalDevice(deviceValues)) {
|
|
nprintf(("vulkan", "Could not find suitable physical Vulkan device.\n"));
|
|
return false;
|
|
}
|
|
|
|
// Validate MSAA sample count against device limits
|
|
if (Cmdline_msaa_enabled > 0) {
|
|
auto limits = deviceValues.properties.limits;
|
|
vk::SampleCountFlags supported = limits.framebufferColorSampleCounts & limits.framebufferDepthSampleCounts;
|
|
|
|
// Map requested count to vk::SampleCountFlagBits
|
|
vk::SampleCountFlagBits requested = vk::SampleCountFlagBits::e1;
|
|
switch (Cmdline_msaa_enabled) {
|
|
case 4: requested = vk::SampleCountFlagBits::e4; break;
|
|
case 8: requested = vk::SampleCountFlagBits::e8; break;
|
|
case 16: requested = vk::SampleCountFlagBits::e16; break;
|
|
default:
|
|
nprintf(("vulkan", "Vulkan: Unsupported MSAA count %d, disabling MSAA\n", Cmdline_msaa_enabled));
|
|
Cmdline_msaa_enabled = 0;
|
|
break;
|
|
}
|
|
|
|
if (Cmdline_msaa_enabled > 0) {
|
|
if (supported & requested) {
|
|
m_msaaSampleCount = requested;
|
|
nprintf(("vulkan", "Vulkan: MSAA enabled with %dx sample count\n", Cmdline_msaa_enabled));
|
|
} else {
|
|
// Clamp down to highest supported
|
|
vk::SampleCountFlagBits fallback = vk::SampleCountFlagBits::e1;
|
|
int fallbackCount = 0;
|
|
if ((supported & vk::SampleCountFlagBits::e8) && Cmdline_msaa_enabled >= 8) {
|
|
fallback = vk::SampleCountFlagBits::e8; fallbackCount = 8;
|
|
} else if (supported & vk::SampleCountFlagBits::e4) {
|
|
fallback = vk::SampleCountFlagBits::e4; fallbackCount = 4;
|
|
}
|
|
|
|
if (fallbackCount > 0) {
|
|
nprintf(("vulkan", "Vulkan: Requested MSAA %dx not supported, falling back to %dx\n",
|
|
Cmdline_msaa_enabled, fallbackCount));
|
|
Cmdline_msaa_enabled = fallbackCount;
|
|
m_msaaSampleCount = fallback;
|
|
} else {
|
|
nprintf(("vulkan", "Vulkan: No suitable MSAA support, disabling MSAA\n"));
|
|
Cmdline_msaa_enabled = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!createLogicalDevice(deviceValues)) {
|
|
nprintf(("vulkan", "Failed to create logical device.\n"));
|
|
return false;
|
|
}
|
|
|
|
createCommandPool(deviceValues);
|
|
|
|
if (!createSwapChain(deviceValues)) {
|
|
nprintf(("vulkan", "Failed to create swap chain.\n"));
|
|
return false;
|
|
}
|
|
|
|
createDepthResources();
|
|
createCompositionResources();
|
|
createEncodeRenderPass();
|
|
createRenderPass();
|
|
createFrameBuffers();
|
|
|
|
createPresentSyncObjects();
|
|
|
|
// Initialize texture manager (needs command pool for uploads)
|
|
m_textureManager = std::make_unique<VulkanTextureManager>();
|
|
if (!m_textureManager->init(m_device.get(), m_physicalDevice, m_memoryManager.get(),
|
|
m_graphicsCommandPool.get(), m_graphicsQueue)) {
|
|
nprintf(("vulkan", "Failed to initialize Vulkan texture manager!\n"));
|
|
return false;
|
|
}
|
|
setTextureManager(m_textureManager.get());
|
|
|
|
// Initialize shader manager
|
|
m_shaderManager = std::make_unique<VulkanShaderManager>();
|
|
if (!m_shaderManager->init(m_device.get())) {
|
|
nprintf(("vulkan", "Failed to initialize Vulkan shader manager!\n"));
|
|
return false;
|
|
}
|
|
setShaderManager(m_shaderManager.get());
|
|
|
|
// Initialize raytraced shadow BLAS/TLAS manager (no-op internally if unsupported).
|
|
// Must happen before the descriptor manager below, since the Global set's
|
|
// layout needs to know whether to include the TLAS binding, and the
|
|
// descriptor fallbacks need the raytracing manager's fallback TLAS.
|
|
m_raytracingManager = std::make_unique<VulkanRaytracingManager>();
|
|
bool raytracingReady = m_raytracingManager->init(m_device.get(), m_physicalDevice, m_memoryManager.get(),
|
|
m_bufferManager.get(), m_graphicsCommandPool.get(),
|
|
m_graphicsQueue, m_supportsRaytracedShadows);
|
|
if (!raytracingReady) {
|
|
if (m_supportsRaytracedShadows) {
|
|
mprintf(("Warning: Failed to initialize Vulkan raytracing manager, raytraced shadows will be disabled\n"));
|
|
}
|
|
m_raytracingManager.reset();
|
|
} else {
|
|
setRaytracingManager(m_raytracingManager.get());
|
|
}
|
|
bool rtDescriptorSupport = m_raytracingManager && m_raytracingManager->isEnabled();
|
|
|
|
// Keep the public capability query (gr_is_capable(CAPABILITY_RAYTRACED_SHADOWS),
|
|
// used to decide the shader-side RT path) in lockstep with whether the Global
|
|
// set's layout actually declares the TLAS binding. m_supportsRaytracedShadows
|
|
// was originally set from device-extension negotiation alone; downgrade it here
|
|
// if the raytracing manager itself failed to come up (e.g. fallback TLAS build
|
|
// failure) so the two can never disagree about whether binding 5 exists.
|
|
m_supportsRaytracedShadows = rtDescriptorSupport;
|
|
|
|
// Initialize descriptor manager
|
|
m_descriptorManager = std::make_unique<VulkanDescriptorManager>();
|
|
if (!m_descriptorManager->init(m_device.get(), rtDescriptorSupport)) {
|
|
mprintf(("Failed to initialize Vulkan descriptor manager!\n"));
|
|
return false;
|
|
}
|
|
setDescriptorManager(m_descriptorManager.get());
|
|
m_descriptorManager->buildFallbacks(m_bufferManager.get(), m_textureManager.get(), m_raytracingManager.get());
|
|
|
|
// Initialize pipeline manager
|
|
m_pipelineManager = std::make_unique<VulkanPipelineManager>();
|
|
if (!m_pipelineManager->init(m_device.get(), m_shaderManager.get(), m_descriptorManager.get())) {
|
|
nprintf(("vulkan", "Failed to initialize Vulkan pipeline manager!\n"));
|
|
return false;
|
|
}
|
|
setPipelineManager(m_pipelineManager.get());
|
|
m_pipelineManager->loadPipelineCache("vulkan_pipeline.cache");
|
|
|
|
// Initialize state tracker
|
|
m_stateTracker = std::make_unique<VulkanStateTracker>();
|
|
if (!m_stateTracker->init(m_device.get())) {
|
|
nprintf(("vulkan", "Failed to initialize Vulkan state tracker!\n"));
|
|
return false;
|
|
}
|
|
setStateTracker(m_stateTracker.get());
|
|
|
|
// Initialize draw manager
|
|
m_drawManager = std::make_unique<VulkanDrawManager>();
|
|
if (!m_drawManager->init(m_device.get())) {
|
|
nprintf(("vulkan", "Failed to initialize Vulkan draw manager!\n"));
|
|
return false;
|
|
}
|
|
setDrawManager(m_drawManager.get());
|
|
|
|
// Initialize post-processing
|
|
m_postProcessor = std::make_unique<VulkanPostProcessor>();
|
|
if (!m_postProcessor->init(m_device.get(), m_physicalDevice, m_memoryManager.get(),
|
|
m_renderExtent, m_depthFormat, m_hdrActive)) {
|
|
mprintf(("Warning: Failed to initialize Vulkan post-processor, post-processing will be disabled\n"));
|
|
m_postProcessor.reset();
|
|
} else {
|
|
setPostProcessor(m_postProcessor.get());
|
|
}
|
|
|
|
// Initialize shared post-processing manager (bloom/lightshaft settings, post-effect table)
|
|
// This is renderer-agnostic; OpenGL creates it in opengl_post_process_init().
|
|
if (!graphics::Post_processing_manager) {
|
|
graphics::Post_processing_manager = std::make_unique<graphics::PostProcessingManager>();
|
|
if (!graphics::Post_processing_manager->parse_table()) {
|
|
nprintf(("vulkan", "Warning: Unable to read post-processing table\n"));
|
|
}
|
|
}
|
|
|
|
// Initialize shared uniform buffer managers; this is renderer-agnostic and OpenGL
|
|
// creates it in gr_opengl_init().
|
|
gr_uniform_buffer_managers_init();
|
|
|
|
// Initialize query manager for GPU timestamp profiling
|
|
m_queryManager = std::make_unique<VulkanQueryManager>();
|
|
if (!m_queryManager->init(m_device.get(), m_physicalDevice.getProperties().limits.timestampPeriod,
|
|
m_graphicsCommandPool.get(), m_graphicsQueue)) {
|
|
nprintf(("vulkan", "Warning: Failed to initialize Vulkan query manager, GPU profiling will be disabled\n"));
|
|
m_queryManager.reset();
|
|
} else {
|
|
setQueryManager(m_queryManager.get());
|
|
}
|
|
|
|
// Prepare the rendering state by acquiring our first swap chain image
|
|
acquireNextSwapChainImage();
|
|
|
|
// Initialize ImGui Vulkan rendering backend
|
|
initImGui();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VulkanRenderer::initDisplayDevice() const
|
|
{
|
|
os::ViewPortProperties attrs;
|
|
attrs.enable_opengl = false;
|
|
attrs.enable_vulkan = true;
|
|
|
|
attrs.display = gr_get_preferred_display();
|
|
|
|
// 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 (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
|
|
break;
|
|
case os::ViewportState::Borderless:
|
|
attrs.flags.set(os::ViewPortFlags::Borderless);
|
|
break;
|
|
case os::ViewportState::Fullscreen:
|
|
attrs.flags.set(os::ViewPortFlags::Fullscreen);
|
|
break;
|
|
}
|
|
} else {
|
|
if (!Cmdline_window && !Cmdline_fullscreen_window) {
|
|
attrs.flags.set(os::ViewPortFlags::Fullscreen);
|
|
} else if (Cmdline_fullscreen_window) {
|
|
attrs.flags.set(os::ViewPortFlags::Borderless);
|
|
}
|
|
}
|
|
|
|
if (Cmdline_capture_mouse)
|
|
attrs.flags.set(os::ViewPortFlags::Capture_Mouse);
|
|
|
|
auto viewPort = m_graphicsOps->createViewport(attrs);
|
|
if (!viewPort) {
|
|
return false;
|
|
}
|
|
|
|
const auto port = os::addViewport(std::move(viewPort));
|
|
os::setMainViewPort(port);
|
|
|
|
return true;
|
|
}
|
|
bool VulkanRenderer::initializeInstance()
|
|
{
|
|
const auto vkGetInstanceProcAddr =
|
|
reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr());
|
|
|
|
VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);
|
|
|
|
VkInstanceCreateFlags createFlags = 0;
|
|
uint32_t count = 0;
|
|
|
|
auto extPtr = SDL_Vulkan_GetInstanceExtensions(&count);
|
|
|
|
if ( !extPtr ) {
|
|
mprintf(("Error in SDL_Vulkan_GetInstanceExtensions: %s\n", SDL_GetError()));
|
|
return false;
|
|
}
|
|
|
|
SCP_vector<const char*> extensions;
|
|
extensions.reserve(count);
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
// SDL 3.2 will include portability enueration extension even if it's not
|
|
// supported, so make sure not to add it blindly, and check for it later
|
|
if (SDL_strcmp(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, extPtr[i])) {
|
|
extensions.push_back(extPtr[i]);
|
|
}
|
|
}
|
|
|
|
const auto instanceVersion = vk::enumerateInstanceVersion();
|
|
gameversion::version vulkanVersion(VK_VERSION_MAJOR(instanceVersion),
|
|
VK_VERSION_MINOR(instanceVersion),
|
|
VK_VERSION_PATCH(instanceVersion),
|
|
0);
|
|
nprintf(("vulkan", "Vulkan instance version %s\n", gameversion::format_version(vulkanVersion).c_str()));
|
|
|
|
if (vulkanVersion < MinVulkanVersion) {
|
|
nprintf(("vulkan", "Vulkan version is less than the minimum which is %s.\n",
|
|
gameversion::format_version(MinVulkanVersion).c_str()));
|
|
return false;
|
|
}
|
|
|
|
// Enable the Vulkan validation layer + debug callbacks for a debug build,
|
|
// the engine graphics-debug switch (-gr_debug), or the standalone GPU
|
|
// synchronization-validation switch (-gr_sync_validation). The latter
|
|
// deliberately does NOT set Cmdline_graphics_debug_output, so it must be
|
|
// checked here explicitly.
|
|
const bool enableVulkanValidation = FSO_DEBUG || Cmdline_graphics_debug_output || Cmdline_gr_sync_validation;
|
|
|
|
const auto supportedExtensions = vk::enumerateInstanceExtensionProperties();
|
|
nprintf(("vulkan", "Instance extensions:\n"));
|
|
for (const auto& ext : supportedExtensions) {
|
|
mprintf((" Found support for %s version %" PRIu32 "\n", ext.extensionName.data(), ext.specVersion));
|
|
// Enables the driver to advertise HDR color spaces (e.g. HDR10 ST.2084)
|
|
// in vkGetPhysicalDeviceSurfaceFormatsKHR. Required to negotiate an HDR
|
|
// swap chain. Harmless to enable even when HDR output is disabled.
|
|
if (!stricmp(ext.extensionName, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) {
|
|
extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
|
|
mprintf((" Enabling %s (HDR color space support)\n", VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME));
|
|
}
|
|
if (enableVulkanValidation) {
|
|
if (!stricmp(ext.extensionName, VK_EXT_DEBUG_REPORT_EXTENSION_NAME)) {
|
|
extensions.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
|
|
m_debugReportEnabled = true;
|
|
}
|
|
if (!stricmp(ext.extensionName, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) {
|
|
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
|
|
m_debugUtilsEnabled = true;
|
|
}
|
|
}
|
|
|
|
// if portability enumeration extension is present then we need to add it
|
|
if ( !stricmp(ext.extensionName, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) ) {
|
|
extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
|
|
createFlags += VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR;
|
|
}
|
|
}
|
|
|
|
SCP_vector<const char*> layers;
|
|
bool khronosValidationEnabled = false;
|
|
const auto supportedLayers = vk::enumerateInstanceLayerProperties();
|
|
nprintf(("vulkan", "Instance layers:\n"));
|
|
for (const auto& layer : supportedLayers) {
|
|
nprintf(("vulkan", " Found layer %s(%s). Spec version %d.%d.%d and implementation %" PRIu32 "\n",
|
|
layer.layerName.data(),
|
|
layer.description.data(),
|
|
VK_VERSION_MAJOR(layer.specVersion),
|
|
VK_VERSION_MINOR(layer.specVersion),
|
|
VK_VERSION_PATCH(layer.specVersion),
|
|
layer.implementationVersion));
|
|
if (enableVulkanValidation) {
|
|
if (!stricmp(layer.layerName, "VK_LAYER_KHRONOS_validation")) {
|
|
layers.push_back("VK_LAYER_KHRONOS_validation");
|
|
khronosValidationEnabled = true;
|
|
} else if (!stricmp(layer.layerName, "VK_LAYER_LUNARG_core_validation")) {
|
|
layers.push_back("VK_LAYER_LUNARG_core_validation");
|
|
}
|
|
}
|
|
}
|
|
|
|
vk::ApplicationInfo appInfo(Window_title.c_str(), 1, EngineName, 1, VulkanApiVersion);
|
|
|
|
// Now we can make the Vulkan instance
|
|
vk::InstanceCreateInfo createInfo(vk::Flags<vk::InstanceCreateFlagBits>(createFlags), &appInfo);
|
|
createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size());
|
|
createInfo.ppEnabledExtensionNames = extensions.data();
|
|
createInfo.enabledLayerCount = static_cast<uint32_t>(layers.size());
|
|
createInfo.ppEnabledLayerNames = layers.data();
|
|
|
|
vk::DebugReportCallbackCreateInfoEXT createInstanceReportInfo(vk::DebugReportFlagBitsEXT::eError |
|
|
vk::DebugReportFlagBitsEXT::eWarning |
|
|
vk::DebugReportFlagBitsEXT::ePerformanceWarning);
|
|
createInstanceReportInfo.pfnCallback = debugReportCallback;
|
|
|
|
// Debug-utils messenger config. Chained into instance creation (so messages
|
|
// emitted during vkCreateInstance itself are captured) and reused below for
|
|
// the persistent messenger.
|
|
vk::DebugUtilsMessengerCreateInfoEXT messengerInfo;
|
|
messengerInfo.messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eError |
|
|
vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning;
|
|
messengerInfo.messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral |
|
|
vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation |
|
|
vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance;
|
|
messengerInfo.pfnUserCallback = debugUtilsMessengerCallback;
|
|
|
|
// Opt-in GPU synchronization validation (-gr_sync_validation). The struct is
|
|
// consumed by the Khronos validation layer, so it is only chained when that
|
|
// layer was actually enabled above.
|
|
const std::array<vk::ValidationFeatureEnableEXT, 1> enabledValidationFeatures = {
|
|
vk::ValidationFeatureEnableEXT::eSynchronizationValidation,
|
|
};
|
|
vk::ValidationFeaturesEXT validationFeatures;
|
|
validationFeatures.enabledValidationFeatureCount = static_cast<uint32_t>(enabledValidationFeatures.size());
|
|
validationFeatures.pEnabledValidationFeatures = enabledValidationFeatures.data();
|
|
|
|
vk::StructureChain<vk::InstanceCreateInfo,
|
|
vk::DebugReportCallbackCreateInfoEXT,
|
|
vk::DebugUtilsMessengerCreateInfoEXT,
|
|
vk::ValidationFeaturesEXT>
|
|
createInstanceChain(createInfo, createInstanceReportInfo, messengerInfo, validationFeatures);
|
|
|
|
// Prefer the debug-utils messenger; the deprecated debug-report callback is
|
|
// only a fallback for loaders without VK_EXT_debug_utils.
|
|
if (m_debugUtilsEnabled || !m_debugReportEnabled) {
|
|
createInstanceChain.unlink<vk::DebugReportCallbackCreateInfoEXT>();
|
|
}
|
|
if (!m_debugUtilsEnabled) {
|
|
createInstanceChain.unlink<vk::DebugUtilsMessengerCreateInfoEXT>();
|
|
}
|
|
if (Cmdline_gr_sync_validation && khronosValidationEnabled) {
|
|
mprintf(("Vulkan: Synchronization validation enabled (-gr_sync_validation)\n"));
|
|
} else {
|
|
if (Cmdline_gr_sync_validation) {
|
|
mprintf(("Vulkan: -gr_sync_validation requested but VK_LAYER_KHRONOS_validation is not available; "
|
|
"synchronization validation disabled\n"));
|
|
}
|
|
createInstanceChain.unlink<vk::ValidationFeaturesEXT>();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
VULKAN_HPP_DEFAULT_DISPATCHER.init(instance.get());
|
|
|
|
if (m_debugUtilsEnabled) {
|
|
m_debugMessenger = instance->createDebugUtilsMessengerEXTUnique(messengerInfo);
|
|
} else if (m_debugReportEnabled) {
|
|
vk::DebugReportCallbackCreateInfoEXT reportCreateInfo(vk::DebugReportFlagBitsEXT::eError |
|
|
vk::DebugReportFlagBitsEXT::eWarning |
|
|
vk::DebugReportFlagBitsEXT::ePerformanceWarning);
|
|
reportCreateInfo.pfnCallback = debugReportCallback;
|
|
|
|
m_debugReport = instance->createDebugReportCallbackEXTUnique(reportCreateInfo);
|
|
}
|
|
|
|
m_vkInstance = std::move(instance);
|
|
return true;
|
|
}
|
|
|
|
bool VulkanRenderer::initializeSurface()
|
|
{
|
|
const auto window = os::getSDLMainWindow();
|
|
|
|
VkSurfaceKHR surface;
|
|
if (!SDL_Vulkan_CreateSurface(window, static_cast<VkInstance>(*m_vkInstance), nullptr, &surface)) {
|
|
nprintf(("vulkan", "Failed to create vulkan surface: %s\n", SDL_GetError()));
|
|
return false;
|
|
}
|
|
|
|
const vk::detail::ObjectDestroy<vk::Instance, VULKAN_HPP_DEFAULT_DISPATCHER_TYPE> deleter(*m_vkInstance,
|
|
nullptr,
|
|
VULKAN_HPP_DEFAULT_DISPATCHER);
|
|
m_vkSurface = vk::UniqueSurfaceKHR(vk::SurfaceKHR(surface), deleter);
|
|
return true;
|
|
}
|
|
|
|
bool VulkanRenderer::pickPhysicalDevice(PhysicalDeviceValues& deviceValues)
|
|
{
|
|
const auto devices = m_vkInstance->enumeratePhysicalDevices();
|
|
if (devices.empty()) {
|
|
return false;
|
|
}
|
|
|
|
SCP_vector<PhysicalDeviceValues> values;
|
|
std::transform(devices.cbegin(), devices.cend(), std::back_inserter(values), [](const vk::PhysicalDevice& dev) {
|
|
PhysicalDeviceValues vals;
|
|
vals.device = dev;
|
|
vals.properties = dev.getProperties2().properties;
|
|
|
|
// Query ray tracing feature bits alongside the base features. It is
|
|
// safe to include extension feature structs here even if the device
|
|
// doesn't support the corresponding extension -- this is the standard
|
|
// way to discover whether to request it below. Extension *presence*
|
|
// is checked separately in createLogicalDevice() before enabling.
|
|
auto featureChain = dev.getFeatures2<vk::PhysicalDeviceFeatures2,
|
|
vk::PhysicalDeviceAccelerationStructureFeaturesKHR,
|
|
vk::PhysicalDeviceRayQueryFeaturesKHR,
|
|
vk::PhysicalDeviceBufferDeviceAddressFeatures,
|
|
vk::PhysicalDeviceSynchronization2Features>();
|
|
vals.features = featureChain.get<vk::PhysicalDeviceFeatures2>().features;
|
|
vals.accelerationStructureFeatureSupported =
|
|
featureChain.get<vk::PhysicalDeviceAccelerationStructureFeaturesKHR>().accelerationStructure != VK_FALSE;
|
|
vals.rayQueryFeatureSupported =
|
|
featureChain.get<vk::PhysicalDeviceRayQueryFeaturesKHR>().rayQuery != VK_FALSE;
|
|
vals.bufferDeviceAddressFeatureSupported =
|
|
featureChain.get<vk::PhysicalDeviceBufferDeviceAddressFeatures>().bufferDeviceAddress != VK_FALSE;
|
|
vals.synchronization2FeatureSupported =
|
|
featureChain.get<vk::PhysicalDeviceSynchronization2Features>().synchronization2 != VK_FALSE;
|
|
|
|
auto qprops = dev.getQueueFamilyProperties();
|
|
vals.queueProperties.assign(qprops.begin(), qprops.end());
|
|
return vals;
|
|
});
|
|
|
|
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(),
|
|
[this](PhysicalDeviceValues& value) { return isDeviceUnsuitable(value, m_vkSurface); }),
|
|
values.end());
|
|
if (values.empty()) {
|
|
return false;
|
|
}
|
|
|
|
// Sort the suitability of the devices in increasing order
|
|
std::sort(values.begin(), values.end(), compareDevices);
|
|
|
|
deviceValues = values.back();
|
|
mprintf(("Selected device %s (%d) as the primary Vulkan device.\n",
|
|
deviceValues.properties.deviceName.data(),
|
|
deviceValues.properties.deviceID));
|
|
mprintf(("Device extensions:\n"));
|
|
for (const auto& extProp : deviceValues.extensions) {
|
|
mprintf((" Found support for %s version %" PRIu32 "\n", extProp.extensionName.data(), extProp.specVersion));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VulkanRenderer::createLogicalDevice(const PhysicalDeviceValues& deviceValues)
|
|
{
|
|
float queuePriority = 1.0f;
|
|
|
|
SCP_vector<vk::DeviceQueueCreateInfo> queueInfos;
|
|
const std::set<uint32_t> familyIndices{deviceValues.graphicsQueueIndex.index,
|
|
deviceValues.presentQueueIndex.index};
|
|
|
|
queueInfos.reserve(familyIndices.size());
|
|
for (auto index : familyIndices) {
|
|
queueInfos.emplace_back(vk::DeviceQueueCreateFlags(), index, 1, &queuePriority);
|
|
}
|
|
|
|
// Build extension list: required + optional
|
|
SCP_vector<const char*> enabledExtensions(RequiredDeviceExtensions.begin(), RequiredDeviceExtensions.end());
|
|
|
|
// Check for VK_EXT_shader_viewport_index_layer (needed for shadow cascade routing)
|
|
m_supportsShaderViewportLayerOutput = false;
|
|
m_hdrMetadataSupported = false;
|
|
bool hasAccelerationStructureExt = false;
|
|
bool hasRayQueryExt = false;
|
|
bool hasDeferredHostOperationsExt = false;
|
|
for (const auto& ext : deviceValues.extensions) {
|
|
if (strcmp(ext.extensionName, VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME) == 0) {
|
|
m_supportsShaderViewportLayerOutput = true;
|
|
enabledExtensions.push_back(VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME);
|
|
mprintf(("Vulkan: Enabling %s (shadow cascade support)\n", VK_EXT_SHADER_VIEWPORT_INDEX_LAYER_EXTENSION_NAME));
|
|
}
|
|
// VK_EXT_hdr_metadata lets us advertise mastering display / content
|
|
// luminance to the compositor for HDR10 output. Optional.
|
|
if (strcmp(ext.extensionName, VK_EXT_HDR_METADATA_EXTENSION_NAME) == 0) {
|
|
m_hdrMetadataSupported = true;
|
|
enabledExtensions.push_back(VK_EXT_HDR_METADATA_EXTENSION_NAME);
|
|
mprintf(("Vulkan: Enabling %s (HDR10 metadata)\n", VK_EXT_HDR_METADATA_EXTENSION_NAME));
|
|
}
|
|
if (strcmp(ext.extensionName, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) == 0) {
|
|
hasAccelerationStructureExt = true;
|
|
}
|
|
if (strcmp(ext.extensionName, VK_KHR_RAY_QUERY_EXTENSION_NAME) == 0) {
|
|
hasRayQueryExt = true;
|
|
}
|
|
if (strcmp(ext.extensionName, VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME) == 0) {
|
|
hasDeferredHostOperationsExt = true;
|
|
}
|
|
}
|
|
|
|
// Raytraced shadows need the extension trio present AND their feature bits
|
|
// enabled. bufferDeviceAddress is core in Vulkan 1.2 (no extension name to
|
|
// check for), but its feature bit still has to be queried/enabled
|
|
// explicitly -- core promotion only drops the extension-name requirement.
|
|
m_supportsRaytracedShadows = hasAccelerationStructureExt && hasRayQueryExt && hasDeferredHostOperationsExt &&
|
|
deviceValues.accelerationStructureFeatureSupported && deviceValues.rayQueryFeatureSupported &&
|
|
deviceValues.bufferDeviceAddressFeatureSupported;
|
|
|
|
if (m_supportsRaytracedShadows) {
|
|
enabledExtensions.push_back(VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME);
|
|
enabledExtensions.push_back(VK_KHR_RAY_QUERY_EXTENSION_NAME);
|
|
enabledExtensions.push_back(VK_KHR_DEFERRED_HOST_OPERATIONS_EXTENSION_NAME);
|
|
mprintf(("Vulkan: Enabling ray tracing extensions (raytraced shadow support)\n"));
|
|
} else {
|
|
nprintf(("vulkan", "Vulkan: Raytraced shadows not available on this device "
|
|
"(extensions: accel_struct=%d ray_query=%d deferred_host_ops=%d; "
|
|
"features: accel_struct=%d ray_query=%d buffer_device_address=%d)\n",
|
|
hasAccelerationStructureExt, hasRayQueryExt, hasDeferredHostOperationsExt,
|
|
deviceValues.accelerationStructureFeatureSupported, deviceValues.rayQueryFeatureSupported,
|
|
deviceValues.bufferDeviceAddressFeatureSupported));
|
|
}
|
|
|
|
vk::DeviceCreateInfo deviceCreate;
|
|
deviceCreate.pQueueCreateInfos = queueInfos.data();
|
|
deviceCreate.queueCreateInfoCount = static_cast<uint32_t>(queueInfos.size());
|
|
|
|
deviceCreate.ppEnabledExtensionNames = enabledExtensions.data();
|
|
deviceCreate.enabledExtensionCount = static_cast<uint32_t>(enabledExtensions.size());
|
|
|
|
// Device features are enabled via a pNext-chained VkPhysicalDeviceFeatures2
|
|
// rather than the legacy DeviceCreateInfo::pEnabledFeatures, since the spec
|
|
// requires pEnabledFeatures to be null once a VkPhysicalDeviceFeatures2 is
|
|
// linked. The RT feature structs are always linked into the chain and
|
|
// unlinked when unsupported, mirroring the pattern used for the instance's
|
|
// optional debug report callback above.
|
|
// Enable ONLY the core device features the renderer actually uses, each gated
|
|
// on the device's support bit (deviceValues.features holds the queried support;
|
|
// an unsupported feature stays false and is therefore never requested). Enabling
|
|
// every supported feature would needlessly pull in ones this renderer never uses
|
|
// -- notably robustBufferAccess (per-access bounds-checking cost), plus
|
|
// wideLines, imageCubeArray, geometryShader, etc.
|
|
//
|
|
// Usage has been audited against the pipeline/sampler creation paths and the
|
|
// compiled shaders. Because this only ever narrows the enabled feature set,
|
|
// the failure mode of an audit gap is a used feature not being enabled; the
|
|
// reliable exit criterion is a -gr_sync_validation soak across the test matrix
|
|
// (deferred, MSAA, HDR, RT shadows, wireframe, RTT/env-map, decals) reporting
|
|
// zero "feature not enabled" messages -- static analysis can't prove
|
|
// completeness for shader-driven features.
|
|
vk::PhysicalDeviceFeatures2 deviceFeatures2;
|
|
const auto& supportedFeatures = deviceValues.features;
|
|
auto& enabledFeatures = deviceFeatures2.features;
|
|
enabledFeatures.samplerAnisotropy = supportedFeatures.samplerAnisotropy; // anisotropic texture filtering (VulkanTextureManager samplers)
|
|
enabledFeatures.independentBlend = supportedFeatures.independentBlend; // per-attachment blend states (deferred decal G-buffer write masks)
|
|
enabledFeatures.fillModeNonSolid = supportedFeatures.fillModeNonSolid; // wireframe (GR_FILL_MODE_WIRE -> vk::PolygonMode::eLine)
|
|
enabledFeatures.shaderClipDistance = supportedFeatures.shaderClipDistance; // gl_ClipDistance[] in main / shadow-map / default-material vertex shaders
|
|
enabledFeatures.textureCompressionBC = supportedFeatures.textureCompressionBC; // BC/DXT texture formats (optional; usage gated by isTextureCompressionBCSupported)
|
|
enabledFeatures.depthClamp = supportedFeatures.depthClamp; // clamp depth in the shadow pass (optional; when absent, CAPABILITY_SHADOWS reports false and shadows are disabled)
|
|
|
|
vk::PhysicalDeviceAccelerationStructureFeaturesKHR accelStructFeatures;
|
|
accelStructFeatures.accelerationStructure = VK_TRUE;
|
|
|
|
vk::PhysicalDeviceRayQueryFeaturesKHR rayQueryFeatures;
|
|
rayQueryFeatures.rayQuery = VK_TRUE;
|
|
|
|
vk::PhysicalDeviceBufferDeviceAddressFeatures bufferDeviceAddressFeatures;
|
|
bufferDeviceAddressFeatures.bufferDeviceAddress = VK_TRUE;
|
|
|
|
// VK_KHR_synchronization2 is required (RequiredDeviceExtensions,
|
|
// isDeviceUnsuitable() already rejected devices without the feature bit),
|
|
// so this is always linked -- unlike the RT trio below, it's never unlinked.
|
|
vk::PhysicalDeviceSynchronization2Features sync2Features;
|
|
sync2Features.synchronization2 = VK_TRUE;
|
|
|
|
vk::StructureChain<vk::DeviceCreateInfo,
|
|
vk::PhysicalDeviceFeatures2,
|
|
vk::PhysicalDeviceAccelerationStructureFeaturesKHR,
|
|
vk::PhysicalDeviceRayQueryFeaturesKHR,
|
|
vk::PhysicalDeviceBufferDeviceAddressFeatures,
|
|
vk::PhysicalDeviceSynchronization2Features>
|
|
deviceCreateChain(deviceCreate, deviceFeatures2, accelStructFeatures, rayQueryFeatures,
|
|
bufferDeviceAddressFeatures, sync2Features);
|
|
|
|
if (!m_supportsRaytracedShadows) {
|
|
deviceCreateChain.unlink<vk::PhysicalDeviceAccelerationStructureFeaturesKHR>();
|
|
deviceCreateChain.unlink<vk::PhysicalDeviceRayQueryFeaturesKHR>();
|
|
deviceCreateChain.unlink<vk::PhysicalDeviceBufferDeviceAddressFeatures>();
|
|
}
|
|
|
|
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());
|
|
|
|
// Create queues
|
|
m_graphicsQueue = m_device->getQueue(deviceValues.graphicsQueueIndex.index, 0);
|
|
m_presentQueue = m_device->getQueue(deviceValues.presentQueueIndex.index, 0);
|
|
|
|
// Store physical device and queue family indices for later use
|
|
m_physicalDevice = deviceValues.device;
|
|
// Cache properties + features once (the limit/feature getters read these).
|
|
m_deviceProperties = m_physicalDevice.getProperties();
|
|
m_deviceFeatures = m_physicalDevice.getFeatures();
|
|
m_graphicsQueueFamilyIndex = deviceValues.graphicsQueueIndex.index;
|
|
m_presentQueueFamilyIndex = deviceValues.presentQueueIndex.index;
|
|
|
|
// Initialize memory manager
|
|
m_memoryManager = std::make_unique<VulkanMemoryManager>();
|
|
if (!m_memoryManager->init(m_vkInstance.get(), m_physicalDevice, m_device.get(), m_supportsRaytracedShadows)) {
|
|
mprintf(("Failed to initialize Vulkan memory manager!\n"));
|
|
return false;
|
|
}
|
|
setMemoryManager(m_memoryManager.get());
|
|
|
|
// Initialize deletion queue for deferred resource destruction
|
|
m_deletionQueue = std::make_unique<VulkanDeletionQueue>();
|
|
m_deletionQueue->init(m_device.get(), m_memoryManager.get());
|
|
setDeletionQueue(m_deletionQueue.get());
|
|
|
|
// Initialize buffer manager
|
|
m_bufferManager = std::make_unique<VulkanBufferManager>();
|
|
if (!m_bufferManager->init(m_device.get(), m_memoryManager.get(),
|
|
m_graphicsQueueFamilyIndex,
|
|
getMinUniformBufferOffsetAlignment())) {
|
|
nprintf(("vulkan", "Failed to initialize Vulkan buffer manager!\n"));
|
|
return false;
|
|
}
|
|
setBufferManager(m_bufferManager.get());
|
|
// Set initial frame index for buffer manager
|
|
m_bufferManager->setCurrentFrame(m_currentFrame, m_frameNumber);
|
|
|
|
return true;
|
|
}
|
|
bool VulkanRenderer::createSwapChain(const PhysicalDeviceValues& deviceValues, vk::SwapchainKHR oldSwapchain)
|
|
{
|
|
// Choose one more than the minimum to avoid driver synchronization if it is not done with a thread yet
|
|
uint32_t imageCount = deviceValues.surfaceCapabilities.minImageCount + 1;
|
|
if (deviceValues.surfaceCapabilities.maxImageCount > 0 &&
|
|
imageCount > deviceValues.surfaceCapabilities.maxImageCount) {
|
|
imageCount = deviceValues.surfaceCapabilities.maxImageCount;
|
|
}
|
|
|
|
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, window.width, window.height);
|
|
createInfo.imageArrayLayers = 1;
|
|
createInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment
|
|
| vk::ImageUsageFlagBits::eTransferSrc
|
|
| vk::ImageUsageFlagBits::eTransferDst;
|
|
|
|
const uint32_t queueFamilyIndices[] = {deviceValues.graphicsQueueIndex.index, deviceValues.presentQueueIndex.index};
|
|
if (deviceValues.graphicsQueueIndex.index != deviceValues.presentQueueIndex.index) {
|
|
createInfo.imageSharingMode = vk::SharingMode::eConcurrent;
|
|
createInfo.queueFamilyIndexCount = 2;
|
|
createInfo.pQueueFamilyIndices = queueFamilyIndices;
|
|
} else {
|
|
createInfo.imageSharingMode = vk::SharingMode::eExclusive;
|
|
}
|
|
|
|
createInfo.preTransform = deviceValues.surfaceCapabilities.currentTransform;
|
|
createInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque;
|
|
createInfo.presentMode = choosePresentMode(deviceValues);
|
|
createInfo.clipped = true;
|
|
createInfo.oldSwapchain = oldSwapchain;
|
|
|
|
auto newSwapChain = m_device->createSwapchainKHRUnique(createInfo);
|
|
|
|
// Clear old resources before replacing the swap chain
|
|
m_swapChainFramebuffers.clear();
|
|
m_swapChainImageViews.clear();
|
|
|
|
m_swapChain = std::move(newSwapChain);
|
|
|
|
auto swapChainImages = m_device->getSwapchainImagesKHR(m_swapChain.get());
|
|
m_swapChainImages.assign(swapChainImages.begin(), swapChainImages.end());
|
|
m_swapChainImageFormat = surfaceFormat.format;
|
|
m_swapChainColorSpace = surfaceFormat.colorSpace;
|
|
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) {
|
|
vk::ImageViewCreateInfo viewCreateInfo;
|
|
viewCreateInfo.image = image;
|
|
viewCreateInfo.viewType = vk::ImageViewType::e2D;
|
|
viewCreateInfo.format = m_swapChainImageFormat;
|
|
|
|
viewCreateInfo.components.r = vk::ComponentSwizzle::eIdentity;
|
|
viewCreateInfo.components.g = vk::ComponentSwizzle::eIdentity;
|
|
viewCreateInfo.components.b = vk::ComponentSwizzle::eIdentity;
|
|
viewCreateInfo.components.a = vk::ComponentSwizzle::eIdentity;
|
|
|
|
viewCreateInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
|
|
viewCreateInfo.subresourceRange.baseMipLevel = 0;
|
|
viewCreateInfo.subresourceRange.levelCount = 1;
|
|
viewCreateInfo.subresourceRange.baseArrayLayer = 0;
|
|
viewCreateInfo.subresourceRange.layerCount = 1;
|
|
|
|
m_swapChainImageViews.push_back(m_device->createImageViewUnique(viewCreateInfo));
|
|
}
|
|
|
|
// No layout transition needed for the new images: the only pass that writes
|
|
// them (m_encodeRenderPass) uses initialLayout=eUndefined with
|
|
// loadOp=eDontCare, so their first use never reads prior contents.
|
|
|
|
// Advertise HDR10 mastering/content metadata to the compositor when active.
|
|
if (m_hdrActive && m_hdrMetadataSupported) {
|
|
vk::HdrMetadataEXT metadata;
|
|
// BT.2020 display primaries and D65 white point
|
|
metadata.displayPrimaryRed = vk::XYColorEXT{0.708f, 0.292f};
|
|
metadata.displayPrimaryGreen = vk::XYColorEXT{0.170f, 0.797f};
|
|
metadata.displayPrimaryBlue = vk::XYColorEXT{0.131f, 0.046f};
|
|
metadata.whitePoint = vk::XYColorEXT{0.3127f, 0.3290f};
|
|
metadata.maxLuminance = Gr_hdr_peak_nits;
|
|
metadata.minLuminance = 0.0f;
|
|
metadata.maxContentLightLevel = Gr_hdr_peak_nits;
|
|
metadata.maxFrameAverageLightLevel = Gr_hdr_paperwhite_nits;
|
|
m_device->setHdrMetadataEXT(m_swapChain.get(), metadata);
|
|
mprintf(("Vulkan: HDR10 metadata set (peak %.0f nits, paper white %.0f nits)\n",
|
|
Gr_hdr_peak_nits, Gr_hdr_paperwhite_nits));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool VulkanRenderer::recreateSwapChain()
|
|
{
|
|
nprintf(("vulkan", "Vulkan: Recreating swap chain...\n"));
|
|
|
|
// Wait for all frames to finish so no resources are in use
|
|
for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i) {
|
|
m_frames[i]->waitForFinish();
|
|
}
|
|
m_device->waitIdle();
|
|
|
|
// Re-query surface state (may have changed due to resize/compositor)
|
|
PhysicalDeviceValues freshValues;
|
|
freshValues.device = m_physicalDevice;
|
|
freshValues.surfaceCapabilities = m_physicalDevice.getSurfaceCapabilitiesKHR(m_vkSurface.get());
|
|
auto fmts = m_physicalDevice.getSurfaceFormatsKHR(m_vkSurface.get());
|
|
freshValues.surfaceFormats.assign(fmts.begin(), fmts.end());
|
|
auto modes = m_physicalDevice.getSurfacePresentModesKHR(m_vkSurface.get());
|
|
freshValues.presentModes.assign(modes.begin(), modes.end());
|
|
freshValues.graphicsQueueIndex = {true, m_graphicsQueueFamilyIndex};
|
|
freshValues.presentQueueIndex = {true, m_presentQueueFamilyIndex};
|
|
|
|
// Check for 0x0 extent (minimized window) — caller should retry later
|
|
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;
|
|
}
|
|
|
|
// Recreate all size-dependent resources. The render passes (including
|
|
// m_encodeRenderPass) are intentionally NOT recreated so cached pipelines
|
|
// remain valid; only images, views, and framebuffers are rebuilt.
|
|
const vk::Format oldSwapChainFormat = m_swapChainImageFormat;
|
|
createSwapChain(freshValues, m_swapChain.get());
|
|
|
|
// Known limitation: if the surface format changes across recreation (e.g.
|
|
// the window moves to a display that flips HDR10 availability),
|
|
// m_encodeRenderPass and the post-processor's LDR format would need a full
|
|
// rebuild, which we don't support yet. Log it loudly.
|
|
if (m_swapChainImageFormat != oldSwapChainFormat) {
|
|
mprintf(("Vulkan: WARNING - swap chain surface format changed across recreation (%d -> %d); "
|
|
"rendering may be broken until restart\n",
|
|
static_cast<int>(oldSwapChainFormat), static_cast<int>(m_swapChainImageFormat)));
|
|
}
|
|
|
|
// The depth buffer is extent-sized; recreate it before the framebuffers
|
|
// that attach its view. createDepthResources() verifies the format is stable
|
|
// (the kept render passes bake it in).
|
|
destroyDepthResources();
|
|
createDepthResources();
|
|
|
|
createCompositionResources();
|
|
createFrameBuffers();
|
|
|
|
// 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_renderExtent)) {
|
|
mprintf(("Vulkan: post-processor resize failed, disabling post-processing!\n"));
|
|
setPostProcessor(nullptr);
|
|
m_postProcessor->shutdown();
|
|
m_postProcessor.reset();
|
|
}
|
|
|
|
// Drop renderer-side cached state that may reference destroyed views
|
|
if (m_drawManager) {
|
|
m_drawManager->onResize();
|
|
}
|
|
m_sceneDepthCopiedThisFrame = false;
|
|
|
|
// Update VulkanRenderFrame handles to point to the new swap chain, and
|
|
// recreate their semaphores: an acquire that succeeded against the old swap
|
|
// chain but was never consumed by a submit leaves the image-available
|
|
// semaphore signaled. All frames are idle here (waited above), so
|
|
// recreating is safe and unambiguous.
|
|
for (auto& frame : m_frames) {
|
|
frame->updateSwapChain(m_swapChain.get());
|
|
frame->recreateSyncObjects();
|
|
}
|
|
|
|
// Reset swap chain image tracking
|
|
m_swapChainImageRenderImage.clear();
|
|
m_swapChainImageRenderImage.resize(m_swapChainImages.size(), nullptr);
|
|
m_previousSwapChainImage = UINT32_MAX;
|
|
|
|
m_swapChainNeedsRecreation = false;
|
|
|
|
nprintf(("vulkan", "Vulkan: Swap chain recreated successfully (%ux%u, %zu images)\n",
|
|
m_swapChainExtent.width, m_swapChainExtent.height, m_swapChainImages.size()));
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace graphics::vulkan
|