#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 #endif namespace graphics::vulkan { namespace { std::unique_ptr 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()) { if (rt->onModelUnloaded(pm_id)) { getDescriptorManager()->setCurrentShadowTlas(rt->getTlasForShaderBinding()); getDrawManager()->invalidateGlobalSet(); } } } 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(dest) = static_cast(renderer->getMinUniformBufferOffsetAlignment()); return true; case gr_property::UNIFORM_BUFFER_MAX_SIZE: *reinterpret_cast(dest) = static_cast(renderer->getMaxUniformBufferSize()); return true; case gr_property::MAX_ANISOTROPY: *reinterpret_cast(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(sync); } bool vulkan_sync_wait(gr_sync sync, uint64_t timeoutns) { if (!sync) { return true; } auto* renderer = getRendererInstance(); auto* syncObj = static_cast(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(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(w), static_cast(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(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(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 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&& 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