Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ TARGET_LINK_LIBRARIES(code PUBLIC jansson)
target_link_libraries(code PUBLIC anl)

target_link_libraries(code PUBLIC imgui)
target_link_libraries(code PUBLIC implot)

IF(FSO_BUILD_WITH_OPENXR)
target_link_libraries(code PUBLIC OpenXR::openxr_loader)
Expand Down
7 changes: 3 additions & 4 deletions code/cmdline/cmdline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2339,10 +2339,9 @@ bool SetCmdlineParams()
if (gr_sync_validation_arg.found()) {
// Enables the Vulkan validation layer + debug messenger + synchronization
// validation feature (see VulkanRenderer::initializeInstance), but NOT the
// engine-level graphics debug output: -gr_debug additionally draws debug
// overlays (e.g. output_uniform_debug_data), which are unrelated to GPU
// sync validation and would otherwise appear as spurious on-screen
// artifacts. Keep this a pure GPU-validation switch.
// engine-level graphics debug output: -gr_debug additionally feeds extra
// stat groups into the ImGui frame profiler overlay (see gr_get_debug_stats),
// which are unrelated to GPU sync validation. Keep this a pure GPU-validation switch.
Cmdline_gr_sync_validation = true;
}

Expand Down
67 changes: 56 additions & 11 deletions code/graphics/2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "render/3d.h"
#include "scripting/hook_api.h"
#include "scripting/scripting.h"
#include "tracing/ProfilerOverlay.h"
#include "tracing/tracing.h"
#include "utils/boost/hash_combine.h"
#include "utils/string_utils.h"
Expand All @@ -54,6 +55,9 @@
#include "graphics/vulkan/gr_vulkan.h"
#endif

#include "imgui.h"
#include "backends/imgui_impl_sdl3.h"

#include <algorithm>
#include <climits>

Expand Down Expand Up @@ -3146,20 +3150,57 @@ void gr_set_bitmap(int bitmap_num, int alphablend_mode, int bitblt_mode, float a
gr_screen.current_bitmap = bitmap_num;
}

