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
85 changes: 73 additions & 12 deletions code/graphics/2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "material.h"
#include "matrix.h"

#include "bmpman/bmpman.h"
#include "cmdline/cmdline.h"
#include "debugconsole/console.h"
#include "executor/global_executors.h"
Expand All @@ -42,6 +43,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 Down Expand Up @@ -3170,24 +3172,38 @@ 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) {
return;
gr_debug_stats stats;

if (!Cmdline_graphics_debug_output) {
return stats;
}

int line_height = gr_get_font_height() + 1;
if (UniformBufferManager) {
stats.uniform_buffer_valid = true;
stats.uniform_buffer_size = UniformBufferManager->getBufferSize();
stats.uniform_buffer_used = UniformBufferManager->getCurrentlyUsedSize();
}

gr_set_color_fast(&Color_bright_white);
gr_screen.gf_get_debug_stats(stats);

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());
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;
}

gr_imgui_new_frame(); // renderer backend (OpenGL/Vulkan)
ImGui_ImplSDL3_NewFrame(); // platform backend, derives the display size from the SDL window

Expand All @@ -3205,6 +3221,23 @@ void gr_imgui_begin_frame()
}

ImGui::NewFrame();
Imgui_frame_active = true;
}

void gr_imgui_end_frame()
{
if (!Imgui_frame_active) {
return;
}

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 @@ -3225,9 +3258,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 Expand Up @@ -3496,6 +3533,30 @@ void gr_heap_deallocate(GpuHeap heap_type, size_t data_offset)
gpuHeap->freeGpuData(data_offset);
}

gr_memory_stats gr_get_memory_stats()
{
gr_memory_stats stats;

// gpu_heaps[] entries stay null when gpu_heap_init() early-returned (GraphicsAPI::Stub, e.g.
// a build with both graphics backends disabled), so this must not assume a live heap.
auto vertex_heap = get_gpu_heap(GpuHeap::ModelVertex);
auto index_heap = get_gpu_heap(GpuHeap::ModelIndex);
if (vertex_heap != nullptr && index_heap != nullptr) {
stats.model_heap_valid = true;
stats.model_vertex_heap_used = vertex_heap->usedBytes();
stats.model_vertex_heap_size = vertex_heap->bufferSize();
stats.model_index_heap_used = index_heap->usedBytes();
stats.model_index_heap_size = index_heap->bufferSize();
}

stats.locked_bitmap_ram_valid = true;
stats.locked_bitmap_ram_bytes = bm_texture_ram;

gr_screen.gf_get_memory_stats(stats);

return stats;
}

