mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -10:00
The shadow-mapped path's penumbra width was a fixed per-cascade value (shadow_smoothness_scale()'s ceiling), so every occluder blurred its shadow by the same amount regardless of how close it actually was to the receiver -- unlike the raytraced path, which gets that for free from traceShadowRayCone()'s real occluder distance. Close that gap with PCSS: search nearby texels for occluders, then scale the filter radius by how far away they were. The blocker search (pcssBlockerSearch() in shadows.sdr) needs raw, uncompared depth reads, which the existing PCF sampler can't do -- it's bound with a compare mode for the hardware shadow-compare instructions the filtered sampling itself relies on. So shadow maps now get a second sampler, shadow_map_raw, bound to the same texture with compare mode off; on OpenGL that needs a second sampler object and GL 3.3 (shadow_contact_hardening_supported()), Vulkan can always bind it. Where the raw sampler isn't available, or the new Graphics.ShadowContactHardening option is off, shadows_start_render() sets Shadow_penumbra_scale[cascade] to -1 as a sentinel and pcssPenumbraRadius() (shadows.sdr) falls straight back to the fixed radius, bit-identical to the pre-contact-hardening behavior and skipping the extra texture reads. When it is on, shadows_start_render() computes each cascade's penumbra scale from its own frustum extents and the sun's tanθ; the shader turns that plus the blocker search's average depth into a per-pixel radius, clamped to [1 texel, the old fixed ceiling]. A blocker search that finds nothing doesn't fall back to "no shadow" -- a sparse 8-tap search can miss a thin occluder, so it instead runs the full filter at the hardest (1-texel) radius, trading a touch of extra blur for not popping a real shadow to nothing. Drops RT_SHADOW_SUN_SIZE_CALIBRATION accordingly: that constant existed only to fake a blocker-distance-driven penumbra on the shadow-mapped path by pre-scaling the raytraced cone to match it, and the shadow-mapped path now has a real blocker search of its own, so the raytraced cone goes back to its physically correct size.
667 lines
23 KiB
C++
667 lines
23 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()) {
|
|
if (rt->onModelUnloaded(pm_id)) {
|
|
getDescriptorManager()->setCurrentShadowTlas(rt->getTlasForShaderBinding());
|
|
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();
|
|
case gr_capability::CAPABILITY_SHADOW_CONTACT_HARDENING:
|
|
// Vulkan samplers are independent of images, so a second (non-compare) sampler on
|
|
// the same shadow map view is always available -- no capability gap here.
|
|
return true;
|
|
}
|
|
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_get_debug_stats(gr_debug_stats& stats)
|
|
{
|
|
const auto& frameStats = getDrawManager()->getFrameStats();
|
|
|
|
stats.draw_stats_valid = true;
|
|
stats.draw_calls = frameStats.drawCalls;
|
|
stats.draw_indexed_calls = frameStats.drawIndexedCalls;
|
|
stats.total_vertices = frameStats.totalVertices;
|
|
stats.total_indices = frameStats.totalIndices;
|
|
stats.apply_material_calls = frameStats.applyMaterialCalls;
|
|
stats.apply_material_failures = frameStats.applyMaterialFailures;
|
|
stats.no_pipeline_skips = frameStats.noPipelineSkips;
|
|
stats.on_demand_texture_uploads = frameStats.onDemandTextureUploads;
|
|
|
|
stats.descriptor_sets_allocated = static_cast<int>(getDescriptorManager()->getSetsAllocatedThisFrame());
|
|
stats.descriptor_writes = static_cast<int>(getDescriptorManager()->getWritesThisFrame());
|
|
stats.pipeline_count = getPipelineManager()->getPipelineCount();
|
|
}
|
|
|
|
void vulkan_get_memory_stats(gr_memory_stats& stats)
|
|
{
|
|
auto* memoryManager = getMemoryManager();
|
|
if (memoryManager == nullptr) {
|
|
return;
|
|
}
|
|
|
|
stats.gpu_purpose_valid = true;
|
|
stats.gpu_texture_bytes = memoryManager->getTextureBytes();
|
|
stats.gpu_geometry_bytes = memoryManager->getGeometryBytes();
|
|
stats.gpu_render_target_bytes = memoryManager->getRenderTargetBytes();
|
|
}
|
|
|
|
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 just bound its own pipeline/descriptor set directly on the command buffer,
|
|
// inside the same (already-active) render pass FSO's own draws share -- it can't begin
|
|
// its own pass here, unlike the other raw recorders whose staleness setRenderPass()
|
|
// alone recovers from. Without this, the next tracked draw (e.g. a HUD gauge rendered
|
|
// after the profiler overlay) can skip rebinding its own pipeline/descriptor set because
|
|
// the tracker's/draw-manager's cached handles still match what THEY last bound, even
|
|
// though ImGui has since changed what's actually bound on the command buffer -- see
|
|
// VulkanStateTracker::invalidateExternalBindings().
|
|
getStateTracker()->invalidateExternalBindings();
|
|
getDrawManager()->invalidateDrawStateCaches();
|
|
}
|
|
}
|
|
|
|
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_get_debug_stats = vulkan_get_debug_stats;
|
|
gr_screen.gf_get_memory_stats = vulkan_get_memory_stats;
|
|
|
|
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
|
|
|
|
// Outside the anonymous namespace: declared in gr_vulkan.h so the RTAO fallback
|
|
// trigger in vulkan_deferred_lighting_finish() (VulkanDeferred.cpp) can call it
|
|
// when shadow rendering didn't build this frame's TLAS.
|
|
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();
|
|
}
|
|
}
|
|
|
|
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
|