Files
fs2open.github.com/code/graphics/vulkan/gr_vulkan.cpp
T
BMagnu a73e2565c8 Add Vulkan + Wayland init logic for OpenXR (#7767)
* Add Vulkan + Wayland init logic for OpenXR

* typo

* clang tidy
2026-09-08 10:48:33 -04:00

619 lines
20 KiB
C++

#include "gr_vulkan.h"
#include "VulkanRenderer.h"
#include "VulkanBuffer.h"
#include "VulkanTexture.h"
#include "VulkanShader.h"
#include "VulkanDescriptorManager.h"
#include "VulkanPipeline.h"
#include "VulkanQuery.h"
#include "VulkanState.h"
#include "VulkanDraw.h"
#include "VulkanDeferred.h"
#include "VulkanOpenXR.h"
#include "VulkanPostProcessing.h"
#include "backends/imgui_impl_sdl3.h"
#include "backends/imgui_impl_vulkan.h"
#include "osapi/osapi.h"
#include "graphics/util/pixel_swizzle.h"
#include "bmpman/bmpman.h"
#include "cfile/cfile.h"
#include "cmdline/cmdline.h"
#include "graphics/2d.h"
#include "graphics/matrix.h"
#include "graphics/material.h"
#include "graphics/post_processing.h"
#include "graphics/grinternal.h"
#include "graphics/shadows.h"
#include "lighting/lighting.h"
#include "pngutils/pngutils.h"
#if defined(_WIN32)
#include <direct.h>
#endif
namespace graphics::vulkan {
namespace {
std::unique_ptr<VulkanRenderer> renderer_instance;
// Sync object for tracking frame completion
struct VulkanSyncObject {
uint64_t frameNumber;
};
// ========== Renderer-level functions ==========
void vulkan_setup_frame()
{
auto* renderer = getRendererInstance();
renderer->setupFrame();
}
void vulkan_flip()
{
renderer_instance->flip();
}
void vulkan_model_loaded(int pm_id)
{
if (auto* rt = getRaytracingManager()) {
rt->onModelLoaded(pm_id);
}
}
void vulkan_model_unloaded(int pm_id)
{
if (auto* rt = getRaytracingManager()) {
rt->onModelUnloaded(pm_id);
}
}
void vulkan_build_shadow_tlas()
{
if (auto* rt = getRaytracingManager()) {
// vkCmdBuildAccelerationStructuresKHR (and the memory barrier that follows
// it in buildTlas()) must be recorded outside any render pass instance.
// This is called once per frame, before the shadow map render pass begins,
// while the previous render pass (G-buffer/scene) is typically still
// active -- end it here. vulkan_shadow_map_start()'s first_pass branch
// skips its own endRenderPass() call when it finds none active.
auto* stateTracker = getStateTracker();
if (stateTracker->getCurrentRenderPass()) {
stateTracker->getCommandBuffer().endRenderPass();
stateTracker->setRenderPass(vk::RenderPass());
}
rt->buildTlas();
// Refresh the Global set's live TLAS fallback so every writeSet(Global)
// this frame picks up the freshly built TLAS automatically -- see
// DescriptorFallbacks::shadowTlas.
getDescriptorManager()->setCurrentShadowTlas(rt->getTlasForShaderBinding());
// The memoized per-frame Global set (VulkanDrawManager) must rebuild so the
// new TLAS reaches subsequent draws' Set 0 instead of a cached stale one.
getDrawManager()->invalidateGlobalSet();
}
}
bool vulkan_is_capable(gr_capability capability)
{
switch (capability) {
case gr_capability::CAPABILITY_ENVIRONMENT_MAP:
return true;
case gr_capability::CAPABILITY_NORMAL_MAP:
return Cmdline_normal != 0;
case gr_capability::CAPABILITY_HEIGHT_MAP:
return Cmdline_height != 0;
case gr_capability::CAPABILITY_SOFT_PARTICLES:
return Gr_post_processing_enabled;
case gr_capability::CAPABILITY_DISTORTION:
return Gr_post_processing_enabled;
case gr_capability::CAPABILITY_POST_PROCESSING:
return Gr_post_processing_enabled;
case gr_capability::CAPABILITY_DEFERRED_LIGHTING:
return light_deferred_enabled();
case gr_capability::CAPABILITY_SHADOWS:
// Depth clamp is a hard requirement, not a nice-to-have: the shadow pass
// relies on it so geometry beyond the light frustum's far plane still
// writes depth instead of being clipped, otherwise nearby shadows break
// up. It's an optional Vulkan device feature (unlike OpenGL, where it's
// core since 3.2), so when it's unavailable we report no shadow support
// at all and let gr_init disable shadows gracefully rather than render
// them broken.
return getRendererInstance()->supportsShaderViewportLayerOutput() &&
getRendererInstance()->isDepthClampSupported();
case gr_capability::CAPABILITY_THICK_OUTLINE:
return false;
case gr_capability::CAPABILITY_BATCHED_SUBMODELS:
return true;
case gr_capability::CAPABILITY_TIMESTAMP_QUERY:
return getQueryManager() != nullptr;
case gr_capability::CAPABILITY_SEPARATE_BLEND_FUNCTIONS:
// Vulkan supports per-attachment blend by spec
return true;
case gr_capability::CAPABILITY_PERSISTENT_BUFFER_MAPPING:
// Vulkan has persistently mappable host-visible memory
return true;
case gr_capability::CAPABILITY_BPTC:
return getRendererInstance()->isTextureCompressionBCSupported();
case gr_capability::CAPABILITY_S3TC:
// Vulkan always supports BC1/BC2/BC3 (S3TC equivalent) as core features
return true;
case gr_capability::CAPABILITY_LARGE_SHADER:
// Same troubleshooting switch as OpenGL: -no_large_shaders splits the model
// ubershader into per-flag-combination variants for drivers that choke on the
// single large program (see main_large.sdr / main_small.sdr).
return !Cmdline_no_large_shaders;
case gr_capability::CAPABILITY_INSTANCED_RENDERING:
return true;
case gr_capability::CAPABILITY_FAST_SHADOWS:
// Vulkan always writes gl_Layer directly from the vertex shader (see
// shadow_map-v.sdr), so it never needs the geometry-shader cascade fallback.
return getRendererInstance()->supportsShaderViewportLayerOutput();
case gr_capability::CAPABILITY_QUERIES_REUSABLE:
// Vulkan queries require explicit reset between read and write.
// The backend manages this lifecycle internally via deleteQueryObject.
return false;
case gr_capability::CAPABILITY_RAYTRACED_SHADOWS:
return getRendererInstance()->supportsRaytracedShadows();
}
return false;
}
bool vulkan_get_property(gr_property prop, void* dest)
{
auto* renderer = getRendererInstance();
switch (prop) {
case gr_property::UNIFORM_BUFFER_OFFSET_ALIGNMENT:
*reinterpret_cast<int*>(dest) = static_cast<int>(renderer->getMinUniformBufferOffsetAlignment());
return true;
case gr_property::UNIFORM_BUFFER_MAX_SIZE:
*reinterpret_cast<int*>(dest) = static_cast<int>(renderer->getMaxUniformBufferSize());
return true;
case gr_property::MAX_ANISOTROPY:
*reinterpret_cast<float*>(dest) = renderer->getMaxAnisotropy();
return true;
default:
return false;
}
}
void vulkan_push_debug_group(const char* name)
{
auto* renderer = getRendererInstance();
if (!renderer->isDebugUtilsEnabled()) {
return;
}
auto* stateTracker = getStateTracker();
vk::DebugUtilsLabelEXT label;
label.pLabelName = name;
label.color = {{ 1.0f, 1.0f, 1.0f, 1.0f }};
stateTracker->getCommandBuffer().beginDebugUtilsLabelEXT(label);
}
void vulkan_pop_debug_group()
{
auto* renderer = getRendererInstance();
if (!renderer->isDebugUtilsEnabled()) {
return;
}
auto* stateTracker = getStateTracker();
stateTracker->getCommandBuffer().endDebugUtilsLabelEXT();
}
void vulkan_imgui_new_frame()
{
ImGui_ImplVulkan_NewFrame();
}
void vulkan_imgui_render_draw_data()
{
auto* renderer = getRendererInstance();
if (renderer) {
ImGui_ImplVulkan_RenderDrawData(ImGui::GetDrawData(), renderer->getVkCurrentCommandBuffer());
// ImGui recorded its own pipeline/descriptor/viewport/scissor binds
// directly on the command buffer, mid-pass. Anything the engine draws
// before the next pass boundary (e.g. gr_flip's debug overlay or cached
// UI model instances) would otherwise run with ImGui's pipeline still
// bound because the tracker believes its own pipeline is current.
getStateTracker()->invalidateExternalBindings();
}
}
gr_sync vulkan_sync_fence()
{
auto* renderer = getRendererInstance();
auto* sync = new VulkanSyncObject();
sync->frameNumber = renderer->getCurrentFrameNumber();
return static_cast<gr_sync>(sync);
}
bool vulkan_sync_wait(gr_sync sync, uint64_t timeoutns)
{
if (!sync) {
return true;
}
auto* renderer = getRendererInstance();
auto* syncObj = static_cast<VulkanSyncObject*>(sync);
// Wait on the specific frame's fence. Honestly reports "not complete" on
// timeout or when the fence was taken during the still-recording frame --
// callers (e.g. UniformBufferManager's segment fences) depend on this to
// know whether the GPU is done with a resource.
return renderer->waitForFrame(syncObj->frameNumber, timeoutns);
}
void vulkan_sync_delete(gr_sync sync)
{
if (sync) {
delete static_cast<VulkanSyncObject*>(sync);
}
}
// ========== Screen capture (save/restore, screenshots) ==========
ubyte* Vulkan_saved_screen = nullptr;
int Vulkan_saved_screen_id = -1;
int vulkan_save_screen()
{
if (Vulkan_saved_screen) {
// Already have a saved screen
return -1;
}
ubyte* pixels = nullptr;
uint32_t w, h;
if (!renderer_instance->readbackFramebuffer(&pixels, &w, &h)) {
return -1;
}
int bmpId = bm_create(32, static_cast<int>(w), static_cast<int>(h), pixels, 0);
if (bmpId < 0) {
vm_free(pixels);
return -1;
}
Vulkan_saved_screen = pixels;
Vulkan_saved_screen_id = bmpId;
return Vulkan_saved_screen_id;
}
void vulkan_restore_screen(int bmp_id)
{
gr_reset_clip();
if (!Vulkan_saved_screen) {
gr_clear();
return;
}
Assert((bmp_id < 0) || (bmp_id == Vulkan_saved_screen_id));
if (Vulkan_saved_screen_id < 0) {
return;
}
gr_set_bitmap(Vulkan_saved_screen_id);
gr_bitmap(0, 0, GR_RESIZE_NONE);
}
void vulkan_free_screen(int bmp_id)
{
if (!Vulkan_saved_screen) {
return;
}
vm_free(Vulkan_saved_screen);
Vulkan_saved_screen = nullptr;
Assert((bmp_id < 0) || (bmp_id == Vulkan_saved_screen_id));
if (Vulkan_saved_screen_id >= 0) {
bm_release(Vulkan_saved_screen_id);
Vulkan_saved_screen_id = -1;
}
}
void vulkan_print_screen(const char* filename)
{
ubyte* pixels = nullptr;
uint32_t w, h;
if (!renderer_instance->readbackFramebuffer(&pixels, &w, &h)) {
return;
}
graphics::util::swizzle_bgra_to_rgba(pixels, static_cast<size_t>(w) * h);
char tmp[MAX_PATH_LEN];
snprintf(tmp, MAX_PATH_LEN - 1, "screenshots/%s.png", filename);
_mkdir(os_get_config_path("screenshots").c_str());
if (!png_write_bitmap(os_get_config_path(tmp).c_str(), w, h, false, pixels)) {
ReleaseWarning(LOCATION, "Failed to write screenshot to \"%s\".", os_get_config_path(tmp).c_str());
}
vm_free(pixels);
}
SCP_string vulkan_blob_screen()
{
ubyte* pixels = nullptr;
uint32_t w, h;
// If an off-screen render target is bound (e.g. SCPUI generating ship/weapon icons via
// gr.setTarget + gr.screenToBlob), capture that target rather than the on-screen frame.
// Its color image is R8G8B8A8_UNORM, already in RGBA order with real alpha, so it needs
// no BGRA swizzle. Reading the framebuffer here instead would grab whatever is currently
// on screen (and force it opaque), which is exactly the icon corruption bug.
auto* texManager = getTextureManager();
const int rtHandle = texManager ? texManager->getCurrentRenderTarget() : -1;
if (rtHandle >= 0) {
static bool logged_rt_capture = false;
if (!logged_rt_capture) {
logged_rt_capture = true;
mprintf(("Vulkan: gr.screenToBlob capturing active render target (handle %i) instead of the "
"framebuffer (logged once).\n", rtHandle));
}
auto* ts = texManager->getTextureSlot(rtHandle);
if (ts && renderer_instance->readbackRenderTarget(ts, &pixels, &w, &h)) {
// Flip vertically to match OpenGL's gr_opengl_blob_screen (which also passes y_flip=true).
// OpenGL render targets are stored bottom-up, so its readback flips to produce an upright
// PNG; Vulkan stores top-down but reads row 0 first, so the same flip is required to yield
// the SAME orientation OpenGL produces. Mods drive screenToBlob assuming OpenGL behavior
// (e.g. SCPUI deliberately draws its source icons V-flipped into the target to cancel this
// flip), so matching OpenGL here keeps that content correct instead of upside-down.
SCP_string result = png_b64_bitmap(w, h, true, pixels);
vm_free(pixels);
return "data:image/png;base64," + result;
}
return "";
}
if (!renderer_instance->readbackFramebuffer(&pixels, &w, &h)) {
return "";
}
graphics::util::swizzle_bgra_to_rgba(pixels, static_cast<size_t>(w) * h);
SCP_string result = png_b64_bitmap(w, h, false, pixels);
vm_free(pixels);
return "data:image/png;base64," + result;
}
// get_region: intentional no-op. The only caller is neb2_pre_render() in
// NEB2_RENDER_POF mode, which renders a 32x32 background thumbnail into a
// CPU buffer that is never actually read — the pixel data, ex_scale, and
// ey_scale it computes have no consumers. Modern nebula rendering uses
// NEB2_RENDER_HTL (fog color + gr_clear) and doesn't need get_region at all.
void vulkan_get_region(int /*front*/, int /*w*/, int /*h*/, ubyte* /*data*/) {}
void stub_dump_envmap(const char* /*filename*/) {}
std::unique_ptr<os::Viewport> stub_create_viewport(const os::ViewPortProperties& /*props*/)
{
return {};
}
void stub_use_viewport(os::Viewport* /*view*/) {}
// ========== Function pointer table ==========
// Implementations are defined in their respective files:
// VulkanDraw.cpp, VulkanBuffer.cpp, VulkanTexture.cpp, VulkanShader.cpp, VulkanState.cpp
void init_function_pointers()
{
// function pointers...
gr_screen.gf_setup_frame = vulkan_setup_frame;
gr_screen.gf_set_clip = vulkan_set_clip;
gr_screen.gf_reset_clip = vulkan_reset_clip;
gr_screen.gf_clear = vulkan_clear;
gr_screen.gf_print_screen = vulkan_print_screen;
gr_screen.gf_blob_screen = vulkan_blob_screen;
gr_screen.gf_zbuffer_get = vulkan_zbuffer_get;
gr_screen.gf_zbuffer_set = vulkan_zbuffer_set;
gr_screen.gf_zbuffer_clear = vulkan_zbuffer_clear;
gr_screen.gf_stencil_set = vulkan_stencil_set;
gr_screen.gf_stencil_clear = vulkan_stencil_clear;
gr_screen.gf_alpha_mask_set = vulkan_alpha_mask_set;
gr_screen.gf_save_screen = vulkan_save_screen;
gr_screen.gf_restore_screen = vulkan_restore_screen;
gr_screen.gf_free_screen = vulkan_free_screen;
gr_screen.gf_get_region = vulkan_get_region;
// now for the bitmap functions
gr_screen.gf_bm_free_data = vulkan_bm_free_data;
gr_screen.gf_bm_create = vulkan_bm_create;
gr_screen.gf_bm_init = vulkan_bm_init;
gr_screen.gf_bm_page_in_start = vulkan_bm_page_in_start;
gr_screen.gf_bm_data = vulkan_bm_data;
gr_screen.gf_bm_make_render_target = vulkan_bm_make_render_target;
gr_screen.gf_bm_set_render_target = vulkan_bm_set_render_target;
gr_screen.gf_set_cull = vulkan_set_cull;
gr_screen.gf_set_color_buffer = vulkan_set_color_buffer;
gr_screen.gf_set_clear_color = vulkan_set_clear_color;
gr_screen.gf_preload = vulkan_preload;
gr_screen.gf_set_texture_addressing = vulkan_set_texture_addressing;
gr_screen.gf_zbias = vulkan_zbias;
gr_screen.gf_set_fill_mode = vulkan_set_fill_mode;
gr_screen.gf_create_buffer = vulkan_create_buffer;
gr_screen.gf_delete_buffer = vulkan_delete_buffer;
gr_screen.gf_model_loaded = vulkan_model_loaded;
gr_screen.gf_model_unloaded = vulkan_model_unloaded;
gr_screen.gf_build_shadow_tlas = vulkan_build_shadow_tlas;
gr_screen.gf_update_transform_buffer = vulkan_update_transform_buffer;
gr_screen.gf_update_buffer_data = vulkan_update_buffer_data;
gr_screen.gf_update_buffer_data_offset = vulkan_update_buffer_data_offset;
gr_screen.gf_map_buffer = vulkan_map_buffer;
gr_screen.gf_flush_mapped_buffer = vulkan_flush_mapped_buffer;
gr_screen.gf_post_process_set_effect = vulkan_post_process_set_effect;
gr_screen.gf_post_process_set_defaults = vulkan_post_process_set_defaults;
gr_screen.gf_post_process_begin = vulkan_post_process_begin;
gr_screen.gf_post_process_end = vulkan_post_process_end;
gr_screen.gf_post_process_save_zbuffer = vulkan_post_process_save_zbuffer;
gr_screen.gf_post_process_restore_zbuffer = vulkan_post_process_restore_zbuffer;
gr_screen.gf_scene_texture_begin = vulkan_scene_texture_begin;
gr_screen.gf_scene_texture_end = vulkan_scene_texture_end;
gr_screen.gf_copy_effect_texture = vulkan_copy_effect_texture;
gr_screen.gf_deferred_lighting_begin = vulkan_deferred_lighting_begin;
gr_screen.gf_deferred_lighting_msaa = vulkan_deferred_lighting_msaa;
gr_screen.gf_deferred_lighting_end = vulkan_deferred_lighting_end;
gr_screen.gf_deferred_lighting_finish = vulkan_deferred_lighting_finish;
gr_screen.gf_calculate_irrmap = vulkan_calculate_irrmap;
gr_screen.gf_dump_envmap = stub_dump_envmap;
gr_screen.gf_override_fog = vulkan_override_fog;
gr_screen.gf_imgui_new_frame = vulkan_imgui_new_frame;
gr_screen.gf_imgui_render_draw_data = vulkan_imgui_render_draw_data;
gr_screen.gf_set_line_width = vulkan_set_line_width;
gr_screen.gf_sphere = vulkan_draw_sphere;
gr_screen.gf_shadow_map_start = vulkan_shadow_map_start;
gr_screen.gf_shadow_map_end = vulkan_shadow_map_end;
gr_screen.gf_start_decal_pass = vulkan_start_decal_pass;
gr_screen.gf_stop_decal_pass = vulkan_stop_decal_pass;
gr_screen.gf_render_decals = vulkan_render_decals;
gr_screen.gf_render_shield_impact = vulkan_render_shield_impact;
gr_screen.gf_maybe_create_shader = vulkan_maybe_create_shader;
gr_screen.gf_recompile_all_shaders = vulkan_recompile_all_shaders;
gr_screen.gf_clear_states = vulkan_clear_states;
gr_screen.gf_update_texture = vulkan_update_texture;
gr_screen.gf_get_bitmap_from_texture = vulkan_get_bitmap_from_texture;
gr_screen.gf_render_model = vulkan_render_model;
gr_screen.gf_render_shadow_draw = vulkan_render_shadow_draw;
gr_screen.gf_render_primitives = vulkan_render_primitives;
gr_screen.gf_render_primitives_particle = vulkan_render_primitives_particle;
gr_screen.gf_render_primitives_distortion = vulkan_render_primitives_distortion;
gr_screen.gf_render_movie = vulkan_render_movie;
gr_screen.gf_render_nanovg = vulkan_render_nanovg;
gr_screen.gf_render_primitives_batched = vulkan_render_primitives_batched;
gr_screen.gf_render_rocket_primitives = vulkan_render_rocket_primitives;
gr_screen.gf_is_capable = vulkan_is_capable;
gr_screen.gf_get_property = vulkan_get_property;
gr_screen.gf_push_debug_group = vulkan_push_debug_group;
gr_screen.gf_pop_debug_group = vulkan_pop_debug_group;
gr_screen.gf_create_query_object = vulkan_create_query_object;
gr_screen.gf_query_value = vulkan_query_value;
gr_screen.gf_query_value_available = vulkan_query_value_available;
gr_screen.gf_get_query_value = vulkan_get_query_value;
gr_screen.gf_delete_query_object = vulkan_delete_query_object;
gr_screen.gf_create_viewport = stub_create_viewport;
gr_screen.gf_use_viewport = stub_use_viewport;
gr_screen.gf_bind_uniform_buffer = vulkan_bind_uniform_buffer;
gr_screen.gf_sync_fence = vulkan_sync_fence;
gr_screen.gf_sync_wait = vulkan_sync_wait;
gr_screen.gf_sync_delete = vulkan_sync_delete;
gr_screen.gf_set_viewport = vulkan_set_viewport;
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
void initialize_function_pointers() {
init_function_pointers();
}
bool initialize(std::unique_ptr<os::GraphicsOperations>&& graphicsOps)
{
renderer_instance.reset(new VulkanRenderer(std::move(graphicsOps)));
if (!renderer_instance->initialize()) {
return false;
}
// Size the CPU-side shadow cascade bookkeeping (Shadow_frustums/Shadow_proj_matrix/
// Shadow_cascade_distances) and create the cascade params UBO up front, matching
// OpenGL's opengl_tnl_init(). This must happen before any frame is rendered --
// shadows_start_render() indexes these vectors unconditionally, so it can't wait
// for the shadow map framebuffer's lazy first-use init (see vulkan_shadow_map_start).
if (Shadow_quality != ShadowQuality::Disabled) {
shadow_cascade_params_init();
}
// Initialize ImGui SDL3 backend for input handling.
// The Vulkan rendering backend (ImGui_ImplVulkan) is initialized
// inside VulkanRenderer::initImGui() after all Vulkan objects are ready.
SDL_Window* window = os::getSDLMainWindow();
if (window) {
ImGui_ImplSDL3_InitForVulkan(window);
}
gr_screen.gf_flip = vulkan_flip;
// Initialize matrices and viewport (matching OpenGL backend initialization)
gr_reset_matrices();
gr_setup_viewport();
// Start first frame so a command buffer is active before the first draw calls.
// The engine draws the title screen during game_init(), before the main loop's
// first gr_flip() → setupFrame(). Without this, any gr_clear/gr_bitmap before
// the first flip would hit a null command buffer. Matches OpenGL init behavior.
gr_setup_frame();
nprintf(("vulkan", "Vulkan: Initialization complete\n"));
return true;
}
VulkanRenderer* getRendererInstance()
{
return renderer_instance.get();
}
void cleanup()
{
renderer_instance->shutdown();
renderer_instance = nullptr;
}
} // namespace graphics::vulkan