void gr_set_gamma(float gamma)
{
if (gr_screen.mode == GraphicsAPI::Stub) {
Expand Down
85 changes: 85 additions & 0 deletions code/graphics/2d.h
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,56 @@ 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;
};

/**
* @brief Cross-backend memory usage snapshot for the profiler overlay's memory panel
*
* Unlike gr_debug_stats, this is always populated when queried (no -gr_debug gate) -- memory
* usage is meant to be visible to anyone with the profiler overlay open. Each group still carries
* its own "valid" flag since not every backend/build collects every group (e.g. both graphics
* backends disabled at build time). See gr_get_memory_stats().
*/
struct gr_memory_stats {
bool model_heap_valid = false;
size_t model_vertex_heap_used = 0;
size_t model_vertex_heap_size = 0;
size_t model_index_heap_used = 0;
size_t model_index_heap_size = 0;

bool locked_bitmap_ram_valid = false;
size_t locked_bitmap_ram_bytes = 0;

bool gpu_purpose_valid = false;
size_t gpu_texture_bytes = 0;
size_t gpu_geometry_bytes = 0;
size_t gpu_render_target_bytes = 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 +1013,14 @@ 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&) {};

// Fills in whichever memory_stats groups this backend collects (GPU-purpose byte totals).
// Defaults to a no-op so backends without per-purpose tagging don't have to assign it.
std::function<void(gr_memory_stats& stats)> gf_get_memory_stats = [](gr_memory_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 @@ -1153,6 +1211,23 @@ 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();

/**
* @brief Collects whichever cross-backend memory usage stats are available
*
* Unlike gr_get_debug_stats(), this is always populated (no -gr_debug gate) -- memory usage is
* meant to be visible whenever the profiler overlay is open. Safe to call every frame regardless
* of backend or build configuration; groups a backend/build doesn't support stay invalid/zero.
*/
gr_memory_stats gr_get_memory_stats();

inline void gr_setup_frame() {
gr_screen.gf_setup_frame();
}
Expand Down Expand Up @@ -1310,6 +1385,16 @@ inline void gr_post_process_restore_zbuffer()
*/
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
1 change: 1 addition & 0 deletions code/graphics/opengl/gropengl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,7 @@ void gr_opengl_init_function_pointers()

gr_screen.gf_is_capable = gr_opengl_is_capable;
gr_screen.gf_get_property = gr_opengl_get_property;
gr_screen.gf_get_memory_stats = gr_opengl_get_memory_stats;

gr_screen.gf_push_debug_group = gr_opengl_push_debug_group;
gr_screen.gf_pop_debug_group = gr_opengl_pop_debug_group;
Expand Down
60 changes: 60 additions & 0 deletions code/graphics/opengl/gropengldraw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,66 @@ void opengl_scene_texture_shutdown()
Scene_framebuffer_in_frame = false;
}

size_t opengl_get_scene_render_target_bytes()
{
// The bytes for each pixel come from the internal formats that opengl_setup_scene_textures()
// gives to these textures: GL_RGBA16F holds 8 bytes, GL_RGBA8 and GL_DEPTH24_STENCIL8 hold 4.
// Keep the values below equal to the formats at the creation sites in that function.
constexpr size_t RGBA16F_BYTES = 8;
constexpr size_t RGBA8_BYTES = 4;
constexpr size_t DEPTH24_STENCIL8_BYTES = 4;

size_t bytes = 0;

auto add_texture = [&bytes](GLuint texture_id, size_t width, size_t height, size_t bytes_per_pixel,
size_t samples = 1) {
if (texture_id == 0) {
return;
}
bytes += width * height * bytes_per_pixel * samples;
};

const auto scene_w = static_cast<size_t>(Scene_texture_width);
const auto scene_h = static_cast<size_t>(Scene_texture_height);

add_texture(Scene_color_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_ldr_texture, scene_w, scene_h, RGBA8_BYTES);
add_texture(Scene_position_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_normal_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_specular_texture, scene_w, scene_h, RGBA8_BYTES);
add_texture(Scene_emissive_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_composite_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Scene_luminance_texture, scene_w, scene_h, RGBA16F_BYTES);
add_texture(Cockpit_depth_texture, scene_w, scene_h, DEPTH24_STENCIL8_BYTES);
add_texture(Scene_depth_texture, scene_w, scene_h, DEPTH24_STENCIL8_BYTES);

// The multisample textures hold one value for each sample, thus their bytes go up with the
// MSAA level. Cmdline_msaa_enabled holds the number of samples and is 0 when MSAA is off.
const auto samples = static_cast<size_t>(std::max(1, Cmdline_msaa_enabled));

add_texture(Scene_color_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_position_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_normal_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_specular_texture_ms, scene_w, scene_h, RGBA8_BYTES, samples);
add_texture(Scene_emissive_texture_ms, scene_w, scene_h, RGBA16F_BYTES, samples);
add_texture(Scene_depth_texture_ms, scene_w, scene_h, DEPTH24_STENCIL8_BYTES, samples);

// The back buffer and gamma blit textures use the size of the screen, not the (clamped) size
// of the scene textures.
const auto screen_w = static_cast<size_t>(gr_screen.max_w);
const auto screen_h = static_cast<size_t>(gr_screen.max_h);

add_texture(Back_texture, screen_w, screen_h, RGBA16F_BYTES);
add_texture(Back_depth_texture, screen_w, screen_h, DEPTH24_STENCIL8_BYTES);
add_texture(GammaBlit_texture, screen_w, screen_h, RGBA8_BYTES);

// The two distortion textures always have the size 32x32.
add_texture(Distortion_texture[0], 32, 32, RGBA8_BYTES);
add_texture(Distortion_texture[1], 32, 32, RGBA8_BYTES);

return bytes;
}

void gr_opengl_scene_texture_begin()
{
if ( !Scene_texture_initialized ) {
Expand Down
9 changes: 9 additions & 0 deletions code/graphics/opengl/gropengldraw.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ void gr_opengl_render_shield_impact(shield_material* material_info,

void opengl_setup_scene_textures();
void opengl_scene_texture_shutdown();

/**
* @brief Gives the bytes that the scene, backbuffer, and distortion textures hold
*
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
* counts only the textures that exist at the moment of the call, thus it gives 0 before
* opengl_setup_scene_textures() runs and after opengl_scene_texture_shutdown() runs.
*/
size_t opengl_get_scene_render_target_bytes();
void gr_opengl_scene_texture_begin();
void gr_opengl_scene_texture_end();
void gr_opengl_copy_effect_texture();
Expand Down
Loading
Loading