static void output_uniform_debug_data()
gr_debug_stats gr_get_debug_stats()
{
if (gr_screen.mode == GraphicsAPI::Stub) {
gr_debug_stats stats;

if (!Cmdline_graphics_debug_output) {
return stats;
}

if (UniformBufferManager) {
stats.uniform_buffer_valid = true;
stats.uniform_buffer_size = UniformBufferManager->getBufferSize();
stats.uniform_buffer_used = UniformBufferManager->getCurrentlyUsedSize();
}

gr_screen.gf_get_debug_stats(stats);

return stats;
}

static bool Imgui_frame_active = false;

void gr_imgui_begin_frame()
{
if (Imgui_frame_active) {
return;
}
// The stub renderer (standalone server) never assigns the ImGui entry points.
if (!gr_screen.gf_imgui_new_frame || !ImGui::GetCurrentContext()) {
return;
}

int line_height = gr_get_font_height() + 1;
gr_imgui_new_frame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
Imgui_frame_active = true;
}

gr_set_color_fast(&Color_bright_white);
void gr_imgui_end_frame()
{
if (!Imgui_frame_active) {
return;
}

gr_printf_no_resize(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 160,
"Uniform buffer size: " SIZE_T_ARG, UniformBufferManager->getBufferSize());
gr_printf_no_resize(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 160 + line_height,
"Currently used data: " SIZE_T_ARG, UniformBufferManager->getCurrentlyUsedSize());
ImGui::Render();
gr_imgui_render_draw_data();
Imgui_frame_active = false;
}

bool gr_imgui_frame_active()
{
return Imgui_frame_active;
}

void gr_flip(bool execute_scripting)
Expand All @@ -3180,9 +3221,13 @@ void gr_flip(bool execute_scripting)

model_process_cached_ui_render_instances();

if (Cmdline_graphics_debug_output) {
output_uniform_debug_data();
}
// Every presented frame drains the frame profiler and contributes the overlay window to
// this frame's ImGui pass. Doing it here rather than per game state is what keeps the
// profiler's event buffer bounded: collection is global, so the drain has to be too.
tracing::profiler_overlay_frame();

// Closes whatever ImGui frame the overlay (or the lab, or the options screen) opened.
gr_imgui_end_frame();

// IMPORTANT: No rendering may happen after this point until gf_flip()/gr_setup_frame().
// gr_reset_immediate_buffer() resets the write offset to 0, so any subsequent immediate
Expand Down
63 changes: 63 additions & 0 deletions code/graphics/2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,32 @@ enum class BufferUsageHint { Static, Dynamic, Streaming, PersistentMapping };
*/
typedef void* gr_sync;

/**
* @brief Per-backend diagnostic counters, populated only when -gr_debug is active
*
* Each stat group carries its own "valid" flag since not every backend collects every
* group (e.g. per-draw-call counters currently only exist in the Vulkan backend). See
* gr_get_debug_stats().
*/
struct gr_debug_stats {
bool uniform_buffer_valid = false;
size_t uniform_buffer_size = 0;
size_t uniform_buffer_used = 0;

bool draw_stats_valid = false;
int draw_calls = 0;
int draw_indexed_calls = 0;
int total_vertices = 0;
int total_indices = 0;
int apply_material_calls = 0;
int apply_material_failures = 0;
int no_pipeline_skips = 0;
int descriptor_sets_allocated = 0;
int descriptor_writes = 0;
size_t pipeline_count = 0;
int on_demand_texture_uploads = 0;
};

typedef struct screen {
int max_w = 0, max_h = 0; // Width and height
int max_w_unscaled = 0, max_h_unscaled = 0;
Expand Down Expand Up @@ -963,6 +989,10 @@ typedef struct screen {
std::function<void(const char* name)> gf_push_debug_group;
std::function<void()> gf_pop_debug_group;

// Fills in whichever debug_stats groups this backend collects. Defaults to a no-op
// so backends without per-draw-call counters (OpenGL, stub) don't have to assign it.
std::function<void(gr_debug_stats& stats)> gf_get_debug_stats = [](gr_debug_stats&) {};

std::function<int()> gf_create_query_object;
std::function<void(int obj, QueryType type)> gf_query_value;
std::function<bool(int obj)> gf_query_value_available;
Expand Down Expand Up @@ -1133,6 +1163,14 @@ bool gr_is_screenshot_requested();
//#define gr_flip GR_CALL(gr_screen.gf_flip)
void gr_flip(bool execute_scripting = true);

/**
* @brief Collects whichever graphics-API debug stats are available for the active backend
*
* Returns a default-constructed (all-invalid) gr_debug_stats unless -gr_debug is active. Safe
* to call every frame regardless of backend or debug flag.
*/
gr_debug_stats gr_get_debug_stats();

inline void gr_setup_frame() {
gr_screen.gf_setup_frame();
}
Expand Down Expand Up @@ -1281,6 +1319,31 @@ inline void gr_post_process_restore_zbuffer()
#define gr_imgui_new_frame GR_CALL(gr_screen.gf_imgui_new_frame)
#define gr_imgui_render_draw_data GR_CALL(gr_screen.gf_imgui_render_draw_data)

/**
* @brief Begins the frame's ImGui pass, if one is not already open
*
* There is exactly one ImGui frame per presented graphics frame, owned here rather than by
* each ImGui user, so that several unrelated consumers (the lab, the in-game options screen,
* the frame profiler overlay) can contribute windows to the same frame. Submitting a second
* NewFrame()/Render() cycle before the flip would make ImGui treat the first cycle's windows
* as no longer submitted, thrashing focus and hover state every frame.
*
* Contributors call this and then build their windows; gr_flip() closes the frame. No-op when
* the active backend has no ImGui implementation (the stub renderer used by the standalone
* server), which is also why gr_imgui_frame_active() must be consulted rather than assumed.
*/
void gr_imgui_begin_frame();

/**
* @brief Renders and submits the open ImGui frame, if any. Called by gr_flip().
*/
void gr_imgui_end_frame();

/**
* @brief Whether an ImGui frame is currently open for contributions
*/
bool gr_imgui_frame_active();

inline void gr_render_primitives(material* material_info,
primitive_type prim_type,
vertex_layout* layout,
Expand Down
14 changes: 14 additions & 0 deletions code/graphics/vulkan/VulkanConvert.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ vk::PrimitiveTopology convertPrimitiveType(primitive_type type)
}
}

bool topologySupportsPrimitiveRestart(vk::PrimitiveTopology topology)
{
switch (topology) {
case vk::PrimitiveTopology::eLineStrip:
case vk::PrimitiveTopology::eTriangleStrip:
case vk::PrimitiveTopology::eTriangleFan:
case vk::PrimitiveTopology::eLineStripWithAdjacency:
case vk::PrimitiveTopology::eTriangleStripWithAdjacency:
return true;
default:
return false;
}
}

vk::CullModeFlags convertCullMode(bool cullEnabled)
{
return cullEnabled ? vk::CullModeFlagBits::eBack : vk::CullModeFlagBits::eNone;
Expand Down
9 changes: 9 additions & 0 deletions code/graphics/vulkan/VulkanConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ vk::StencilOp convertStencilOp(StencilOperation op);
*/
vk::PrimitiveTopology convertPrimitiveType(primitive_type type);

/**
* @brief Whether a topology may declare primitiveRestartEnable
*
* True for the strip/fan topologies. For list topologies the Vulkan spec requires
* primitiveRestartEnable to be VK_FALSE unless VK_EXT_primitive_topology_list_restart is
* enabled, which FSO does not request.
*/
bool topologySupportsPrimitiveRestart(vk::PrimitiveTopology topology);

/**
* @brief Convert FSO cull mode to Vulkan cull mode
* @param cullEnabled Whether culling is enabled
Expand Down
31 changes: 14 additions & 17 deletions code/graphics/vulkan/VulkanDeferred.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -369,13 +369,13 @@ void vulkan_deferred_lighting_msaa()
// Global set (fallback — resolve shader doesn't use global bindings)
vk::DescriptorSet globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global);
Assert(globalSet);
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
writer.writeSet(DescriptorSetIndex::Global, globalSet);

// Material set: All 6 MSAA textures in binding 1 array (elements 0-5)
// [0]=color, [1]=position, [2]=normal, [3]=specular, [4]=emissive, [5]=depth
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);

// Build texture array: elements 0-5 are MSAA textures, 6-15 are fallback
vk::Sampler nearestSampler = texMgr->getSampler(
Expand All @@ -397,7 +397,7 @@ void vulkan_deferred_lighting_msaa()
// PerDraw set: GenericData UBO with {samples, fov} at binding 0
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);

struct MsaaResolveData {
int samples;
Expand All @@ -411,15 +411,8 @@ void vulkan_deferred_lighting_msaa()
ring.alloc(descriptorMgr->getCurrentFrame(), &resolveData, sizeof(resolveData));
writer.setBuffer(PerDrawBinding::GenericData, {ring.buffer(), slotOffset, ring.slotSize()});
writer.flush();
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::Global), globalSet, {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::Material), materialSet, {});
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
pipelineMgr->getPipelineLayout(),
static_cast<uint32_t>(DescriptorSetIndex::PerDraw), perDrawSet, {});
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
writer.bindSets(cmd, pipelineMgr->getPipelineLayout(), DescriptorSetIndex::Global, sets);

cmd.draw(3, 1, 0, 0);
}
Expand Down Expand Up @@ -1008,12 +1001,12 @@ void vulkan_render_decals(decal_material* material_info,
// Set 0: Global (all fallback)
vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
Assert(globalSet);
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
writer.writeSet(DescriptorSetIndex::Global, globalSet);

// Set 1: Material
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);

// Binding 2: DecalGlobals UBO
writer.setBuffer(MaterialBinding::DecalGlobals,
Expand Down Expand Up @@ -1047,15 +1040,19 @@ void vulkan_render_decals(decal_material* material_info,
// Set 2: PerDraw
vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
writer.setBuffer(PerDrawBinding::Matrices,
getPendingBufInfo(static_cast<size_t>(uniform_block_type::Matrices)));
writer.setBuffer(PerDrawBinding::DecalInfo,
getPendingBufInfo(static_cast<size_t>(uniform_block_type::DecalInfo)));
writer.flush();
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material,
materialSet,
writer.dynamicOffsets(DescriptorSetIndex::Material));
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw,
perDrawSet,
writer.dynamicOffsets(DescriptorSetIndex::PerDraw));

// Bind vertex buffers: binding 0 = box VBO, binding 1 = instance buffer
vk::Buffer boxVBO = bufferManager->getVkBuffer(buffers.Vbuffer_handle);
Expand Down
Loading
Loading