Compare commits

...
21 Commits
Author SHA1 Message Date
the-eandClaude Opus 5.5 c165b82996 Address third round of review feedback on the profiler overlay
- Move the overlay option strings to XSTR 1940 and 1941. The touch
  overlay on master uses 1936 to 1939.
- Give FrameProfiler the main thread ID when it is made. Before, it
  used the thread of the first event, which was a race.
- Let mouse motion go to the game while the pointer is over the
  overlay, so that mouse flight does not stop there.
- Find the dynamic bindings of the PerDraw key with
  dynamic_offset_slot(), as the other call sites do.
- Make disable_inside_open_scope count the GPU timestamp queries, so
  that it fails if end() does not close the GPU event.
- Make the copy test copy the category, not bind a reference.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:36:52 +01:00
Fabian WoltermannandClaude Opus 5.5 4f3a0d4cff Return size_t from Category::getCount() and tidy the registry code
- Use structured bindings and std::scoped_lock in the category registry.
- Return size_t from getCount() and update its callers and tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:36:42 +01:00
Fabian WoltermannandClaude Opus 5.5 8266392f00 Address second round of review feedback on the profiler overlay
- Make the category registry thread-safe and give categories with the
  same name one id, so the registry does not grow without limit.
- Add tests that toggle the profiler inside an open trace scope.
- Correct the next free XSTR id note to 1938.
- Use the existing Windows include block for psapi.h in osapi.cpp.
- Use list_range in ship_get_num_ships() and shorten some comments.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-07 19:36:42 +01:00
the-eandClaude Sonnet 5.5 68166ed419 Use wall-clock frame time and read the history without a copy
The Avg and FPS line used traced self-time, which reads too high.
Measure the time between overlay frames instead. Draw the graph with
PlotLineG so the history deque needs no per-frame copy.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 19:36:31 +01:00
the-eandClaude Sonnet 5.5 fa4b755b94 Address review feedback on the profiler overlay
Skip complete::end for a scope that did not start. This fixes a crash
when Lua turns the overlay on while a scope is open. Track the GPU begin
event so that turning the overlay off mid-scope still closes it.

Use XSTR 1936 and 1937 for the overlay option. Master now uses 1932
and 1933.

Remove the default value of dynamicOffsets in bindDescriptorSet. Use the
PerDraw binding constants in the PerDraw key.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-07 19:36:31 +01:00
the-e b5bcfeb8d1 Refactor profiler overlay to use snapshot-based frame timing 2026-11-07 19:36:31 +01:00
the-e b1c90ea5c5 Replace category pointers with IDs to prevent use-after-free
- Updated `trace_event` to store `category_id` instead of a pointer to prevent dangling references to category objects that may already be destroyed.
- Modified all references to category names to use `Category::getNameById()` instead of dereferencing pointers.
- Expanded memory statistics in the profiler overlay with new sound, Lua, and OS-level memory usage tracking.
- Added new tests to verify proper use of category IDs in frame profiling and event tracing.
- Improved memory safety and stability across tracing, profiling, and overlay systems.
2026-11-07 19:36:31 +01:00
the-eandClaude Opus 5 dd4a08925c Fix profiler overlay crash on a category that is not a static
The category registry kept a Category pointer for each id. The comment said that all categories
are global statics, but that is not correct. The Lua API tracing_category constructs a Category
and copies it into a script-owned object. The registry then kept a pointer to the dead temporary.
build_overlay_snapshot called getName() on that pointer, made an SCP_string from the garbage
char*, and the process stopped with SIGABRT.

The registry now keeps the name by value and never keeps a Category pointer. getById() becomes
getNameById(), which is all that build_overlay_snapshot needs. A copy of a Category keeps the id
of the original, which stays correct because the id only selects a name.

Add two tests: one shows that the name stays available after the Category object is dead, and one
drives the full path that failed, from processEvent to the overlay snapshot.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:36:13 +01:00
the-eandClaude Opus 5 647554e182 Add OpenGL memory statistics to the profiler overlay
The memory panel showed no texture bytes and no geometry bytes under
OpenGL. Only the depth renderbuffers went into the render target group.
All three groups now give a value.

Textures: GL_texture_bytes_used holds the bytes of every live texture
cache slot. opengl_texture_release_bytes() removes the bytes of a slot at
each site that drops its texture data. The bitmap render targets record
the bytes of their color texture.

Geometry: the unused GL_vertex_data_in global becomes
GL_geometry_bytes_used, which counts only vertex and index buffers. This
matches the geometry group of the Vulkan backend.
gr_opengl_delete_buffer() sets the size of the buffer to 0, because
opengl_destroy_all_buffers() calls it a second time for a buffer that the
game already deleted.

Render targets: three new functions add the scene textures, the
post-processing textures, and the shadow map to the group. They sum the
bytes at the moment of the call from the textures that exist, thus they
need no bookkeeping at the create and delete sites.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:36:13 +01:00
the-e 57c28798a7 fix ci issues 2026-11-07 19:36:13 +01:00
the-e 3c456a088c Add memory statistics and tracking to profiler overlay 2026-11-07 19:36:13 +01:00
the-e cd48b5b500 Replace exception with sentinel for non-dynamic Vulkan descriptor bindings
- Introduced `DYNAMIC_SLOT_INVALID` sentinel to replace the throw in `dynamic_offset_slot` for MSVC compatibility.
- Added `static_assert` checks to validate binding constants, ensuring compile-time validation for dynamic offsets.
- Updated comments to clarify design rationale and avoid potential misuse.
2026-11-07 19:36:13 +01:00
Fabian Woltermann f1812e212d Enable VK_KHR_portability_subset for compatibility 2026-11-07 19:36:13 +01:00
the-e 817f641478 Enable primitive restart for supported topologies in Vulkan pipeline
- Added `topologySupportsPrimitiveRestart` to determine restart eligibility for each topology.
- Updated `primitiveRestartEnable` to be conditionally set based on topology, addressing compatibility issues with Metal/MoltenVK.
- Prevented crash scenarios on macOS by ensuring pipelines are valid for strip/fan topologies requiring primitive restart.
2026-11-07 19:36:13 +01:00
the-e eec4a9ce3e Refactor Vulkan descriptor set binding to reduce dynamic offset churn
- Consolidated dynamic descriptor offset logic into `DescriptorWriter::bindSets`, replacing repetitive `vkCmdBindDescriptorSets` patterns across Vulkan modules.
- Eliminated redundant descriptor binding operations for Material and PerDraw sets by centralizing logic in `bindSets`.
- Simplified descriptors by directly splitting offset changes from descriptor contents, avoiding unnecessary updates.
- Updated comments and documentation to align with the new approach while maintaining reference consistency.
- Applied optimizations across Vulkan modules (`VulkanPostProcessing`, `VulkanDescriptorManager`, etc.) to reduce per-draw overhead and improve readability.
2026-11-07 19:36:13 +01:00
the-eandClaude Opus 5 b2de895055 Use dynamic uniform buffers to stop per-draw descriptor set churn in Vulkan
The Material and PerDraw descriptor sets were reallocated and fully rewritten on
essentially every draw call. model_draw_list::render_buffer rebinds ModelData with
a fresh uniform buffer offset for each queued draw, and that offset was part of the
Material set's memoization key, so the cache in VulkanDrawManager::applyMaterial
missed every time - taking a frame-pool allocation, a full template write, the
16-entry material texture resolution and a vkUpdateDescriptorSets with it.
GenericData/Matrices did the same to the PerDraw set. renderShadowDraw was worse
still: all three sets allocated and rewritten per shadow draw, with no memoization
at all.

A large asteroid field (Solaris m21) measured 2566 descriptor sets and 19107
descriptor writes per frame at 1606 model draws, and grew an extra descriptor pool
chunk every frame.

Declare the bindings whose offset moves per draw - MaterialBinding::ModelData,
MaterialBinding::ShadowMapData, PerDrawBinding::GenericData and
PerDrawBinding::Matrices - as eUniformBufferDynamic. DescriptorWriter::setBuffer
now splits a dynamic binding's offset out of the descriptor, writing
{buffer, 0, range} and stashing the offset for vkCmdBindDescriptorSets'
pDynamicOffsets, so an offset-only change leaves the set's contents identical and
the memoization caches start hitting. Shader sources are unchanged.

VulkanStateTracker::bindDescriptorSet now compares the dynamic offsets as well as
the set handle. This is required, not an optimization: the common case is now the
same set rebound with a new offset, and the old handle-only redundancy check would
have skipped that bind and fed every draw the previous draw's uniforms.

renderShadowDraw memoizes its three sets the way applyMaterial does. It does not
invalidate applyMaterial's caches - those record what a set contains, the shadow
pass allocates its own sets rather than overwriting theirs, and it binds through
the state tracker, so the next material cache hit still rebinds correctly.

Also switch allocateFrameSet to the non-allocating allocateDescriptorSets overload;
the vector-returning one heap-allocated once per set, thousands of times a frame.
MAX_SETS_PER_POOL stays at 1024 - the per-frame pool growth it used to log was a
symptom of the churn, and one chunk now covers the scene outright.

Measured on that mission in a Debug build, A/B on the same tree with and without
this change:

  descriptor sets/frame  @ ~1370 draws  2566   -> 250-345
  descriptor writes/frame                19107 -> ~1700-2300
  median frametime                       92.9/83.6 ms -> 72.0/66.5 ms
  Render Buffer (profiler overlay)       24.68 ms (30.8%) -> 12.73 ms (18.4%)
  Build Shadow Map                       5.44 ms -> off the top-5 entirely

Release builds already inlined most of this away and are unchanged, as expected.
Verified with the validation layers (sync validation enabled): no new VUIDs versus
the same build without the change.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-07 19:36:13 +01:00
the-e 7d4d61042e Refactor frame profiler to use per-category self-time accumulation
- Introduced `accumulate_self_times` for single-pass exclusive time computation, improving performance and accuracy.
- Updated `FrameProfiler` and overlay snapshot logic to leverage `Category`'s stable `getId` and `getCount`.
- Replaced pie chart with a stacked bar for frame budget visualization, optimizing rendering cost.
- Cached `get_tid` and `get_pid` with thread-local storage to minimize syscall overhead.
- Added support for unit testing frame profiling functionality and included new test cases.
- Improved code comments and clarified documentation.
2026-11-07 19:36:13 +01:00
the-e fd77b84c41 Update ImPlot to v1.1 WIP and improve internal consistency
- Adjust copyright notices for extended license coverage (2025-2026).
- Modernize constants with `constexpr` replacements, improving type safety and clarity.
- Enforce `nullptr` usage over `NULL` throughout, aligning with modern C++ practices.
- Add various enhancements to ImPlot legends, markers, and axes behavior.
- Introduce additional helper methods for time handling and timestamp calculations.
- Address UI and typo improvements in comments and documentation, refining accuracy.
2026-11-07 19:36:13 +01:00
the-e 2166d253a8 Add graphics and Vulkan debug stats to ImGui profiler overlay
- Integrated per-backend diagnostic counters into the ImGui frame profiler overlay, triggered by `-gr_debug`.
- Consolidated and cleaned up Vulkan rendering state and descriptor management for compatibility with ImGui's Vulkan pipeline.
- Enabled uniform buffer and draw statistics reporting for enhanced performance insights during debugging.
2026-11-07 19:36:13 +01:00
the-e f5b29e721b feat: add ImGui frame profiler overlay
Replaces the old text-dump frame profile display (-profile_frame_time)
with a runtime-toggleable ImGui/ImPlot overlay showing average/median
frametime, a scrolling frametime graph, and a pie chart of the top
contributors to frame time. Toggled via a new "Frame Profiler Overlay"
option, which also seeds from -profile_frame_time for backward
compatibility and now drives frame profiling collection at runtime
instead of only at startup.
2026-11-07 19:36:13 +01:00
the-e 9763c7a312 chore: vendor ImPlot library
Adds ImPlot as a build dependency, to be used for the upcoming ImGui
frame profiler overlay's frametime graph and pie chart.
2026-11-07 19:36:01 +01:00
81 changed files with 15740 additions and 388 deletions

No files matched your search

+1
View File
@@ -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)
+3 -4
View File
@@ -2444,10 +2444,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;
}
+73 -12
View File
@@ -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"
@@ -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"
@@ -3263,24 +3265,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
@@ -3298,6 +3314,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)
@@ -3318,9 +3351,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
@@ -3589,6 +3626,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) {
+85
View File
@@ -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;
@@ -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;
@@ -1161,6 +1219,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();
}
@@ -1318,6 +1393,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,
+1
View File
@@ -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;
+60
View File
@@ -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 ) {
+9
View File
@@ -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();
@@ -1093,6 +1093,53 @@ void opengl_post_process_shutdown_bloom()
}
}
size_t opengl_get_postprocessing_render_target_bytes()
{
// The bytes for each pixel come from the internal formats at the creation sites in this file:
// GL_RGBA16F holds 8 bytes and GL_RGBA8 holds 4. Keep these values equal to those formats.
constexpr size_t RGBA16F_BYTES = 8;
constexpr size_t RGBA8_BYTES = 4;
size_t bytes = 0;
// The bloom textures are half the size of the post-processing textures. opengl_setup_bloom_textures()
// calls glGenerateMipmap() before it sets GL_TEXTURE_MAX_LEVEL, thus the driver holds the full
// mipmap chain, not only MAX_MIP_BLUR_LEVELS levels.
for (GLuint bloom_texture : Bloom_textures) {
if (bloom_texture == 0) {
continue;
}
auto width = static_cast<size_t>(std::max(1, Post_texture_width >> 1));
auto height = static_cast<size_t>(std::max(1, Post_texture_height >> 1));
while (true) {
bytes += width * height * RGBA16F_BYTES;
if ((width == 1) && (height == 1)) {
break;
}
width = std::max<size_t>(1, width >> 1);
height = std::max<size_t>(1, height >> 1);
}
}
// The SMAA area and search textures stay out of this total. They hold lookup data of a fixed
// small size, thus they are not render targets.
const auto post_w = static_cast<size_t>(Post_texture_width);
const auto post_h = static_cast<size_t>(Post_texture_height);
if (Smaa_edges_tex != 0) {
bytes += post_w * post_h * RGBA8_BYTES;
}
if (Smaa_blend_tex != 0) {
bytes += post_w * post_h * RGBA8_BYTES;
}
if (Smaa_output_tex != 0) {
bytes += post_w * post_h * RGBA8_BYTES;
}
return bytes;
}
void opengl_post_process_init()
{
// Externally used manager class to expose information about post processing externally
@@ -8,6 +8,14 @@
void opengl_post_process_init();
void opengl_post_process_shutdown();
/**
* @brief Gives the bytes that the bloom and SMAA render 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.
*/
size_t opengl_get_postprocessing_render_target_bytes();
void gr_opengl_post_process_set_effect(const char *name, int x, const vec3d *rgb);
void gr_opengl_post_process_set_defaults();
void gr_opengl_post_process_save_zbuffer();
+108
View File
@@ -15,11 +15,14 @@
#include "gropenglstate.h"
#include "gropengltexture.h"
#include "gropengldraw.h"
#include "gropenglpostprocessing.h"
#include "gropengltnl.h"
#include "bmpman/bm_internal.h"
#include "bmpman/bmpman.h"
#include "cmdline/cmdline.h"
#include "ddsutils/ddsutils.h"
#include "globalincs/systemvars.h"
#include "graphics/2d.h"
#include "graphics/grinternal.h"
#include "math/vecmat.h"
#include "options/Option.h"
@@ -146,6 +149,27 @@ static auto AnisotropyOption = options::OptionBuilder<float>("Graphics.Anisotrop
int opengl_free_texture(tcache_slot_opengl *t);
int opengl_create_texture (int bitmap_handle, int bitmap_type, tcache_slot_opengl *tslot = NULL);
// Running total of the bytes of texture data held by all live texture cache slots, for the
// profiler overlay's memory panel. Kept in sync at every site that fills or drops a slot's data
// rather than summed on demand, because the slots live in bmpman's blocks and a full walk of them
// each frame is too expensive. Animations put all their frames in one texture array but give each
// frame its own slot, so freeing some frames of an animation lowers this total while the array
// itself stays alive until the last frame goes away.
static size_t GL_texture_bytes_used = 0;
/**
* @brief Removes a texture cache slot's bytes from GL_texture_bytes_used
*
* Call this at each site that drops the texture data of a slot, immediately before the code that
* clears the slot. The function sets the size of the slot to 0, thus a second call for the same
* slot does nothing.
*/
static void opengl_texture_release_bytes(tcache_slot_opengl* t)
{
GL_texture_bytes_used -= static_cast<size_t>(t->size);
t->size = 0;
}
extern int get_num_mipmap_levels(int w, int h);
void opengl_set_additive_tex_env()
@@ -236,6 +260,14 @@ void opengl_tcache_flush()
opengl_free_texture(static_cast<tcache_slot_opengl*>(slot.gr_info));
}
}
// A flush releases the texture of every slot, thus the total must go to 0. A value that is not
// 0 shows that a site which drops texture data does not call opengl_texture_release_bytes().
// The shutdown sequence is the one exception: bm_close() removes the blocks before it closes
// the graphics backend, thus the loop above then finds no slot to release.
Assertion(bm_blocks.empty() || (GL_texture_bytes_used == 0),
"OpenGL texture memory tracking lost " SIZE_T_ARG " bytes. A texture release site is missing.",
GL_texture_bytes_used);
}
extern void opengl_kill_all_render_targets();
@@ -334,6 +366,7 @@ int opengl_free_texture(tcache_slot_opengl *t)
// There are still used frames left in the animation so we can't free the texture object here
// We still need to reset this slot though since it has been "freed"
opengl_texture_release_bytes(t);
t->reset();
// Everything is fine
@@ -353,6 +386,7 @@ int opengl_free_texture(tcache_slot_opengl *t)
glDeleteTextures (1, &t->texture_id);
// Finally, reset the data of this slot
opengl_texture_release_bytes(t);
t->reset();
return 1;
@@ -731,10 +765,14 @@ static int opengl_texture_set_level(int bitmap_handle, int bitmap_type, int bmap
} // end switch
tSlot->bitmap_handle = bitmap_handle;
// Remove the previous bytes of the slot before the new size replaces them. This function also
// runs for a texture that is filled again (a streaming animation, for example).
opengl_texture_release_bytes(tSlot);
tSlot->size = (dsize) ? ((doffset + dsize) - skip_size) : (tex_w * tex_h * byte_mult);
tSlot->w = (ushort)tex_w;
tSlot->h = (ushort)tex_h;
GL_texture_bytes_used += static_cast<size_t>(tSlot->size);
GL_textures_in_frame += tSlot->size;
GL_CHECK_FOR_ERRORS("end of create_texture_sub()");
@@ -964,6 +1002,7 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
GL_state.Texture.Delete(start_slot->texture_id);
glDeleteTextures(1, &start_slot->texture_id);
opengl_texture_release_bytes(start_slot);
start_slot->reset();
}
}
@@ -1034,6 +1073,7 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
if (tslot->texture_id == 0) {
mprintf(("!!OpenGL DEBUG!! t->texture_id == 0\n"));
opengl_texture_release_bytes(tslot);
tslot->reset();
return 0;
}
@@ -1110,7 +1150,13 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl
auto index = frame - animation_begin;
auto frame_slot = bm_get_gr_info<tcache_slot_opengl>(frame, true);
// The copy below overwrites the size of the slot, thus the old bytes must go away first
opengl_texture_release_bytes(frame_slot);
*frame_slot = *tslot; // Copy the existing properties to the slot of this texture
// The copy also brought in the size of tslot, which belongs to a different slot and is
// already part of GL_texture_bytes_used. opengl_texture_set_level() below gives this slot
// its own size.
frame_slot->size = 0;
frame_slot->array_index = (uint32_t) index;
// set the bits per pixel
@@ -1641,6 +1687,7 @@ struct fbo_t {
// these first vars should only be modified in opengl_make_render_target()
GLuint renderbuffer_id = 0;
GLuint framebuffer_id = 0;
size_t renderbuffer_bytes = 0; // for the profiler overlay's memory panel; see GL_renderbuffer_bytes_used
int width = 0;
int height = 0;
// these next 2 should only be modifed in opengl_set_render_target()
@@ -1653,6 +1700,12 @@ static SCP_vector<fbo_t> RenderTarget;
static fbo_t *render_target = NULL;
static int next_fbo_id = 0;
// Running total of bytes used by depth/stencil renderbuffers across all live FBOs, for the
// profiler overlay's memory panel. Kept in sync with each fbo_t's renderbuffer_bytes at every
// create/delete site below rather than summed on demand, since RenderTarget entries are reused
// (opengl_get_free_fbo) and reset in place.
static size_t GL_renderbuffer_bytes_used = 0;
static fbo_t* opengl_get_fbo(int id) {
if (id < 0) {
return nullptr;
@@ -1768,11 +1821,31 @@ void opengl_kill_render_target(bitmap_slot* slot)
if (fbo->renderbuffer_id) {
glDeleteRenderbuffers(1, &fbo->renderbuffer_id);
fbo->renderbuffer_id = 0;
GL_renderbuffer_bytes_used -= fbo->renderbuffer_bytes;
fbo->renderbuffer_bytes = 0;
}
opengl_free_fbo_slot(fbo->fbo_id);
}
void gr_opengl_get_memory_stats(gr_memory_stats& stats)
{
stats.gpu_purpose_valid = true;
// The textures of the bitmap cache. Render targets that the game creates through bmpman are
// part of this group, which matches the Vulkan backend: its bm_make_render_target() images
// also go into the texture group.
stats.gpu_texture_bytes = GL_texture_bytes_used;
stats.gpu_geometry_bytes = opengl_get_geometry_bytes();
// The render target group holds the resources of the backend itself: the depth/stencil
// renderbuffers of the bitmap render targets, the scene and backbuffer textures, the
// post-processing textures, and the shadow map.
stats.gpu_render_target_bytes = GL_renderbuffer_bytes_used + opengl_get_scene_render_target_bytes() +
opengl_get_postprocessing_render_target_bytes() + opengl_get_shadow_map_bytes();
}
void opengl_kill_all_render_targets()
{
for (size_t i = 0; i < RenderTarget.size(); i++) {
@@ -1786,10 +1859,18 @@ void opengl_kill_all_render_targets()
if (fbo->renderbuffer_id) {
glDeleteRenderbuffers(1, &fbo->renderbuffer_id);
fbo->renderbuffer_id = 0;
GL_renderbuffer_bytes_used -= fbo->renderbuffer_bytes;
fbo->renderbuffer_bytes = 0;
}
}
RenderTarget.clear();
// Every renderbuffer is gone now, thus the total must go to 0. A value that is not 0 shows
// that a site which deletes a renderbuffer does not correct GL_renderbuffer_bytes_used.
Assertion(GL_renderbuffer_bytes_used == 0,
"OpenGL renderbuffer memory tracking lost " SIZE_T_ARG " bytes. A renderbuffer delete site is missing.",
GL_renderbuffer_bytes_used);
}
int opengl_set_render_target( int slot, int face, int is_static )
@@ -1946,6 +2027,26 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
glTexParameteri(GL_texture_target, GL_TEXTURE_MAX_LEVEL, ts->mipmap_levels - 1);
// Record the bytes of the color texture, which opengl_tex_array_storage() gave the format
// GL_RGBA8 (4 bytes for each pixel). A cube map holds 6 faces of that size.
size_t rt_texture_bytes = 0;
for (int level = 0; level < ts->mipmap_levels; ++level) {
auto level_w = static_cast<size_t>(std::max(1, *w >> level));
auto level_h = static_cast<size_t>(std::max(1, *h >> level));
rt_texture_bytes += level_w * level_h * 4;
}
if (flags & BMP_FLAG_CUBEMAP) {
rt_texture_bytes *= 6;
}
// A slot that held a render target before must lose its old bytes first.
opengl_texture_release_bytes(ts);
// The size field of the slot is an int, thus the value must stay inside its range. The counter
// takes the same (clamped) value, which keeps the add here and the subtract in
// opengl_texture_release_bytes() equal.
ts->size = static_cast<int>(std::min(rt_texture_bytes, static_cast<size_t>(INT_MAX)));
GL_texture_bytes_used += static_cast<size_t>(ts->size);
GL_state.Texture.Enable(0);
// render buffer (depth)
@@ -1954,6 +2055,10 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
glBindRenderbuffer(GL_RENDERBUFFER, new_fbo->renderbuffer_id);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, *w, *h);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
// GL_DEPTH24_STENCIL8 is 4 bytes/pixel
new_fbo->renderbuffer_bytes = static_cast<size_t>(*w) * static_cast<size_t>(*h) * 4;
GL_renderbuffer_bytes_used += new_fbo->renderbuffer_bytes;
}
// frame buffer
@@ -1980,6 +2085,7 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
GL_state.Texture.Delete(ts->texture_id);
glDeleteTextures(1, &ts->texture_id);
opengl_texture_release_bytes(ts);
ts->reset();
glDeleteFramebuffers(1, &new_fbo->framebuffer_id);
@@ -1987,6 +2093,8 @@ int opengl_make_render_target( int handle, int *w, int *h, int *bpp, int *mm_lvl
if (new_fbo->renderbuffer_id) {
glDeleteRenderbuffers(1, &new_fbo->renderbuffer_id);
new_fbo->renderbuffer_id = 0;
GL_renderbuffer_bytes_used -= new_fbo->renderbuffer_bytes;
new_fbo->renderbuffer_bytes = 0;
}
opengl_set_texture_target();
+2
View File
@@ -81,6 +81,8 @@ void opengl_set_additive_tex_env();
void opengl_set_modulate_tex_env();
void opengl_preload_init();
void opengl_kill_render_target(bitmap_slot* slot);
struct gr_memory_stats;
void gr_opengl_get_memory_stats(gr_memory_stats& stats);
int opengl_make_render_target(int handle, int *w, int *h, int *bpp, int *mm_lvl, int flags);
int opengl_set_render_target(int slot, int face = -1, int is_static = 0);
ubyte* gr_opengl_get_bitmap_from_texture(int bitmap_num, int* width_out, int* height_out);
+47 -4
View File
@@ -59,7 +59,21 @@ extern vec3d G3_user_clip_point;
extern bool Envmap_override;
extern bool Shadow_override;
size_t GL_vertex_data_in = 0;
// Running total of the bytes held by live vertex and index buffers, for the profiler overlay's
// memory panel (see opengl_get_geometry_bytes()). Uniform buffers and texture buffers stay out of
// this total, which keeps it equal to the "geometry" group of the Vulkan backend.
static size_t GL_geometry_bytes_used = 0;
// Tells if a buffer of this OpenGL type counts as geometry for GL_geometry_bytes_used.
static bool opengl_buffer_is_geometry(GLenum type)
{
return (type == GL_ARRAY_BUFFER) || (type == GL_ELEMENT_ARRAY_BUFFER);
}
size_t opengl_get_geometry_bytes()
{
return GL_geometry_bytes_used;
}
GLint GL_max_elements_vertices = 4096;
GLint GL_max_elements_indices = 4096;
@@ -68,6 +82,11 @@ GLuint Shadow_map_depth_texture = 0;
GLuint shadow_fbo = 0;
int Shadow_texture_size = 0;
// Number of layers in the shadow map texture array, kept from the moment
// opengl_init_shadow_framebuffer() created the array. opengl_get_shadow_map_bytes() uses it, thus
// a later change of the cascade counts cannot make that value wrong.
static int Shadow_texture_layers = 0;
gr_buffer_handle Transform_buffer_handle;
SCP_unordered_map<vertex_layout, GLuint> Stored_vertex_arrays;
@@ -275,9 +294,11 @@ void gr_opengl_update_buffer_data(gr_buffer_handle handle, size_t size, const vo
glBufferData(buffer_obj.type, size, data, buffer_obj.gl_usage);
}
GL_vertex_data_in -= buffer_obj.size;
if (opengl_buffer_is_geometry(buffer_obj.type)) {
GL_geometry_bytes_used -= buffer_obj.size;
GL_geometry_bytes_used += size;
}
buffer_obj.size = size;
GL_vertex_data_in += buffer_obj.size;
}
void gr_opengl_update_buffer_data_offset(gr_buffer_handle handle, size_t offset, size_t size, const void* data)
@@ -365,7 +386,12 @@ void gr_opengl_delete_buffer(gr_buffer_handle handle)
glDeleteTextures(1, &buffer_obj.texture);
}
GL_vertex_data_in -= buffer_obj.size;
if (opengl_buffer_is_geometry(buffer_obj.type)) {
GL_geometry_bytes_used -= buffer_obj.size;
}
// The size must go to 0 here. opengl_destroy_all_buffers() calls this function for every
// handle, thus a buffer that the game already deleted comes through a second time.
buffer_obj.size = 0;
glDeleteBuffers(1, &buffer_obj.buffer_id);
}
@@ -487,6 +513,7 @@ static bool opengl_init_shadow_framebuffer(int size)
// Everything is fine
mprintf(("Shadow framebuffer created successfully.\n"));
Shadow_texture_size = size;
Shadow_texture_layers = Num_shadow_cascades + Num_cockpit_shadow_cascades;
return true;
}
@@ -517,6 +544,22 @@ static bool opengl_init_shadow_framebuffer(int size)
return false;
}
size_t opengl_get_shadow_map_bytes()
{
if (Shadow_map_depth_texture == 0) {
return 0;
}
// opengl_init_shadow_framebuffer() gives the texture array the internal format
// GL_DEPTH_COMPONENT24, which the driver holds in 4 bytes for each texel.
constexpr size_t DEPTH_COMPONENT24_BYTES = 4;
const auto size = static_cast<size_t>(Shadow_texture_size);
const auto layers = static_cast<size_t>(Shadow_texture_layers);
return size * size * layers * DEPTH_COMPONENT24_BYTES;
}
void opengl_tnl_init()
{
Transform_buffer_handle = opengl_create_texture_buffer_object();
+16
View File
@@ -53,6 +53,22 @@ void gr_opengl_bind_uniform_buffer(uniform_block_type bind_point, size_t offset,
void* gr_opengl_map_buffer(gr_buffer_handle handle);
void gr_opengl_flush_mapped_buffer(gr_buffer_handle handle, size_t offset, size_t size);
/**
* @brief Gives the bytes that all live vertex and index buffers hold
*
* The profiler overlay's memory panel shows this value as the "geometry" group. Uniform buffers
* and texture buffers are not part of it.
*/
size_t opengl_get_geometry_bytes();
/**
* @brief Gives the bytes that the shadow map texture array holds
*
* The profiler overlay's memory panel adds this value to the "render targets" group. The function
* gives 0 when shadows are off.
*/
size_t opengl_get_shadow_map_bytes();
/**
* @brief Retrieves the OpenGL handle of a generic buffer handle
* @param expected_type The expected type of the buffer. Mainly used for debug checking.
+6
View File
@@ -70,6 +70,12 @@ void GPUMemoryHeap::freeGpuData(size_t offset) {
gr_buffer_handle GPUMemoryHeap::bufferHandle() {
return _bufferHandle;
}
size_t GPUMemoryHeap::usedBytes() const {
return _allocator->usedBytes();
}
size_t GPUMemoryHeap::bufferSize() const {
return _bufferSize;
}
}
}
+12
View File
@@ -54,6 +54,18 @@ class GPUMemoryHeap {
* @return The graphics code buffer handle.
*/
gr_buffer_handle bufferHandle();
/**
* @brief Gets the number of bytes currently allocated from this heap
* @return The sum of the sizes of all active allocations.
*/
size_t usedBytes() const;
/**
* @brief Gets the total size of the backing GPU buffer, including both allocated and free space
* @return The current buffer size.
*/
size_t bufferSize() const;
};
}
+18 -2
View File
@@ -339,7 +339,22 @@ vk::BufferUsageFlags VulkanBufferManager::getVkUsageFlags(BufferType type, bool
return flags;
}
MemoryUsage VulkanBufferManager::getMemoryUsage(BufferUsageHint hint)
namespace {
MemoryPurpose bufferTypeToPurpose(BufferType type)
{
switch (type) {
case BufferType::Vertex:
case BufferType::Index:
return MemoryPurpose::Geometry;
case BufferType::Uniform:
default:
// Uniform buffers are already tracked separately via UniformBufferManager/gr_debug_stats.
return MemoryPurpose::Unknown;
}
}
}
MemoryUsage VulkanBufferManager::getMemoryUsage(BufferUsageHint hint)
{
switch (hint) {
case BufferUsageHint::Static:
@@ -455,7 +470,8 @@ bool VulkanBufferManager::createOrResizeBuffer(VulkanBufferObject& bufferObj, si
// Allocate memory
MemoryUsage memUsage = getMemoryUsage(bufferObj.usage);
if (!m_memoryManager->allocateBufferMemory(bufferObj.buffer, memUsage, bufferObj.allocation)) {
if (!m_memoryManager->allocateBufferMemory(
bufferObj.buffer, memUsage, bufferObj.allocation, bufferTypeToPurpose(bufferObj.type))) {
m_device.destroyBuffer(bufferObj.buffer);
bufferObj.buffer = oldBuffer;
bufferObj.allocation = oldAllocation;
+14
View File
@@ -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;
+9
View File
@@ -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
+15 -18
View File
@@ -374,13 +374,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(
@@ -402,7 +402,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;
@@ -416,15 +416,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);
}
@@ -1014,12 +1007,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,
@@ -1053,15 +1046,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::Global, globalSet, nullptr);
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);
+119 -42
View File
@@ -19,6 +19,12 @@ static constexpr DescriptorBindingTemplate s_globalBindings[] = {
static constexpr DescriptorBindingTemplate s_globalTlasBinding{
GlobalBinding::Tlas, vk::DescriptorType::eAccelerationStructureKHR, 1, vk::ShaderStageFlagBits::eFragment};
// GLOBAL_DYNAMIC_OFFSET_COUNT is stated rather than derived because this set's template is
// assembled at runtime (see below); keep the two in agreement. The optional TLAS binding is not
// a dynamic UBO, so it cannot change the count either.
static_assert(dynamic_offset_count(s_globalBindings) == GLOBAL_DYNAMIC_OFFSET_COUNT,
"The Global set gained a dynamic binding; GLOBAL_DYNAMIC_OFFSET_COUNT must follow.");
// The Global set's layout must omit the TLAS binding entirely on devices without
// VK_KHR_acceleration_structure enabled -- a descriptor type the device didn't
// enable the extension for is invalid in vkCreateDescriptorSetLayout. So, unlike
@@ -27,26 +33,10 @@ static constexpr DescriptorBindingTemplate s_globalTlasBinding{
// the per-instance m_globalBindingsRuntime/m_globalTemplateRuntime members
// (see VulkanDescriptorManager.h), rather than compiled in as a fixed constexpr array.
static constexpr DescriptorBindingTemplate s_materialBindings[] = {
{MaterialBinding::ModelData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::TextureArray, vk::DescriptorType::eCombinedImageSampler, 16, vk::ShaderStageFlagBits::eFragment, vk::ImageViewType::e2DArray},
{MaterialBinding::DecalGlobals, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::TransformSSBO, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eVertex},
{MaterialBinding::DepthMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::SceneColor, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::DistortionMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::ShadowMapData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex},
};
static constexpr DescriptorSetTemplate s_materialTemplate(s_materialBindings);
static constexpr DescriptorBindingTemplate s_perDrawBindings[] = {
{PerDrawBinding::GenericData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::Matrices, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::NanoVGData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::DecalInfo, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::MovieData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment},
};
static constexpr DescriptorSetTemplate s_perDrawTemplate(s_perDrawBindings);
// The binding declarations themselves live in the header (MaterialSetBindings /
// PerDrawSetBindings) because the dynamic-offset layout is derived from them there.
static constexpr DescriptorSetTemplate s_materialTemplate(MaterialSetBindings);
static constexpr DescriptorSetTemplate s_perDrawTemplate(PerDrawSetBindings);
// ========== Static uniform binding mappings ==========
@@ -92,12 +82,21 @@ const vk::DescriptorImageInfo& DescriptorFallbacks::getImage(vk::ImageViewType t
// ========== DescriptorWriter template-based methods ==========
void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTemplate& tmpl)
void DescriptorWriter::writeSet(DescriptorSetIndex setIndex, vk::DescriptorSet set)
{
Assert(m_fallbacks);
const DescriptorSetTemplate& tmpl = VulkanDescriptorManager::getSetTemplate(setIndex);
// Clear binding slots for this set
m_bindingSlots = {};
m_currentSet = setIndex;
// Dynamic offsets are consumed in binding order, so assign slots as the template is walked
// (templates list bindings in ascending binding number). This must stay the same rule
// dynamic_offset_slot() applies — hence advancing by b.count, not by one binding.
uint32_t nextDynIndex = 0;
m_dynOffsets[static_cast<size_t>(setIndex)] = {};
for (const auto& b : tmpl) {
Assert(m_writeCount < MAX_WRITES);
@@ -116,6 +115,11 @@ void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTempla
bool isImage = (b.type == vk::DescriptorType::eCombinedImageSampler);
bool isAccelStruct = (b.type == vk::DescriptorType::eAccelerationStructureKHR);
if (b.type == vk::DescriptorType::eUniformBufferDynamic) {
Assert(nextDynIndex + b.count <= MAX_DYNAMIC_OFFSETS_PER_SET);
slot.dynIndex = static_cast<int>(nextDynIndex);
nextDynIndex += b.count;
}
if (isImage) {
Assert(m_imageInfoCount + b.count <= MAX_IMAGE_INFOS);
auto* dst = &m_imageInfos[m_imageInfoCount];
@@ -144,12 +148,40 @@ void DescriptorWriter::writeSet(vk::DescriptorSet set, const DescriptorSetTempla
} else {
Assert(m_bufferInfoCount < MAX_BUFFER_INFOS);
m_bufferInfos[m_bufferInfoCount] = m_fallbacks->buffer;
// The fallback buffer info starts at offset 0, which is also the base a
// dynamic descriptor needs (its offset lives in the dynamic-offset array,
// left at 0 here until setBuffer supplies a real one).
w.pBufferInfo = &m_bufferInfos[m_bufferInfoCount];
slot.bufferInfo = &m_bufferInfos[m_bufferInfoCount++];
}
}
}
void DescriptorWriter::bindSets(vk::CommandBuffer cmd,
vk::PipelineLayout layout,
DescriptorSetIndex firstSet,
ArrayView<vk::DescriptorSet> sets)
{
size_t out = 0;
for (size_t i = 0; i < sets.size; ++i) {
const auto set = static_cast<DescriptorSetIndex>(static_cast<uint32_t>(firstSet) + i);
Assert(static_cast<uint32_t>(set) < static_cast<uint32_t>(DescriptorSetIndex::Count));
const uint32_t dynCount = VulkanDescriptorManager::getDynamicOffsetCount(set);
Assert(dynCount <= MAX_DYNAMIC_OFFSETS_PER_SET);
for (uint32_t j = 0; j < dynCount; ++j) {
Assert(out < m_dynOffsetScratch.size());
m_dynOffsetScratch[out++] = m_dynOffsets[static_cast<size_t>(set)][j];
}
}
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
layout,
static_cast<uint32_t>(firstSet),
vk::ArrayProxy<const vk::DescriptorSet>(static_cast<uint32_t>(sets.size), sets.data),
vk::ArrayProxy<const uint32_t>(static_cast<uint32_t>(out), m_dynOffsetScratch.data()));
}
void DescriptorWriter::flush()
{
if (m_writeCount > 0) {
@@ -167,6 +199,25 @@ void DescriptorWriter::setBuffer(uint32_t binding, const vk::DescriptorBufferInf
Assert(binding < MAX_BINDINGS_PER_SET);
auto& slot = m_bindingSlots[binding];
Assert(slot.bufferInfo);
if (slot.dynIndex >= 0) {
// Dynamic binding: the descriptor holds only {buffer, 0, range}; the caller's
// offset becomes the dynamic offset applied at bind time. Keeping it out of
// the descriptor is what lets the set survive an offset-only change.
auto& dyn = m_dynOffsets[static_cast<size_t>(m_currentSet)][static_cast<size_t>(slot.dynIndex)];
if (info.buffer) {
Assertion(info.offset <= static_cast<vk::DeviceSize>(UINT32_MAX),
"Dynamic uniform buffer offset " SIZE_T_ARG " does not fit in a uint32 dynamic offset!",
static_cast<size_t>(info.offset));
*slot.bufferInfo = vk::DescriptorBufferInfo(info.buffer, 0, info.range);
dyn = static_cast<uint32_t>(info.offset);
} else {
*slot.bufferInfo = m_fallbacks->buffer;
dyn = 0;
}
return;
}
if (info.buffer) {
*slot.bufferInfo = info;
} else {
@@ -279,6 +330,23 @@ const DescriptorSetTemplate& VulkanDescriptorManager::getSetTemplate(DescriptorS
}
}
uint32_t VulkanDescriptorManager::getDynamicOffsetCount(DescriptorSetIndex setIndex)
{
// Constant per set (see the derived *_DYNAMIC_OFFSET_COUNT values), not a scan of the
// template — this runs on every descriptor set bind.
switch (setIndex) {
case DescriptorSetIndex::Global:
return GLOBAL_DYNAMIC_OFFSET_COUNT;
case DescriptorSetIndex::Material:
return MATERIAL_DYNAMIC_OFFSET_COUNT;
case DescriptorSetIndex::PerDraw:
return PERDRAW_DYNAMIC_OFFSET_COUNT;
default:
UNREACHABLE("Invalid descriptor set index %u!", static_cast<uint32_t>(setIndex));
return 0;
}
}
vk::DescriptorSetLayout VulkanDescriptorManager::getSetLayout(DescriptorSetIndex setIndex) const
{
return m_setLayouts[static_cast<size_t>(setIndex)].get();
@@ -294,22 +362,31 @@ vk::DescriptorSet VulkanDescriptorManager::allocateFrameSet(DescriptorSetIndex s
auto& pools = m_framePools[m_currentFrame];
++m_setsAllocatedThisFrame;
// Allocate through the non-throwing, non-allocating vulkan.hpp overload: this is
// one of the hottest calls in the backend (thousands per frame), and the
// vector-returning overload heap-allocates on every single one.
vk::DescriptorSetAllocateInfo allocInfo;
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &layout;
// Try allocating from the last pool in the list
if (!pools.empty()) {
vk::DescriptorSetAllocateInfo allocInfo;
allocInfo.descriptorPool = pools.back().get();
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &layout;
try {
auto sets = m_device.allocateDescriptorSets(allocInfo);
return sets[0];
} catch (const vk::OutOfPoolMemoryError&) {
// Pool exhausted, fall through to create a new one
vk::DescriptorSet set;
vk::Result result = m_device.allocateDescriptorSets(&allocInfo, &set);
if (result == vk::Result::eSuccess) {
return set;
}
if (result == vk::Result::eErrorOutOfPoolMemory) {
nprintf(("vulkan", "VulkanDescriptorManager: frame pool exhausted, allocating an additional pool chunk\n"));
} catch (const vk::FragmentedPoolError&) {
// Pool fragmented, fall through to create a new one
} else if (result == vk::Result::eErrorFragmentedPool) {
nprintf(("vulkan", "VulkanDescriptorManager: frame pool fragmented, allocating an additional pool chunk\n"));
} else {
nprintf(("vulkan",
"VulkanDescriptorManager: descriptor set allocation failed (%s)\n",
vk::to_string(result).c_str()));
return {};
}
}
@@ -318,18 +395,17 @@ vk::DescriptorSet VulkanDescriptorManager::allocateFrameSet(DescriptorSetIndex s
nprintf(("vulkandescriptor", "VulkanDescriptorManager: Grew frame %u pool count to %zu\n",
m_currentFrame, pools.size()));
vk::DescriptorSetAllocateInfo allocInfo;
allocInfo.descriptorPool = pools.back().get();
allocInfo.descriptorSetCount = 1;
allocInfo.pSetLayouts = &layout;
try {
auto sets = m_device.allocateDescriptorSets(allocInfo);
return sets[0];
} catch (const vk::SystemError& e) {
nprintf(("vulkan", "VulkanDescriptorManager: Failed to allocate frame descriptor set after pool growth: %s\n", e.what()));
vk::DescriptorSet set;
vk::Result result = m_device.allocateDescriptorSets(&allocInfo, &set);
if (result != vk::Result::eSuccess) {
nprintf(("vulkan",
"VulkanDescriptorManager: Failed to allocate frame descriptor set after pool growth: %s\n",
vk::to_string(result).c_str()));
return {};
}
return set;
}
void VulkanDescriptorManager::beginFrame()
@@ -403,9 +479,10 @@ vk::UniqueDescriptorPool VulkanDescriptorManager::createFramePool()
// constants in VulkanDescriptorManager.h. Additional pool chunks are created
// automatically when one is exhausted.
SCP_vector<vk::DescriptorPoolSize> poolSizes = {
{ vk::DescriptorType::eUniformBuffer, MAX_UNIFORM_BUFFERS_PER_POOL },
{ vk::DescriptorType::eCombinedImageSampler, MAX_SAMPLERS_PER_POOL },
{ vk::DescriptorType::eStorageBuffer, MAX_SETS_PER_POOL },
{vk::DescriptorType::eUniformBuffer, MAX_UNIFORM_BUFFERS_PER_POOL},
{vk::DescriptorType::eUniformBufferDynamic, MAX_DYNAMIC_UNIFORM_BUFFERS_PER_POOL},
{vk::DescriptorType::eCombinedImageSampler, MAX_SAMPLERS_PER_POOL},
{vk::DescriptorType::eStorageBuffer, MAX_SETS_PER_POOL},
};
if (m_raytracingEnabled) {
poolSizes.emplace_back(vk::DescriptorType::eAccelerationStructureKHR, MAX_SETS_PER_POOL);
+279 -79
View File
@@ -52,71 +52,6 @@ struct DescriptorFallbacks {
const vk::DescriptorImageInfo& getImage(vk::ImageViewType t) const;
};
/**
* @brief Stack-allocated batch writer for descriptor set updates.
*
* Usage: reset() + writeSet() (pre-fills all bindings with fallbacks)
* + setBuffer/setImage overrides for real data + flush().
*/
class DescriptorWriter {
public:
// Fixed capacities for the stack-allocated staging arrays below — NOT Vulkan
// hardware limits. They bound a single vkUpdateDescriptorSets batch (one
// writeSet), sized to comfortably cover the largest descriptor set layout the
// engine builds. Because the backing arrays live on the stack (see m_writes
// etc.), the numbers carry no runtime cost beyond that fixed footprint; the
// only failure mode is under-sizing, which is Assert-guarded at fill time in
// DescriptorWriter::writeSet (e.g. `Assert(m_writeCount < MAX_WRITES)`) so an
// oversized layout trips loudly in debug rather than silently overflowing.
// Raise these if a future set layout legitimately needs more bindings/infos.
static constexpr uint32_t MAX_WRITES = 32; // distinct bindings written per batch
static constexpr uint32_t MAX_BUFFER_INFOS = 20; // buffer descriptors staged per batch
static constexpr uint32_t MAX_IMAGE_INFOS = 24; // image/sampler descriptors staged per batch
static constexpr uint32_t MAX_ACCEL_STRUCT_INFOS = 2; // TLAS descriptors staged per batch (RT)
static constexpr uint32_t MAX_BINDINGS_PER_SET = 16; // highest binding number addressable in a set
void reset(vk::Device device, const DescriptorFallbacks& fallbacks) {
m_device = device;
m_fallbacks = &fallbacks;
m_writeCount = 0;
m_bufferInfoCount = 0;
m_imageInfoCount = 0;
m_accelStructInfoCount = 0;
}
void writeSet(vk::DescriptorSet set, const DescriptorSetTemplate& tmpl);
void setBuffer(uint32_t binding, const vk::DescriptorBufferInfo& info);
void setImage(uint32_t binding, const vk::DescriptorImageInfo& info);
void setImageArray(uint32_t binding, ArrayView<vk::DescriptorImageInfo> infos);
void flush(); // defined in VulkanDescriptorManager.cpp (reports write stats to the manager)
private:
// Per-binding lookup for the current writeSet, indexed by binding number.
// Populated by writeSet, used by setBuffer/setImage/setImageArray for O(1) access.
struct BindingSlot {
vk::DescriptorBufferInfo* bufferInfo = nullptr; // non-null for buffer bindings
vk::DescriptorImageInfo* imageInfo = nullptr; // non-null for image bindings
uint32_t count = 0; // descriptor count (1 or 16 for arrays)
vk::ImageViewType viewType = vk::ImageViewType::e2D; // for fallback lookup
};
vk::Device m_device;
const DescriptorFallbacks* m_fallbacks = nullptr;
std::array<vk::WriteDescriptorSet, MAX_WRITES> m_writes;
std::array<vk::DescriptorBufferInfo, MAX_BUFFER_INFOS> m_bufferInfos;
std::array<vk::DescriptorImageInfo, MAX_IMAGE_INFOS> m_imageInfos;
std::array<vk::AccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_accelStructInfos;
std::array<vk::WriteDescriptorSetAccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_asWriteInfos;
std::array<BindingSlot, MAX_BINDINGS_PER_SET> m_bindingSlots;
uint32_t m_writeCount = 0;
uint32_t m_bufferInfoCount = 0;
uint32_t m_imageInfoCount = 0;
uint32_t m_accelStructInfoCount = 0;
};
/**
* @brief Descriptor set indices for the 3-tier layout
*
@@ -146,15 +81,18 @@ namespace GlobalBinding {
}
// Material Set (Set 1) bindings — per-material data
// ModelData and ShadowMapData are *dynamic* UBOs: their offset moves every draw
// while the rest of the set is unchanged, so keeping the offset out of the
// descriptor is what makes the set cacheable across draws.
namespace MaterialBinding {
static constexpr uint32_t ModelData = 0; // UBO: model/material data
static constexpr uint32_t TextureArray = 1; // sampler2D[16]: material textures
static constexpr uint32_t DecalGlobals = 2; // UBO: decal globals
static constexpr uint32_t TransformSSBO = 3; // SSBO: batched transforms
static constexpr uint32_t DepthMap = 4; // sampler2D: depth (soft particles)
static constexpr uint32_t SceneColor = 5; // sampler2D: scene color (distortion)
static constexpr uint32_t DistortionMap = 6; // sampler2D: distortion texture
static constexpr uint32_t ShadowMapData = 7; // UBO: shadow map generation per-draw data (shadow_render_list)
static constexpr uint32_t ModelData = 0; // dynamic UBO: model/material data
static constexpr uint32_t TextureArray = 1; // sampler2D[16]: material textures
static constexpr uint32_t DecalGlobals = 2; // UBO: decal globals
static constexpr uint32_t TransformSSBO = 3; // SSBO: batched transforms
static constexpr uint32_t DepthMap = 4; // sampler2D: depth (soft particles)
static constexpr uint32_t SceneColor = 5; // sampler2D: scene color (distortion)
static constexpr uint32_t DistortionMap = 6; // sampler2D: distortion texture
static constexpr uint32_t ShadowMapData = 7; // dynamic UBO: shadow map generation per-draw data (shadow_render_list)
}
// Texture array slot indices (elements within MaterialBinding::TextureArray)
@@ -169,14 +107,256 @@ namespace TextureSlot {
}
// PerDraw Set (Set 2) bindings — per-draw-call data
// GenericData and Matrices are *dynamic* UBOs, for the same reason as the
// Material set's ModelData.
namespace PerDrawBinding {
static constexpr uint32_t GenericData = 0; // UBO: generic shader data
static constexpr uint32_t Matrices = 1; // UBO: transform matrices
static constexpr uint32_t NanoVGData = 2; // UBO: NanoVG data
static constexpr uint32_t DecalInfo = 3; // UBO: per-decal info
static constexpr uint32_t MovieData = 4; // UBO: movie playback data
static constexpr uint32_t GenericData = 0; // dynamic UBO: generic shader data
static constexpr uint32_t Matrices = 1; // dynamic UBO: transform matrices
static constexpr uint32_t NanoVGData = 2; // UBO: NanoVG data
static constexpr uint32_t DecalInfo = 3; // UBO: per-decal info
static constexpr uint32_t MovieData = 4; // UBO: movie playback data
}
// ========== Set layout templates ==========
//
// These describe the fixed set layouts. Everything downstream that needs to know
// which bindings are dynamic, how many there are, or which dynamic-offset slot a
// given binding occupies is derived from them below rather than restated, so the
// declarations here are the only place that ordering exists.
inline constexpr DescriptorBindingTemplate MaterialSetBindings[] = {
{MaterialBinding::ModelData,
vk::DescriptorType::eUniformBufferDynamic,
1,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::TextureArray,
vk::DescriptorType::eCombinedImageSampler,
16,
vk::ShaderStageFlagBits::eFragment,
vk::ImageViewType::e2DArray},
{MaterialBinding::DecalGlobals,
vk::DescriptorType::eUniformBuffer,
1,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::TransformSSBO, vk::DescriptorType::eStorageBuffer, 1, vk::ShaderStageFlagBits::eVertex},
{MaterialBinding::DepthMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::SceneColor, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::DistortionMap, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment},
{MaterialBinding::ShadowMapData, vk::DescriptorType::eUniformBufferDynamic, 1, vk::ShaderStageFlagBits::eVertex},
};
inline constexpr DescriptorBindingTemplate PerDrawSetBindings[] = {
{PerDrawBinding::GenericData,
vk::DescriptorType::eUniformBufferDynamic,
1,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::Matrices,
vk::DescriptorType::eUniformBufferDynamic,
1,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::NanoVGData,
vk::DescriptorType::eUniformBuffer,
1,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::DecalInfo,
vk::DescriptorType::eUniformBuffer,
1,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
{PerDrawBinding::MovieData, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eFragment},
};
// ========== Dynamic-offset layout, derived from the templates above ==========
//
// vkCmdBindDescriptorSets consumes a set's dynamic offsets in ascending binding
// order, one entry per eUniformBufferDynamic descriptor. Both the count and the
// slot each binding occupies are computed from the set templates so a call site
// can never disagree with the layout it is binding against — reordering a template
// moves the derived slots with it instead of silently feeding a shader the wrong
// uniforms.
template <size_t N>
constexpr uint32_t dynamic_offset_count(const DescriptorBindingTemplate (&bindings)[N])
{
uint32_t count = 0;
for (size_t i = 0; i < N; ++i) {
if (bindings[i].type == vk::DescriptorType::eUniformBufferDynamic) {
count += bindings[i].count;
}
}
return count;
}
// Returned by dynamic_offset_slot() when the binding is not a dynamic descriptor of the set.
constexpr uint32_t DYNAMIC_SLOT_INVALID = ~0u;
/**
* @brief Position of a dynamic binding within its set's dynamic-offset array
*
* If @c binding is not a dynamic binding of the given set, @c DYNAMIC_SLOT_INVALID is returned.
* Every use below pairs its constant with a static_assert against that sentinel, so a mistyped or
* non-dynamic binding constant is a compile error rather than a silent 0 — these constants index
* fixed-size dynamic-offset arrays at the bind call sites, so a leaked sentinel would be an
* out-of-bounds write.
*
* Do not "clean this up" back into a `throw` on the not-found path: MSVC rejects a constexpr
* function whose flow analysis can reach a throw (C3615), even when no evaluation takes that path.
*/
template <size_t N>
constexpr uint32_t dynamic_offset_slot(const DescriptorBindingTemplate (&bindings)[N], uint32_t binding)
{
uint32_t slot = 0;
for (size_t i = 0; i < N; ++i) {
if (bindings[i].type != vk::DescriptorType::eUniformBufferDynamic) {
continue;
}
if (bindings[i].binding == binding) {
return slot;
}
slot += bindings[i].count;
}
return DYNAMIC_SLOT_INVALID;
}
// The Global set declares no dynamic bindings; it contributes nothing to a bind call.
constexpr uint32_t GLOBAL_DYNAMIC_OFFSET_COUNT = 0;
constexpr uint32_t MATERIAL_DYNAMIC_OFFSET_COUNT = dynamic_offset_count(MaterialSetBindings);
constexpr uint32_t PERDRAW_DYNAMIC_OFFSET_COUNT = dynamic_offset_count(PerDrawSetBindings);
namespace MaterialDynamicSlot {
constexpr uint32_t ModelData = dynamic_offset_slot(MaterialSetBindings, MaterialBinding::ModelData);
constexpr uint32_t ShadowMapData = dynamic_offset_slot(MaterialSetBindings, MaterialBinding::ShadowMapData);
static_assert(ModelData != DYNAMIC_SLOT_INVALID,
"MaterialBinding::ModelData is not a dynamic descriptor of the Material set.");
static_assert(ShadowMapData != DYNAMIC_SLOT_INVALID,
"MaterialBinding::ShadowMapData is not a dynamic descriptor of the Material set.");
}
namespace PerDrawDynamicSlot {
constexpr uint32_t GenericData = dynamic_offset_slot(PerDrawSetBindings, PerDrawBinding::GenericData);
constexpr uint32_t Matrices = dynamic_offset_slot(PerDrawSetBindings, PerDrawBinding::Matrices);
static_assert(GenericData != DYNAMIC_SLOT_INVALID,
"PerDrawBinding::GenericData is not a dynamic descriptor of the PerDraw set.");
static_assert(Matrices != DYNAMIC_SLOT_INVALID,
"PerDrawBinding::Matrices is not a dynamic descriptor of the PerDraw set.");
}
/**
* @brief Stack-allocated batch writer for descriptor set updates.
*
* Usage: reset() + writeSet() (pre-fills all bindings with fallbacks)
* + setBuffer/setImage overrides for real data + flush().
*
* Bindings declared as eUniformBufferDynamic get their offset split out of the
* descriptor and into the dynamic-offset array (see dynamicOffsets()): setBuffer
* writes {buffer, 0, range} into the descriptor and stashes the caller's offset.
* That is what lets a set whose only per-draw change is a UBO offset be written
* once and rebound many times. Callers must hand dynamicOffsets()/
* VulkanDescriptorManager::getDynamicOffsetCount() for a set to
* vkCmdBindDescriptorSets when binding it.
*/
class DescriptorWriter {
public:
// Fixed capacities for the stack-allocated staging arrays below — NOT Vulkan
// hardware limits. They bound a single vkUpdateDescriptorSets batch (one
// writeSet), sized to comfortably cover the largest descriptor set layout the
// engine builds. Because the backing arrays live on the stack (see m_writes
// etc.), the numbers carry no runtime cost beyond that fixed footprint; the
// only failure mode is under-sizing, which is Assert-guarded at fill time in
// DescriptorWriter::writeSet (e.g. `Assert(m_writeCount < MAX_WRITES)`) so an
// oversized layout trips loudly in debug rather than silently overflowing.
// Raise these if a future set layout legitimately needs more bindings/infos.
static constexpr uint32_t MAX_WRITES = 32; // distinct bindings written per batch
static constexpr uint32_t MAX_BUFFER_INFOS = 20; // buffer descriptors staged per batch
static constexpr uint32_t MAX_IMAGE_INFOS = 24; // image/sampler descriptors staged per batch
static constexpr uint32_t MAX_ACCEL_STRUCT_INFOS = 2; // TLAS descriptors staged per batch (RT)
static constexpr uint32_t MAX_BINDINGS_PER_SET = 16; // highest binding number addressable in a set
// Most dynamic (eUniformBufferDynamic) bindings any one set layout declares. Derived from
// the templates rather than counted by hand, so adding a dynamic binding resizes the
// storage that holds its offset automatically.
static constexpr uint32_t MAX_DYNAMIC_OFFSETS_PER_SET =
MATERIAL_DYNAMIC_OFFSET_COUNT > PERDRAW_DYNAMIC_OFFSET_COUNT ? MATERIAL_DYNAMIC_OFFSET_COUNT
: PERDRAW_DYNAMIC_OFFSET_COUNT;
void reset(vk::Device device, const DescriptorFallbacks& fallbacks) {
m_device = device;
m_fallbacks = &fallbacks;
m_writeCount = 0;
m_bufferInfoCount = 0;
m_imageInfoCount = 0;
m_accelStructInfoCount = 0;
m_dynOffsets = {};
}
void writeSet(DescriptorSetIndex setIndex, vk::DescriptorSet set);
void setBuffer(uint32_t binding, const vk::DescriptorBufferInfo& info);
void setImage(uint32_t binding, const vk::DescriptorImageInfo& info);
void setImageArray(uint32_t binding, ArrayView<vk::DescriptorImageInfo> infos);
/**
* @brief Dynamic offsets accumulated for a single set, ordered by binding number.
*
* Only meaningful for a set this writer actually wrote; a set reused from a
* memoization cache was not written here, so its caller owns the offsets.
* Always sized to the set layout's dynamic descriptor count (offsets for
* bindings left at their fallback stay 0). Points at stable per-set storage —
* valid until the next writeSet() of that set.
*/
const uint32_t* dynamicOffsets(DescriptorSetIndex setIndex) const
{
return m_dynOffsets[static_cast<size_t>(setIndex)].data();
}
/**
* @brief Binds a contiguous run of sets in one vkCmdBindDescriptorSets call
*
* Concatenates the run's dynamic offsets in set order, which is the layout
* vkCmdBindDescriptorSets expects: each set contributes exactly its layout's
* dynamic descriptor count (a set declaring none contributes nothing, rather
* than padding). @c sets must list the sets for @c firstSet onwards, in order.
*
* Binding here rather than handing the offsets back to the caller keeps the
* concatenation buffer's lifetime contained: it is writer-owned scratch, reused
* by the next call, and never outlives the command it was built for.
*/
void bindSets(vk::CommandBuffer cmd,
vk::PipelineLayout layout,
DescriptorSetIndex firstSet,
ArrayView<vk::DescriptorSet> sets);
void flush(); // defined in VulkanDescriptorManager.cpp (reports write stats to the manager)
private:
// Per-binding lookup for the current writeSet, indexed by binding number.
// Populated by writeSet, used by setBuffer/setImage/setImageArray for O(1) access.
struct BindingSlot {
vk::DescriptorBufferInfo* bufferInfo = nullptr; // non-null for buffer bindings
vk::DescriptorImageInfo* imageInfo = nullptr; // non-null for image bindings
uint32_t count = 0; // descriptor count (1 or 16 for arrays)
vk::ImageViewType viewType = vk::ImageViewType::e2D; // for fallback lookup
int dynIndex = -1; // slot in m_dynOffsets, or -1 if not dynamic
};
vk::Device m_device;
const DescriptorFallbacks* m_fallbacks = nullptr;
std::array<vk::WriteDescriptorSet, MAX_WRITES> m_writes;
std::array<vk::DescriptorBufferInfo, MAX_BUFFER_INFOS> m_bufferInfos;
std::array<vk::DescriptorImageInfo, MAX_IMAGE_INFOS> m_imageInfos;
std::array<vk::AccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_accelStructInfos;
std::array<vk::WriteDescriptorSetAccelerationStructureKHR, MAX_ACCEL_STRUCT_INFOS> m_asWriteInfos;
std::array<BindingSlot, MAX_BINDINGS_PER_SET> m_bindingSlots;
std::array<std::array<uint32_t, MAX_DYNAMIC_OFFSETS_PER_SET>, static_cast<size_t>(DescriptorSetIndex::Count)>
m_dynOffsets{};
// Scratch used by bindSets() to concatenate a run of sets' offsets.
std::array<uint32_t, static_cast<size_t>(DescriptorSetIndex::Count) * MAX_DYNAMIC_OFFSETS_PER_SET>
m_dynOffsetScratch{};
DescriptorSetIndex m_currentSet = DescriptorSetIndex::Global;
uint32_t m_writeCount = 0;
uint32_t m_bufferInfoCount = 0;
uint32_t m_imageInfoCount = 0;
uint32_t m_accelStructInfoCount = 0;
};
/**
* @brief Manages Vulkan descriptor sets, pools, and layouts
@@ -195,8 +375,19 @@ public:
// larger chunks waste memory, smaller chunks allocate pools more often.
// MAX_SETS_PER_POOL supports ~330 draw calls (3 sets each) per chunk; the per-
// type multipliers cover the worst-case bindings a single set can request.
// One chunk now covers a dense scene outright: with the dynamic-UBO descriptor
// memoization in applyMaterial/renderShadowDraw, a big asteroid field at ~1350
// model draws settles at ~250 sets/frame, so growth is back to being the rare
// case it was meant to be. (The same scene needed ~2500 sets/frame before that.)
// Deliberately left at the original size rather than raised: the per-frame
// growth this used to log was a symptom of the churn, not of undersizing, and
// FSO targets low-end GPUs where a larger pool is real wasted memory.
static constexpr uint32_t MAX_SETS_PER_POOL = 1024;
static constexpr uint32_t MAX_UNIFORM_BUFFERS_PER_POOL = MAX_SETS_PER_POOL * 9; // up to 9 UBOs per draw
// Worst case per set is 3 static UBOs (Global, PerDraw) and 2 dynamic (Material:
// ModelData + ShadowMapData; PerDraw: GenericData + Matrices). Total UBO
// capacity per chunk is slightly below what this was before the dynamic split.
static constexpr uint32_t MAX_UNIFORM_BUFFERS_PER_POOL = MAX_SETS_PER_POOL * 5; // static UBOs
static constexpr uint32_t MAX_DYNAMIC_UNIFORM_BUFFERS_PER_POOL = MAX_SETS_PER_POOL * 2; // dynamic UBOs
static constexpr uint32_t MAX_SAMPLERS_PER_POOL = MAX_SETS_PER_POOL * 16; // up to 16 samplers per material set
VulkanDescriptorManager() = default;
@@ -254,6 +445,15 @@ public:
*/
static const DescriptorSetTemplate& getSetTemplate(DescriptorSetIndex setIndex);
/**
* @brief Number of dynamic (eUniformBufferDynamic) descriptors in a set layout
*
* vkCmdBindDescriptorSets requires exactly this many entries in pDynamicOffsets,
* so every bind site must agree with the layout. VulkanStateTracker asserts on
* it; raw cmd.bindDescriptorSets() callers should pass the matching count.
*/
static uint32_t getDynamicOffsetCount(DescriptorSetIndex setIndex);
/**
* @brief Get descriptor set layout for a given set index
*/
+132 -49
View File
@@ -686,8 +686,12 @@ void VulkanDrawManager::renderModel(model_material* material_info, indexed_verte
stateTracker->getCommandBuffer().drawIndexed(dp.indexCount, 1, dp.firstIndex, dp.baseVertex, 0);
}
void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle, size_t ubo_offset, size_t ubo_size,
vertex_buffer* buffer, indexed_vertex_source* vert_src, size_t texi) const
void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle,
size_t ubo_offset,
size_t ubo_size,
vertex_buffer* buffer,
indexed_vertex_source* vert_src,
size_t texi)
{
if (!buffer || !vert_src) {
return;
@@ -754,51 +758,90 @@ void VulkanDrawManager::renderShadowDraw(gr_buffer_handle ubo_handle, size_t ubo
// (bound separately, per-frame, via shadow_cascade_params_bind) both live in
// the fixed 3-tier layout alongside the batched-submodel transform buffer;
// no material textures are needed for depth-only rendering.
//
// All three sets are memoized across the shadow pass (see m_cachedShadowSets).
// ShadowMapData is a dynamic binding, so the only thing that changes from one
// shadow draw to the next -- its UBO offset -- rides in the dynamic-offset array
// and leaves the set contents untouched. Without that this path allocated and
// fully rewrote three descriptor sets per shadow draw, times the cascade count.
{
DescriptorWriter writer;
writer.reset(descManager->getDevice(), descManager->getFallbacks());
vk::DescriptorSet globalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
Assert(globalSet);
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
ShadowSetInputs shadowInputs;
{
const auto& pending = getPendingUniformBinding(static_cast<size_t>(uniform_block_type::ShadowCascadeParams));
if (pending.valid) {
vk::Buffer buf = bufferManager->getVkBuffer(pending.bufferHandle);
const auto& cascade =
getPendingUniformBinding(static_cast<size_t>(uniform_block_type::ShadowCascadeParams));
shadowInputs.cascadeHandle = cascade.bufferHandle.value();
shadowInputs.cascadeOffset = cascade.offset;
shadowInputs.cascadeSize = cascade.size;
shadowInputs.cascadeValid = cascade.valid;
shadowInputs.shadowDataHandle = ubo_handle.value();
shadowInputs.shadowDataSize = ubo_size; // offset is dynamic, so not part of the key
auto& tf = g_transformBuffers[descManager->getCurrentFrame()];
if (tf.buffer && tf.lastUploadSize > 0) {
shadowInputs.transformBuffer = tf.buffer;
shadowInputs.transformOffset = tf.lastUploadOffset;
shadowInputs.transformSize = tf.lastUploadSize;
}
}
if (!m_cachedShadowValid || shadowInputs != m_cachedShadowInputs) {
DescriptorWriter writer;
writer.reset(descManager->getDevice(), descManager->getFallbacks());
m_cachedShadowGlobalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
Assert(m_cachedShadowGlobalSet);
writer.writeSet(DescriptorSetIndex::Global, m_cachedShadowGlobalSet);
if (shadowInputs.cascadeValid) {
vk::Buffer buf = bufferManager->getVkBuffer(gr_buffer_handle(shadowInputs.cascadeHandle));
if (buf) {
writer.setBuffer(GlobalBinding::ShadowCascadeParams, {buf, pending.offset, pending.size});
writer.setBuffer(GlobalBinding::ShadowCascadeParams,
{buf, shadowInputs.cascadeOffset, shadowInputs.cascadeSize});
}
}
}
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
{
vk::Buffer buf = bufferManager->getVkBuffer(ubo_handle);
if (buf) {
writer.setBuffer(MaterialBinding::ShadowMapData,
{buf, static_cast<vk::DeviceSize>(ubo_offset), static_cast<vk::DeviceSize>(ubo_size)});
m_cachedShadowMaterialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
Assert(m_cachedShadowMaterialSet);
writer.writeSet(DescriptorSetIndex::Material, m_cachedShadowMaterialSet);
{
vk::Buffer buf = bufferManager->getVkBuffer(ubo_handle);
if (buf) {
// Offset 0: the real offset is supplied per draw as a dynamic offset.
writer.setBuffer(MaterialBinding::ShadowMapData, {buf, 0, static_cast<vk::DeviceSize>(ubo_size)});
}
}
}
{
uint32_t tfIdx = descManager->getCurrentFrame();
auto& tf = g_transformBuffers[tfIdx];
if (tf.buffer && tf.lastUploadSize > 0) {
writer.setBuffer(MaterialBinding::TransformSSBO, {tf.buffer,
static_cast<vk::DeviceSize>(tf.lastUploadOffset),
static_cast<vk::DeviceSize>(tf.lastUploadSize)});
if (shadowInputs.transformBuffer) {
writer.setBuffer(MaterialBinding::TransformSSBO,
{shadowInputs.transformBuffer,
static_cast<vk::DeviceSize>(shadowInputs.transformOffset),
static_cast<vk::DeviceSize>(shadowInputs.transformSize)});
}
m_cachedShadowPerDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(m_cachedShadowPerDrawSet);
writer.writeSet(DescriptorSetIndex::PerDraw, m_cachedShadowPerDrawSet);
writer.flush();
m_cachedShadowInputs = shadowInputs;
m_cachedShadowValid = true;
}
vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.flush();
// applyMaterial's caches deliberately are NOT invalidated here. They record
// what a descriptor set *contains*, and the shadow pass allocates its own
// sets rather than overwriting theirs. Which set is currently bound is
// tracked separately by VulkanStateTracker, and this path binds through it,
// so the next applyMaterial cache hit still rebinds its set correctly. (That
// is what separates this from the ImGui case invalidateDrawStateCaches()
// exists for: ImGui binds on the command buffer behind the tracker's back.)
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
// ShadowMapData is the one thing that moves per shadow draw; every other dynamic binding
// on this path holds the fallback buffer and so must stay at offset 0.
uint32_t materialDynOffsets[MATERIAL_DYNAMIC_OFFSET_COUNT] = {};
materialDynOffsets[MaterialDynamicSlot::ShadowMapData] = static_cast<uint32_t>(ubo_offset);
static constexpr uint32_t perDrawDynOffsets[PERDRAW_DYNAMIC_OFFSET_COUNT] = {};
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, m_cachedShadowGlobalSet, nullptr);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, m_cachedShadowMaterialSet, materialDynOffsets);
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, m_cachedShadowPerDrawSet, perDrawDynOffsets);
}
vk::Buffer vbuffer = bufferManager->getVkBuffer(vert_src->Vbuffer_handle);
@@ -960,6 +1003,10 @@ void VulkanDrawManager::resetFrameStats()
m_cachedMaterialValid = false;
m_cachedPerDrawSet = nullptr;
m_cachedPerDrawValid = false;
m_cachedShadowGlobalSet = nullptr;
m_cachedShadowMaterialSet = nullptr;
m_cachedShadowPerDrawSet = nullptr;
m_cachedShadowValid = false;
}
void VulkanDrawManager::printFrameStats()
@@ -1349,6 +1396,28 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
}
};
// Dynamic offsets for the Material/PerDraw sets, ordered by binding number.
// Computed here rather than read back from the writer because the whole point
// of the dynamic bindings is that these change on draws where the set itself
// is reused from cache and never written.
const auto dynOffsetOf = [&](uniform_block_type blockType) -> uint32_t {
const auto& pending = m_pendingUniformBindings[static_cast<size_t>(blockType)];
if (!pending.valid || !bufferManager->getVkBuffer(pending.bufferHandle)) {
return 0; // falls back to the dummy buffer, which is bound at offset 0
}
return static_cast<uint32_t>(pending.offset);
};
// Zero-initialized, then filled by name: MaterialDynamicSlot::ShadowMapData is left at 0
// because ShadowMapData is never bound on this path, so its descriptor still holds the
// fallback buffer -- bound as {buffer, 0, FALLBACK_UNIFORM_BUFFER_SIZE}, i.e. spanning the
// whole buffer, so any non-zero dynamic offset would push offset+range past the end of it.
uint32_t materialDynOffsets[MATERIAL_DYNAMIC_OFFSET_COUNT] = {};
materialDynOffsets[MaterialDynamicSlot::ModelData] = dynOffsetOf(uniform_block_type::ModelData);
uint32_t perDrawDynOffsets[PERDRAW_DYNAMIC_OFFSET_COUNT] = {};
perDrawDynOffsets[PerDrawDynamicSlot::GenericData] = dynOffsetOf(uniform_block_type::GenericData);
perDrawDynOffsets[PerDrawDynamicSlot::Matrices] = dynOffsetOf(uniform_block_type::Matrices);
// Set 0: Global (memoized per frame — see m_cachedGlobalSet). Rebuilt only
// when a Global input changed: a pending Global UBO (m_globalSetDirty set in
// setPendingUniformBinding), the shadow TLAS (invalidateGlobalSet from
@@ -1360,7 +1429,7 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
if (m_globalSetDirty || !m_cachedGlobalSet || shadowReady != m_cachedGlobalHadShadow) {
m_cachedGlobalSet = descManager->allocateFrameSet(DescriptorSetIndex::Global);
Assert(m_cachedGlobalSet);
writer.writeSet(m_cachedGlobalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
writer.writeSet(DescriptorSetIndex::Global, m_cachedGlobalSet);
bindPendingUBOs(DescriptorSetIndex::Global);
if (shadowReady) {
writer.setImage(GlobalBinding::ShadowMap, pp->getShadowTextureInfo());
@@ -1397,9 +1466,14 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
matInputs.sceneColorInfo = m_sceneColorInfo;
matInputs.distMapInfo = m_distMapInfo;
{
// ModelData's offset is deliberately NOT part of the key: it is a dynamic
// binding, so an offset-only change is carried by materialDynOffsets and
// leaves the set's contents identical. Buffer and size still matter --
// both live in the descriptor.
const auto& md = m_pendingUniformBindings[static_cast<size_t>(uniform_block_type::ModelData)];
const auto& dg = m_pendingUniformBindings[static_cast<size_t>(uniform_block_type::DecalGlobals)];
matInputs.uboHandle[0] = md.bufferHandle.value(); matInputs.uboOffset[0] = md.offset;
matInputs.uboHandle[0] = md.bufferHandle.value();
matInputs.uboOffset[0] = 0;
matInputs.uboSize[0] = md.size; matInputs.uboValid[0] = md.valid;
matInputs.uboHandle[1] = dg.bufferHandle.value(); matInputs.uboOffset[1] = dg.offset;
matInputs.uboSize[1] = dg.size; matInputs.uboValid[1] = dg.valid;
@@ -1413,7 +1487,7 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
} else {
materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
bindPendingUBOs(DescriptorSetIndex::Material);
if (matInputs.transformBuffer) {
writer.setBuffer(MaterialBinding::TransformSSBO,
@@ -1433,14 +1507,23 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
// Set 2: PerDraw (previous-set memoized — see m_cachedPerDrawSet)
PerDrawSetInputs pdInputs;
{
static constexpr uniform_block_type pdTypes[NUM_PERDRAW_UBOS] = {
uniform_block_type::GenericData, uniform_block_type::Matrices,
uniform_block_type::NanoVGData, uniform_block_type::DecalInfo,
uniform_block_type::MovieData};
struct PerDrawUbo {
uniform_block_type blockType;
uint32_t binding;
};
static constexpr PerDrawUbo pdUbos[NUM_PERDRAW_UBOS] = {
{uniform_block_type::GenericData, PerDrawBinding::GenericData},
{uniform_block_type::Matrices, PerDrawBinding::Matrices},
{uniform_block_type::NanoVGData, PerDrawBinding::NanoVGData},
{uniform_block_type::DecalInfo, PerDrawBinding::DecalInfo},
{uniform_block_type::MovieData, PerDrawBinding::MovieData}};
for (int i = 0; i < NUM_PERDRAW_UBOS; ++i) {
const auto& p = m_pendingUniformBindings[static_cast<size_t>(pdTypes[i])];
const auto& p = m_pendingUniformBindings[static_cast<size_t>(pdUbos[i].blockType)];
pdInputs.uboHandle[i] = p.bufferHandle.value();
pdInputs.uboOffset[i] = p.offset;
// The offset of a dynamic binding rides in perDrawDynOffsets and is not part
// of the set's contents.
const bool dynamic = dynamic_offset_slot(PerDrawSetBindings, pdUbos[i].binding) != DYNAMIC_SLOT_INVALID;
pdInputs.uboOffset[i] = dynamic ? 0 : p.offset;
pdInputs.uboSize[i] = p.size;
pdInputs.uboValid[i] = p.valid;
}
@@ -1452,16 +1535,16 @@ bool VulkanDrawManager::applyMaterial(material* mat, primitive_type prim_type, v
} else {
perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
bindPendingUBOs(DescriptorSetIndex::PerDraw);
m_cachedPerDrawSet = perDrawSet;
m_cachedPerDrawInputs = pdInputs;
m_cachedPerDrawValid = true;
}
writer.flush();
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Global, globalSet, nullptr);
stateTracker->bindDescriptorSet(DescriptorSetIndex::Material, materialSet, materialDynOffsets);
stateTracker->bindDescriptorSet(DescriptorSetIndex::PerDraw, perDrawSet, perDrawDynOffsets);
}
// Update tracked state for FSO compatibility
+64 -1
View File
@@ -212,7 +212,7 @@ class VulkanDrawManager {
size_t ubo_size,
vertex_buffer* buffer,
indexed_vertex_source* vert_src,
size_t texi) const;
size_t texi);
/**
* @brief Draw a unit sphere with the given material
@@ -286,6 +286,26 @@ class VulkanDrawManager {
*/
void invalidateGlobalSet() { m_globalSetDirty = true; }
/**
* @brief Invalidate every memoized descriptor-set cache (Global + Material + PerDraw)
*
* These previous-set caches (see the class comments above m_cachedGlobalSet /
* m_cachedMaterialSet / m_cachedPerDrawSet) assume nothing rebinds a *different*
* descriptor set on the command buffer between applyMaterial() calls behind their
* back. Code that renders directly on the command buffer without going through
* applyMaterial (currently: ImGui's Vulkan backend, drawing into the same active
* composition pass) breaks that assumption -- call this right afterward, alongside
* VulkanStateTracker::invalidateExternalBindings(), so the next applyMaterial() rebuilds
* and rebinds every set instead of trusting stale cached handles.
*/
void invalidateDrawStateCaches()
{
m_globalSetDirty = true;
m_cachedMaterialValid = false;
m_cachedPerDrawValid = false;
m_cachedShadowValid = false;
}
/**
* @brief Get current texture addressing mode
*/
@@ -449,6 +469,14 @@ class VulkanDrawManager {
mutable FrameStats m_frameStats;
int m_frameStatsFrameNum = 0;
public:
/**
* @brief Read-only access to this frame's diagnostic counters, e.g. for the ImGui profiler
* overlay's -gr_debug section (see printFrameStats() for the nprintf equivalent)
*/
const FrameStats& getFrameStats() const { return m_frameStats; }
private:
// First-N debug-log counter for on-demand texture binds; a member rather
// than a function-local static so it resets on renderer restart. mutable because
// bindMaterialTextures is const. Gates nprintf spam only.
@@ -540,6 +568,41 @@ class VulkanDrawManager {
PerDrawSetInputs m_cachedPerDrawInputs;
bool m_cachedPerDrawValid = false;
// ---- Shadow-pass descriptor memoization (renderShadowDraw) ----
// The shadow pass doesn't go through applyMaterial, so it gets its own cache.
// All three of its sets are constant across the whole pass: the only per-draw
// input, the ShadowMapData UBO offset, is a dynamic binding and therefore not
// part of the set contents. Reset per frame with the rest (resetFrameStats).
struct ShadowSetInputs {
int cascadeHandle = -1;
vk::DeviceSize cascadeOffset = 0;
vk::DeviceSize cascadeSize = 0;
bool cascadeValid = false;
int shadowDataHandle = -1;
size_t shadowDataSize = 0;
vk::Buffer transformBuffer = nullptr;
size_t transformOffset = 0;
size_t transformSize = 0;
bool operator==(const ShadowSetInputs& o) const
{
return cascadeHandle == o.cascadeHandle && cascadeOffset == o.cascadeOffset &&
cascadeSize == o.cascadeSize && cascadeValid == o.cascadeValid &&
shadowDataHandle == o.shadowDataHandle && shadowDataSize == o.shadowDataSize &&
transformBuffer == o.transformBuffer && transformOffset == o.transformOffset &&
transformSize == o.transformSize;
}
bool operator!=(const ShadowSetInputs& o) const
{
return !(*this == o);
}
};
vk::DescriptorSet m_cachedShadowGlobalSet = nullptr;
vk::DescriptorSet m_cachedShadowMaterialSet = nullptr;
vk::DescriptorSet m_cachedShadowPerDrawSet = nullptr;
ShadowSetInputs m_cachedShadowInputs;
bool m_cachedShadowValid = false;
// Texture overrides for material bindings 4-6.
vk::DescriptorImageInfo m_depthTextureInfo; // binding 4: depth/position for soft particles
vk::DescriptorImageInfo m_sceneColorInfo; // binding 5: scene color for distortion
+5 -5
View File
@@ -755,12 +755,12 @@ void vulkan_calculate_irrmap()
// 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 (envmap cubemap at element 0 of texture array)
vk::DescriptorSet materialSet = descManager->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texImages;
// irrmap-f.sdr declares this binding as samplerCube, so every element needs a cube view
@@ -772,14 +772,14 @@ void vulkan_calculate_irrmap()
// Set 2: PerDraw (face UBO at binding 0)
vk::DescriptorSet perDrawSet = descManager->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
writer.setBuffer(PerDrawBinding::GenericData, {faceUBO,
static_cast<vk::DeviceSize>(face) * UBO_SLOT_SIZE, UBO_SLOT_SIZE});
writer.flush();
// Bind all descriptor sets
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
0, {globalSet, materialSet, perDrawSet}, {});
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Global, sets);
// Draw fullscreen triangle
cmd.draw(3, 1, 0, 0);
+10 -2
View File
@@ -140,7 +140,8 @@ VmaMemoryUsage VulkanMemoryManager::toVmaUsage(MemoryUsage usage)
}
}
bool VulkanMemoryManager::allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation)
bool VulkanMemoryManager::allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation,
MemoryPurpose purpose)
{
if (!m_initialized) {
nprintf(("vulkan", "VulkanMemoryManager::allocateBufferMemory called before initialization!\n"));
@@ -176,14 +177,17 @@ bool VulkanMemoryManager::allocateBufferMemory(vk::Buffer buffer, MemoryUsage us
allocation.size = allocInfo.size;
allocation.mappedPtr = allocInfo.pMappedData;
allocation.purpose = purpose;
++m_allocationCount;
m_totalAllocatedBytes += static_cast<size_t>(allocation.size);
m_bytesByPurpose[static_cast<size_t>(purpose)] += static_cast<size_t>(allocation.size);
return true;
}
bool VulkanMemoryManager::allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation)
bool VulkanMemoryManager::allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation,
MemoryPurpose purpose)
{
if (!m_initialized) {
nprintf(("vulkan", "VulkanMemoryManager::allocateImageMemory called before initialization!\n"));
@@ -219,9 +223,11 @@ bool VulkanMemoryManager::allocateImageMemory(vk::Image image, MemoryUsage usage
allocation.size = allocInfo.size;
allocation.mappedPtr = allocInfo.pMappedData;
allocation.purpose = purpose;
++m_allocationCount;
m_totalAllocatedBytes += static_cast<size_t>(allocation.size);
m_bytesByPurpose[static_cast<size_t>(purpose)] += static_cast<size_t>(allocation.size);
return true;
}
@@ -242,10 +248,12 @@ void VulkanMemoryManager::freeAllocation(VulkanAllocation& allocation)
--m_allocationCount;
m_totalAllocatedBytes -= static_cast<size_t>(allocation.size);
m_bytesByPurpose[static_cast<size_t>(allocation.purpose)] -= static_cast<size_t>(allocation.size);
allocation.vmaAlloc = VK_NULL_HANDLE;
allocation.size = 0;
allocation.mappedPtr = nullptr;
allocation.purpose = MemoryPurpose::Unknown;
}
void* VulkanMemoryManager::mapMemory(VulkanAllocation& allocation)
+27 -2
View File
@@ -30,6 +30,19 @@ class VulkanRenderer;
// above in lockstep, or VMA's internal assert will fire.
constexpr uint32_t VulkanApiVersion = VK_API_VERSION_1_2;
/**
* @brief What an allocation is used for, for the profiler overlay's per-purpose GPU memory
* breakdown. Unknown covers anything not worth categorizing for that panel (transient staging
* buffers, uniform buffers -- the latter are already tracked separately via
* UniformBufferManager/gr_debug_stats).
*/
enum class MemoryPurpose {
Unknown,
Texture,
Geometry,
RenderTarget
};
/**
* @brief Memory allocation info returned when allocating GPU memory.
*
@@ -40,6 +53,7 @@ struct VulkanAllocation {
VmaAllocation vmaAlloc = VK_NULL_HANDLE;
vk::DeviceSize size = 0;
void* mappedPtr = nullptr; // Non-null if memory is persistently mapped
MemoryPurpose purpose = MemoryPurpose::Unknown;
bool isValid() const { return vmaAlloc != VK_NULL_HANDLE; }
};
@@ -94,18 +108,24 @@ public:
* @param buffer The buffer to allocate memory for
* @param usage The intended memory usage pattern
* @param[out] allocation Output allocation info
* @param purpose What the buffer is used for, for the per-purpose memory breakdown. Defaults
* to Unknown for callers that don't care about that breakdown (staging buffers, etc).
* @return true on success
*/
bool allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation);
bool allocateBufferMemory(vk::Buffer buffer, MemoryUsage usage, VulkanAllocation& allocation,
MemoryPurpose purpose = MemoryPurpose::Unknown);
/**
* @brief Allocate memory for an image
* @param image The image to allocate memory for
* @param usage The intended memory usage pattern
* @param[out] allocation Output allocation info
* @param purpose What the image is used for, for the per-purpose memory breakdown. Defaults
* to Unknown for callers that don't care about that breakdown.
* @return true on success
*/
bool allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation);
bool allocateImageMemory(vk::Image image, MemoryUsage usage, VulkanAllocation& allocation,
MemoryPurpose purpose = MemoryPurpose::Unknown);
/**
* @brief Free a previous allocation
@@ -148,6 +168,10 @@ public:
size_t getAllocationCount() const { return m_allocationCount; }
size_t getTotalAllocatedBytes() const { return m_totalAllocatedBytes; }
size_t getTextureBytes() const { return m_bytesByPurpose[static_cast<size_t>(MemoryPurpose::Texture)]; }
size_t getGeometryBytes() const { return m_bytesByPurpose[static_cast<size_t>(MemoryPurpose::Geometry)]; }
size_t getRenderTargetBytes() const { return m_bytesByPurpose[static_cast<size_t>(MemoryPurpose::RenderTarget)]; }
private:
static VmaMemoryUsage toVmaUsage(MemoryUsage usage);
@@ -155,6 +179,7 @@ private:
size_t m_allocationCount = 0;
size_t m_totalAllocatedBytes = 0;
size_t m_bytesByPurpose[4] = {}; // indexed by MemoryPurpose
bool m_initialized = false;
};
+18 -1
View File
@@ -394,9 +394,26 @@ vk::UniquePipeline VulkanPipelineManager::createPipeline(const PipelineConfig& c
}
// Input assembly
//
// Primitive restart is enabled for every topology that permits it, rather than disabled
// outright. Metal always performs primitive restart on indexed draws and cannot turn it off, so
// MoltenVK rejects a strip/fan pipeline that asks to disable it -- vkCreateGraphicsPipelines
// fails with VK_ERROR_FEATURE_NOT_PRESENT ("Metal does not support disabling primitive
// restart"). getPipeline() then returns a null pipeline and the draw is skipped, which on macOS
// silently dropped the entire 2D/UI/HUD path (gr_bitmap and friends render quads as
// PRIM_TYPE_TRISTRIP; see graphics/render.cpp) while leaving ImGui -- which builds its own
// TRIANGLE_LIST pipeline -- as the only thing still on screen.
//
// Enabling it changes nothing for FSO's geometry: an index buffer is promoted to 32-bit as soon
// as a mesh reaches USHRT_MAX vertices (VB_FLAG_LARGE_INDEX, see modelinterp.cpp), so 0xFFFF is
// never a real 16-bit index, and 0xFFFFFFFF is unreachable for 32-bit ones. Neither restart
// sentinel can collide with live index data.
//
// List topologies must keep it disabled: the spec only allows primitiveRestartEnable on them
// with VK_EXT_primitive_topology_list_restart, which FSO does not request.
vk::PipelineInputAssemblyStateCreateInfo inputAssembly;
inputAssembly.topology = convertPrimitiveType(config.primitiveType);
inputAssembly.primitiveRestartEnable = VK_FALSE;
inputAssembly.primitiveRestartEnable = topologySupportsPrimitiveRestart(inputAssembly.topology);
// Viewport state (dynamic)
vk::PipelineViewportStateCreateInfo viewportState;
+12 -9
View File
@@ -687,7 +687,7 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
// Set 1: Material — source texture at array slot 0
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
@@ -701,7 +701,7 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
// Set 2: PerDraw — tonemapping UBO (from the per-frame scratch ring)
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
// LDR path: passthrough (tonemapping already ran). Fallback path: live
// parameters from the engine lighting profile.
@@ -732,8 +732,12 @@ void VulkanPostProcessor::blitToSwapChain(vk::CommandBuffer cmd)
{m_ctx.scratchRing.buffer(), slotOffset, m_ctx.scratchRing.slotSize()});
}
writer.flush();
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));
// Draw fullscreen triangle (3 vertices from gl_VertexIndex, no vertex buffer)
cmd.draw(3, 1, 0, 0);
@@ -800,7 +804,7 @@ void VulkanPostProcessor::encodeToSwapChainPass(vk::CommandBuffer cmd, vk::Rende
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
@@ -811,7 +815,7 @@ void VulkanPostProcessor::encodeToSwapChainPass(vk::CommandBuffer cmd, vk::Rende
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
{
vk::DeviceSize slotOffset =
m_ctx.scratchRing.alloc(descriptorMgr->getCurrentFrame(), uboData, uboSize);
@@ -820,9 +824,8 @@ void VulkanPostProcessor::encodeToSwapChainPass(vk::CommandBuffer cmd, vk::Rende
}
writer.flush();
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
static_cast<uint32_t>(DescriptorSetIndex::Material),
{materialSet, perDrawSet}, {});
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
cmd.draw(3, 1, 0, 0);
cmd.endRenderPass();
@@ -170,7 +170,8 @@ bool VulkanBloom::createTargets()
return false;
}
if (!m_ctx->memoryManager->allocateImageMemory(m_tex[i].image, MemoryUsage::GpuOnly, m_tex[i].allocation)) {
if (!m_ctx->memoryManager->allocateImageMemory(
m_tex[i].image, MemoryUsage::GpuOnly, m_tex[i].allocation, MemoryPurpose::RenderTarget)) {
nprintf(("vulkan", "VulkanBloom: Failed to allocate bloom image %zu memory!\n", i));
return false;
}
@@ -176,13 +176,13 @@ void PostProcessContext::drawFullscreenTriangle(vk::CommandBuffer cmd, vk::Rende
if (bindGlobalSet) {
globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global);
Assert(globalSet);
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
writer.writeSet(DescriptorSetIndex::Global, globalSet);
}
// Set 1: Material
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
@@ -194,7 +194,7 @@ void PostProcessContext::drawFullscreenTriangle(vk::CommandBuffer cmd, vk::Rende
// Set 2: PerDraw
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
if (uboData != nullptr && uboSize > 0 && scratchRing.isValid()) {
vk::DeviceSize slotOffset = scratchRing.alloc(descriptorMgr->getCurrentFrame(), uboData, uboSize);
writer.setBuffer(PerDrawBinding::GenericData, {scratchRing.buffer(), slotOffset, scratchRing.slotSize()});
@@ -205,13 +205,11 @@ void PostProcessContext::drawFullscreenTriangle(vk::CommandBuffer cmd, vk::Rende
// contiguous (0,1,2), so when Global is (re)bound here it and Material/PerDraw
// go in one call; otherwise Set 0 is left as whatever frame setup bound.
if (bindGlobalSet) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
static_cast<uint32_t>(DescriptorSetIndex::Global),
{globalSet, materialSet, perDrawSet}, {});
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Global, sets);
} else {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
static_cast<uint32_t>(DescriptorSetIndex::Material),
{materialSet, perDrawSet}, {});
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
}
cmd.draw(3, 1, 0, 0);
@@ -281,7 +279,7 @@ void PostProcessContext::drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
@@ -293,16 +291,15 @@ void PostProcessContext::drawFullscreenTriangleMulti(vk::CommandBuffer cmd, vk::
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
if (uboData != nullptr && uboSize > 0 && scratchRing.isValid()) {
vk::DeviceSize slotOffset = scratchRing.alloc(descriptorMgr->getCurrentFrame(), uboData, uboSize);
writer.setBuffer(PerDrawBinding::GenericData, {scratchRing.buffer(), slotOffset, scratchRing.slotSize()});
}
writer.flush();
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
static_cast<uint32_t>(DescriptorSetIndex::Material),
{materialSet, perDrawSet}, {});
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
cmd.draw(3, 1, 0, 0);
cmd.endRenderPass();
@@ -337,7 +334,7 @@ bool PostProcessContext::createImage(uint32_t width, uint32_t height, vk::Format
}
// Allocate memory
if (!memoryManager->allocateImageMemory(outImage, MemoryUsage::GpuOnly, outAllocation)) {
if (!memoryManager->allocateImageMemory(outImage, MemoryUsage::GpuOnly, outAllocation, MemoryPurpose::RenderTarget)) {
nprintf(("vulkan", "VulkanPostProcessor: Failed to allocate image memory!\n"));
device.destroyImage(outImage);
outImage = nullptr;
@@ -304,7 +304,7 @@ void VulkanFog::renderScene(vk::CommandBuffer cmd)
// Set 1: Material
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
@@ -316,7 +316,7 @@ void VulkanFog::renderScene(vk::CommandBuffer cmd)
// Set 2: PerDraw — fog UBO (from the per-frame scratch ring)
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
{
vk::DeviceSize slotOffset =
m_ctx->scratchRing.alloc(descriptorMgr->getCurrentFrame(), &fogData, sizeof(fogData));
@@ -326,9 +326,8 @@ void VulkanFog::renderScene(vk::CommandBuffer cmd)
writer.flush();
// Bind descriptor sets and draw
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
static_cast<uint32_t>(DescriptorSetIndex::Material),
{materialSet, perDrawSet}, {});
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
cmd.draw(3, 1, 0, 0);
cmd.endRenderPass();
@@ -420,7 +419,8 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
return;
}
Verification(m_ctx->memoryManager->allocateImageMemory(m_emissiveMipmapped.image, MemoryUsage::GpuOnly, m_emissiveMipmapped.allocation),
Verification(m_ctx->memoryManager->allocateImageMemory(m_emissiveMipmapped.image, MemoryUsage::GpuOnly,
m_emissiveMipmapped.allocation, MemoryPurpose::RenderTarget),
"Failed to allocate memory for mipmapped emissive image");
// Create full-mip-chain view for LOD sampling
@@ -614,7 +614,7 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
// Set 1: Material
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
Assert(materialSet);
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
{
std::array<vk::DescriptorImageInfo, VulkanDescriptorManager::MAX_TEXTURE_BINDINGS> texArrayInfos;
texArrayInfos.fill(descriptorMgr->getFallbacks().texture2D);
@@ -638,7 +638,7 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
// Set 2: PerDraw — volumetric fog UBO (from the per-frame scratch ring)
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
Assert(perDrawSet);
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
{
vk::DeviceSize slotOffset =
m_ctx->scratchRing.alloc(descriptorMgr->getCurrentFrame(), &volData, sizeof(volData));
@@ -648,9 +648,8 @@ void VulkanFog::renderVolumetric(vk::CommandBuffer cmd)
writer.flush();
// Bind descriptor sets and draw
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout,
static_cast<uint32_t>(DescriptorSetIndex::Material),
{materialSet, perDrawSet}, {});
const vk::DescriptorSet sets[] = {materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Material, sets);
cmd.draw(3, 1, 0, 0);
cmd.endRenderPass();
@@ -70,7 +70,8 @@ bool VulkanDeferredLighting::initLightVolumes()
return false;
}
if (!m_ctx->memoryManager->allocateBufferMemory(m_sphereMesh.vbo, MemoryUsage::CpuToGpu, m_sphereMesh.vboAlloc)) {
if (!m_ctx->memoryManager->allocateBufferMemory(
m_sphereMesh.vbo, MemoryUsage::CpuToGpu, m_sphereMesh.vboAlloc, MemoryPurpose::Geometry)) {
m_ctx->device.destroyBuffer(m_sphereMesh.vbo);
m_sphereMesh.vbo = nullptr;
return false;
@@ -95,7 +96,8 @@ bool VulkanDeferredLighting::initLightVolumes()
return false;
}
if (!m_ctx->memoryManager->allocateBufferMemory(m_sphereMesh.ibo, MemoryUsage::CpuToGpu, m_sphereMesh.iboAlloc)) {
if (!m_ctx->memoryManager->allocateBufferMemory(
m_sphereMesh.ibo, MemoryUsage::CpuToGpu, m_sphereMesh.iboAlloc, MemoryPurpose::Geometry)) {
m_ctx->device.destroyBuffer(m_sphereMesh.ibo);
m_sphereMesh.ibo = nullptr;
return false;
@@ -126,7 +128,8 @@ bool VulkanDeferredLighting::initLightVolumes()
return false;
}
if (!m_ctx->memoryManager->allocateBufferMemory(m_cylinderMesh.vbo, MemoryUsage::CpuToGpu, m_cylinderMesh.vboAlloc)) {
if (!m_ctx->memoryManager->allocateBufferMemory(
m_cylinderMesh.vbo, MemoryUsage::CpuToGpu, m_cylinderMesh.vboAlloc, MemoryPurpose::Geometry)) {
m_ctx->device.destroyBuffer(m_cylinderMesh.vbo);
m_cylinderMesh.vbo = nullptr;
return false;
@@ -150,7 +153,8 @@ bool VulkanDeferredLighting::initLightVolumes()
return false;
}
if (!m_ctx->memoryManager->allocateBufferMemory(m_cylinderMesh.ibo, MemoryUsage::CpuToGpu, m_cylinderMesh.iboAlloc)) {
if (!m_ctx->memoryManager->allocateBufferMemory(
m_cylinderMesh.ibo, MemoryUsage::CpuToGpu, m_cylinderMesh.iboAlloc, MemoryPurpose::Geometry)) {
m_ctx->device.destroyBuffer(m_cylinderMesh.ibo);
m_cylinderMesh.ibo = nullptr;
return false;
@@ -788,7 +792,7 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
// Set 0: Global
vk::DescriptorSet globalSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Global);
if (!globalSet) return false;
writer.writeSet(globalSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Global));
writer.writeSet(DescriptorSetIndex::Global, globalSet);
writer.setBuffer(GlobalBinding::Lights, {m_deferredUBO,
lightDataOffset + (li * lightDataSize), sizeof(graphics::deferred_light_data)});
writer.setBuffer(GlobalBinding::DeferredData, {m_deferredUBO,
@@ -801,19 +805,19 @@ void VulkanDeferredLighting::render(vk::CommandBuffer cmd)
// Set 1: Material
vk::DescriptorSet materialSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::Material);
if (!materialSet) return false;
writer.writeSet(materialSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::Material));
writer.writeSet(DescriptorSetIndex::Material, materialSet);
writer.setImageArray(MaterialBinding::TextureArray, gbufTexArray);
// Set 2: PerDraw
vk::DescriptorSet perDrawSet = descriptorMgr->allocateFrameSet(DescriptorSetIndex::PerDraw);
if (!perDrawSet) return false;
writer.writeSet(perDrawSet, VulkanDescriptorManager::getSetTemplate(DescriptorSetIndex::PerDraw));
writer.writeSet(DescriptorSetIndex::PerDraw, perDrawSet);
writer.setBuffer(PerDrawBinding::Matrices, {m_deferredUBO,
matrixDataOffset + (li * matrixDataSize), sizeof(graphics::matrix_uniforms)});
writer.flush();
std::array<vk::DescriptorSet, 3> sets = { globalSet, materialSet, perDrawSet };
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipelineLayout, 0, sets, {});
const vk::DescriptorSet sets[] = {globalSet, materialSet, perDrawSet};
writer.bindSets(cmd, pipelineLayout, DescriptorSetIndex::Global, sets);
return true;
};
@@ -206,7 +206,8 @@ bool VulkanDeferredGBuffer::createMsaaTargets()
return false;
}
if (!m_ctx->memoryManager->allocateImageMemory(m_msaaDepthImage, MemoryUsage::GpuOnly, m_msaaDepthAlloc)) {
if (!m_ctx->memoryManager->allocateImageMemory(
m_msaaDepthImage, MemoryUsage::GpuOnly, m_msaaDepthAlloc, MemoryPurpose::RenderTarget)) {
nprintf(("vulkan", "VulkanPostProcessor: Failed to allocate MSAA depth memory!\n"));
m_ctx->device.destroyImage(m_msaaDepthImage);
m_msaaDepthImage = nullptr;
@@ -70,7 +70,8 @@ bool VulkanShadowMap::init(PostProcessContext& ctx)
return false;
}
if (!m_ctx->memoryManager->allocateImageMemory(m_depth.image, MemoryUsage::GpuOnly, m_depth.allocation)) {
if (!m_ctx->memoryManager->allocateImageMemory(
m_depth.image, MemoryUsage::GpuOnly, m_depth.allocation, MemoryPurpose::RenderTarget)) {
m_ctx->device.destroyImage(m_depth.image);
m_depth.image = nullptr;
return false;
+3 -2
View File
@@ -63,7 +63,7 @@ void VulkanRenderer::createCompositionResources()
auto image = m_device->createImageUnique(imageInfo);
VulkanAllocation alloc{};
m_memoryManager->allocateImageMemory(image.get(), MemoryUsage::GpuOnly, alloc);
m_memoryManager->allocateImageMemory(image.get(), MemoryUsage::GpuOnly, alloc, MemoryPurpose::RenderTarget);
vk::ImageViewCreateInfo viewInfo;
viewInfo.image = image.get();
@@ -279,7 +279,8 @@ void VulkanRenderer::createDepthResources()
m_depthImage = m_device->createImageUnique(imageInfo);
// Allocate GPU memory for the depth image
m_memoryManager->allocateImageMemory(m_depthImage.get(), MemoryUsage::GpuOnly, m_depthImageMemory);
m_memoryManager->allocateImageMemory(
m_depthImage.get(), MemoryUsage::GpuOnly, m_depthImageMemory, MemoryPurpose::RenderTarget);
// Create depth image view
vk::ImageViewCreateInfo viewInfo;
@@ -947,6 +947,15 @@ bool VulkanRenderer::createLogicalDevice(const PhysicalDeviceValues& deviceValue
enabledExtensions.push_back(VK_EXT_HDR_METADATA_EXTENSION_NAME);
mprintf(("Vulkan: Enabling %s (HDR10 metadata)\n", VK_EXT_HDR_METADATA_EXTENSION_NAME));
}
// Required by the Vulkan Portability subset whenever the physical device
// advertises it (MoltenVK / macOS). Creating a device without it is
// undefined behavior and trips VUID-VkDeviceCreateInfo-pProperties-04451.
// The extension-name macro lives in vulkan_beta.h (VK_ENABLE_BETA_EXTENSIONS);
// our headers do not enable that, so use the literal string instead.
if (strcmp(ext.extensionName, "VK_KHR_portability_subset") == 0) {
enabledExtensions.push_back("VK_KHR_portability_subset");
mprintf(("Vulkan: Enabling VK_KHR_portability_subset\n"));
}
if (strcmp(ext.extensionName, VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME) == 0) {
hasAccelerationStructureExt = true;
}
+30 -8
View File
@@ -251,25 +251,47 @@ void VulkanStateTracker::bindPipeline(vk::Pipeline pipeline, vk::PipelineLayout
}
}
void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex, vk::DescriptorSet set,
const SCP_vector<uint32_t>& dynamicOffsets)
void VulkanStateTracker::bindDescriptorSet(DescriptorSetIndex setIndex,
vk::DescriptorSet set,
const uint32_t* dynamicOffsets)
{
Assertion(m_cmdBuffer, "bindDescriptorSet called without active command buffer!");
Assertion(m_currentPipelineLayout, "bindDescriptorSet called without bound pipeline layout!");
Assertion(set, "bindDescriptorSet called with null descriptor set!");
auto index = static_cast<uint32_t>(setIndex);
const uint32_t dynCount = VulkanDescriptorManager::getDynamicOffsetCount(setIndex);
Assertion(dynCount == 0 || dynamicOffsets != nullptr,
"bindDescriptorSet: set %u declares %u dynamic descriptors but no offsets were supplied!",
index,
dynCount);
if (m_boundDescriptorSets[index] != set) {
m_cmdBuffer.bindDescriptorSets(
vk::PipelineBindPoint::eGraphics,
// The redundancy check has to cover the dynamic offsets too, not just the set
// handle: with dynamic UBOs the common case is the *same* set rebound with a new
// offset every draw, and skipping that bind would silently feed every draw the
// previous draw's uniforms.
auto& boundDyn = m_boundDynamicOffsets[index];
bool offsetsChanged = false;
for (uint32_t i = 0; i < dynCount; ++i) {
if (boundDyn[i] != dynamicOffsets[i]) {
offsetsChanged = true;
break;
}
}
if (m_boundDescriptorSets[index] != set || offsetsChanged) {
m_cmdBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_currentPipelineLayout,
index,
1, &set,
static_cast<uint32_t>(dynamicOffsets.size()),
dynamicOffsets.empty() ? nullptr : dynamicOffsets.data());
1,
&set,
dynCount,
dynCount > 0 ? dynamicOffsets : nullptr);
m_boundDescriptorSets[index] = set;
for (uint32_t i = 0; i < dynCount; ++i) {
boundDyn[i] = dynamicOffsets[i];
}
}
}
+13 -3
View File
@@ -134,10 +134,15 @@ class VulkanStateTracker {
/**
* @brief Bind descriptor set
*
* @param dynamicOffsets Offsets for the set layout's eUniformBufferDynamic
* bindings, ordered by binding number. Must point to at least
* VulkanDescriptorManager::getDynamicOffsetCount(setIndex) entries
* (DescriptorWriter::dynamicOffsets() produces exactly that); may be
* null only for a set that declares none.
*/
void bindDescriptorSet(DescriptorSetIndex setIndex,
vk::DescriptorSet set,
const SCP_vector<uint32_t>& dynamicOffsets = {});
void
bindDescriptorSet(DescriptorSetIndex setIndex, vk::DescriptorSet set, const uint32_t* dynamicOffsets);
// ========== Buffer Binding ==========
@@ -312,6 +317,11 @@ class VulkanStateTracker {
// Descriptor sets
std::array<vk::DescriptorSet, static_cast<size_t>(DescriptorSetIndex::Count)> m_boundDescriptorSets;
// Dynamic offsets the currently-bound set was bound with, so an offset-only
// change still forces a rebind (see bindDescriptorSet).
std::array<std::array<uint32_t, DescriptorWriter::MAX_DYNAMIC_OFFSETS_PER_SET>,
static_cast<size_t>(DescriptorSetIndex::Count)>
m_boundDynamicOffsets{};
// Dynamic state
vk::Viewport m_viewport;
+1 -1
View File
@@ -2528,7 +2528,7 @@ bool VulkanTextureManager::createImage(uint32_t width, uint32_t height, uint32_t
return false;
}
if (!m_memoryManager->allocateImageMemory(image, memUsage, allocation)) {
if (!m_memoryManager->allocateImageMemory(image, memUsage, allocation, MemoryPurpose::Texture)) {
m_device.destroyImage(image);
image = nullptr;
return false;
+43 -5
View File
@@ -185,6 +185,38 @@ bool vulkan_get_property(gr_property prop, void* dest)
}
}
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();
@@ -222,12 +254,16 @@ void vulkan_imgui_render_draw_data()
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.
// 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();
}
}
@@ -532,6 +568,8 @@ void init_function_pointers()
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;
+1 -2
View File
@@ -412,12 +412,11 @@ void LabManager::onFrame(float frametime) {
if (Cmdline_show_imgui_debug)
ImGui::ShowDemoWindow();
ImGui::Render();
gr_imgui_render_draw_data();
if (CloseThis)
close();
// gr_flip() renders and submits the ImGui frame opened above.
gr_flip();
}
+3 -2
View File
@@ -256,8 +256,9 @@ void LabRenderer::renderModel(float frametime) {
gr_reset_clip();
gr_set_color_fast(&HUD_color_debug);
if (Cmdline_frame_profile) {
tracing::frame_profile_process_frame();
// The legacy -profile_frame_time text dump. gr_flip() drains the profiler, so this only
// renders what the previous frame produced.
if (Cmdline_frame_profile && tracing::frame_profiling_active()) {
gr_string(gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 100 + gr_get_font_height() + 1,
tracing::get_frame_profile_output().c_str(), GR_RESIZE_NONE);
}
+1 -1
View File
@@ -65,7 +65,7 @@ bool *Lcl_unexpected_tstring_check = nullptr;
// NOTE: with map storage of XSTR strings, the indexes no longer need to be contiguous,
// but internal strings should still increment XSTR_SIZE to avoid collisions.
// retail XSTR_SIZE = 1570
// #define XSTR_SIZE 1940 // This is the next available ID
// #define XSTR_SIZE 1942 // This is the next available ID
// struct to allow for strings.tbl-determined x offset
// offset is 0 for english, by default
+1 -1
View File
@@ -26,7 +26,7 @@
#include "popup/popup.h"
#include "sound/audiostr.h"
#include "ui/ui.h"
#include "weapon/weapon.h"
#include "weapon/weapon.h"
+13
View File
@@ -1056,6 +1056,19 @@ void model_load_texture(polymodel *pm, int i, const char *file);
// Returns a pointer to the polymodel structure for model 'n'
polymodel *model_get(int model_num);
/**
* @brief Memory usage summary across all currently loaded polygon models, for the profiler
* overlay's memory panel
*/
struct model_memory_stats {
bool valid = false;
int model_count = 0;
size_t vertex_bytes = 0; // sum of vert_source.Vertex_list_size across loaded models
size_t index_bytes = 0; // sum of vert_source.Index_list_size across loaded models
size_t bsp_data_bytes = 0; // sum of submodel[].bsp_data_size (collision/interp tree data)
};
model_memory_stats model_get_memory_stats();
int num_model_instances();
polymodel_instance* model_get_instance(int model_instance_num);
+22
View File
@@ -411,6 +411,28 @@ void model_page_in_stop()
}
}
model_memory_stats model_get_memory_stats()
{
model_memory_stats stats;
stats.valid = true;
for (const auto pm : Polygon_models) {
if (pm == nullptr) {
continue;
}
stats.model_count++;
stats.vertex_bytes += pm->vert_source.Vertex_list_size;
stats.index_bytes += pm->vert_source.Index_list_size;
for (int j = 0; j < pm->n_models; j++) {
stats.bsp_data_bytes += pm->submodel[j].bsp_data_size;
}
}
return stats;
}
void model_init()
{
int i;
+13
View File
@@ -535,6 +535,19 @@ int obj_allocate(bool essential)
return objnum;
}
object_memory_stats obj_get_memory_stats()
{
object_memory_stats stats;
stats.objects_used = Num_objects;
stats.objects_peak = num_objects_hwm;
stats.ships_used = ship_get_num_ships();
stats.weapons_used = Num_weapons;
return stats;
}
/**
* Frees up an object
*
+18
View File
@@ -432,5 +432,23 @@ void physics_apply_pstate_to_object(object* objp, const physics_snapshot& source
*/
int obj_raw_pof_create(const char* pof_filename, const matrix* orient, const vec3d* pos);
/**
* @brief Occupancy snapshot of the object/ship/weapon pools, for the profiler overlay's memory panel
*
* @details These pools are fixed-size static arrays, so their footprint is a compile-time
* constant; what's actually useful to report is how full they are.
*/
struct object_memory_stats {
int objects_used = 0;
int objects_max = MAX_OBJECTS;
int objects_peak = 0;
int ships_used = 0;
int ships_max = MAX_SHIPS;
int weapons_used = 0;
int weapons_max = MAX_WEAPONS;
};
object_memory_stats obj_get_memory_stats();
#endif
@@ -176,12 +176,11 @@ void OptConfigurator::onFrame() {
if (Cmdline_show_imgui_debug)
ImGui::ShowDemoWindow();
ImGui::Render();
gr_imgui_render_draw_data();
if (CloseThis) {
close();
}
// gr_flip() renders and submits the ImGui frame opened above.
gr_flip();
}
+48 -2
View File
@@ -15,6 +15,7 @@
#include "graphics/2d.h"
#include "graphics/openxr.h"
#include "io/joy_ff.h"
#include "tracing/tracing.h"
#include <fcntl.h>
#include <utf8.h>
@@ -38,6 +39,12 @@
#include "utils/finally.h"
#endif
#if defined(__APPLE__)
#include <mach/mach.h>
#elif defined(__linux__)
#include <unistd.h>
#endif
namespace
{
const char* ORGANIZATION_NAME = "HardLightProductions";
@@ -254,6 +261,7 @@ namespace
// Windows specific includes
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <psapi.h>
#include <backends/imgui_impl_sdl3.h>
// go through all windows and try and find the one that matches the search string
@@ -833,18 +841,23 @@ static void handle_sdl_event(const SDL_Event& event) {
using namespace os::events;
bool imgui_processed_this = false;
if ((gameseq_get_state() == GS_STATE_LAB) || (gameseq_get_state() == GS_STATE_INGAME_OPTIONS)) {
// The profiler overlay is drawn from gr_flip(), so it can be on screen in any state — it
// needs input forwarded wherever it is active, not in a fixed list of states.
const bool imgui_state = (gameseq_get_state() == GS_STATE_LAB) || (gameseq_get_state() == GS_STATE_INGAME_OPTIONS);
if (imgui_state || tracing::frame_profiling_active()) {
//In these states, we always need to forward inputs to ImGUI, and depending on the ImGUI state and the input type, we must consume it here instead of passing it to FSO.
const SDL_Event imgui_event = scale_imgui_mouse_event(event);
ImGui_ImplSDL3_ProcessEvent(&imgui_event);
// In other states, the game also needs mouse motion while the pointer is over the overlay.
// If the overlay consumed the motion, mouse flight would stop there.
imgui_processed_this = (ImGui::GetIO().WantCaptureKeyboard &&
(event.type == SDL_EventType::SDL_EVENT_KEY_UP ||
event.type == SDL_EventType::SDL_EVENT_KEY_DOWN)) ||
(ImGui::GetIO().WantCaptureMouse &&
(event.type == SDL_EventType::SDL_EVENT_MOUSE_BUTTON_UP ||
event.type == SDL_EventType::SDL_EVENT_MOUSE_BUTTON_DOWN||
event.type == SDL_EventType::SDL_EVENT_MOUSE_MOTION));
(imgui_state && event.type == SDL_EventType::SDL_EVENT_MOUSE_MOTION)));
}
if (!imgui_processed_this) {
@@ -1105,3 +1118,36 @@ void os_android_touch_overlay_init()
}
#endif
os_memory_stats os_get_memory_stats()
{
os_memory_stats stats;
#if defined(WIN32)
PROCESS_MEMORY_COUNTERS counters;
if (GetProcessMemoryInfo(GetCurrentProcess(), &counters, sizeof(counters))) {
stats.valid = true;
stats.resident_bytes = counters.WorkingSetSize;
}
#elif defined(__APPLE__)
mach_task_basic_info_data_t info;
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&info), &count) ==
KERN_SUCCESS) {
stats.valid = true;
stats.resident_bytes = info.resident_size;
}
#elif defined(__linux__)
// Field 2 of /proc/self/statm is the resident set size, in pages -- see proc(5).
if (FILE* statm = fopen("/proc/self/statm", "r")) {
long resident_pages = 0;
if (fscanf(statm, "%*d %ld", &resident_pages) == 1) {
stats.valid = true;
stats.resident_bytes = static_cast<size_t>(resident_pages) * static_cast<size_t>(sysconf(_SC_PAGESIZE));
}
fclose(statm);
}
#endif
return stats;
}
+14
View File
@@ -107,6 +107,20 @@ void os_android_touch_overlay_toggle(bool status);
void os_android_set_flags_string(const char* json);
#endif
/**
* @brief Process-wide memory usage snapshot, for the profiler overlay's memory panel
*
* @details Reports the resident set size (RSS) of the FSO process: the physical RAM the operating
* system currently has mapped for this process, across every subsystem (engine heap, Lua heap,
* OpenAL buffers, driver-side graphics allocations). Not available on every platform, hence the
* "valid" flag -- see os_get_memory_stats().
*/
struct os_memory_stats {
bool valid = false;
size_t resident_bytes = 0;
};
os_memory_stats os_get_memory_stats();
namespace os
{
/**
+15
View File
@@ -1067,6 +1067,21 @@ void scripting_state_close()
scripting_state_inited = 0;
}
scripting_memory_stats scripting_get_memory_stats()
{
scripting_memory_stats stats;
lua_State* L = Script_system.GetLuaSession();
if (L == nullptr) {
return stats;
}
stats.valid = true;
stats.lua_heap_bytes = static_cast<size_t>(lua_gc(L, LUA_GCCOUNT, 0)) * 1024 +
static_cast<size_t>(lua_gc(L, LUA_GCCOUNTB, 0));
return stats;
}
void scripting_state_do_frame(float /*frametime*/, bool doKeys)
{
// just incase something is wrong
+12
View File
@@ -88,6 +88,18 @@ void scripting_state_init();
void scripting_state_close();
void scripting_state_do_frame(float frametime, bool doKeys);
/**
* @brief Lua heap usage snapshot, for the profiler overlay's memory panel
*
* @details Reports Lua's own GC byte count for the main script state (see lua_gc(),
* LUA_GCCOUNT/LUA_GCCOUNTB). Invalid before the Lua state is created or after it is torn down.
*/
struct scripting_memory_stats {
bool valid = false;
size_t lua_heap_bytes = 0;
};
scripting_memory_stats scripting_get_memory_stats();
const scripting::HookBase* scripting_string_to_action(const char* action);
ConditionalType scripting_string_to_condition(const char* condition);
+9 -5
View File
@@ -922,12 +922,16 @@ ship_obj *get_ship_obj_ptr_from_index(int index)
*/
int ship_get_num_ships()
{
int count;
ship_obj *so;
int count = 0;
count = 0;
for ( so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) )
{
// Ship_obj_list is only list_init()'d by ship_obj_list_init(), which runs on entering gameplay;
// this can be called before that (e.g. by the profiler overlay's memory stats from the main
// menu), in which case GET_FIRST() returns the list head's zero-initialized null next pointer.
if (GET_FIRST(&Ship_obj_list) == nullptr) {
return count;
}
for (auto so : list_range(&Ship_obj_list)) {
if (Objects[so->objnum].flags[Object::Object_Flags::Should_be_dead])
continue;
+8 -1
View File
@@ -48,7 +48,7 @@ struct loaded_sound {
SCP_vector<loaded_sound> Sounds;
int Sound_enabled = FALSE; // global flag to turn sound on/off
size_t Snd_sram; // mem (in bytes) used up by storing sounds in system memory
size_t Snd_sram; // bytes held in OpenAL sound-effect and music-streaming buffers
float Default_sound_volume = 1.0f; // range is 0 -> 1, used for non-music sound fx
float Master_sound_volume = Default_sound_volume;
@@ -1392,6 +1392,13 @@ int snd_num_playing()
return ds_get_number_channels();
}
sound_memory_stats snd_get_memory_stats()
{
sound_memory_stats stats;
stats.audio_buffer_bytes = Snd_sram;
return stats;
}
// Return the raw sound data for a loaded sound
//
// input: handle => index into Sounds[] array
+13 -1
View File
@@ -122,11 +122,23 @@ extern float Default_sound_volume; // 0 -> 1.0
extern float Default_voice_volume; // 0 -> 1.0
extern float Master_sound_volume; // 0 -> 1.0
extern float Master_voice_volume; // 0 -> 1.0
extern size_t Snd_sram; // System memory consumed by sound data
extern size_t Snd_sram; // Bytes held in OpenAL sound-effect and music-streaming buffers
extern float aav_voice_volume;
extern float aav_music_volume;
extern float aav_effect_volume;
/**
* @brief Audio buffer usage snapshot, for the profiler overlay's memory panel
*
* @details Reports Snd_sram: the bytes held in OpenAL buffers for loaded sound effects and the
* music-streaming ring buffers. This is buffer data handed to the OpenAL driver, not an FSO heap
* allocation, so it is a lower bound on the audio subsystem's real footprint.
*/
struct sound_memory_stats {
size_t audio_buffer_bytes = 0;
};
sound_memory_stats snd_get_memory_stats();
void snd_set_effects_volume(float volume);
void snd_set_voice_volume(float volume);
+2
View File
@@ -1791,6 +1791,8 @@ add_file_folder("Tracing"
tracing/MainFrameTimer.cpp
tracing/Monitor.h
tracing/Monitor.cpp
tracing/ProfilerOverlay.h
tracing/ProfilerOverlay.cpp
tracing/scopes.cpp
tracing/scopes.h
tracing/ThreadedEventProcessor.h
+104 -39
View File
@@ -3,6 +3,7 @@
#include "FrameProfiler.h"
#include "cmdline/cmdline.h"
#include "globalincs/systemvars.h"
using namespace tracing;
@@ -15,6 +16,9 @@ bool event_sorter(const trace_event& left, const trace_event& right) {
}
void process_begin(SCP_vector<profile_sample>& samples, const trace_event& evt) {
// Read the name through the id, not evt.category: the Category it names may already be dead
// (see trace_event::category_id).
const SCP_string category_name = Category::getNameById(evt.category_id);
int parent = -1;
for (int i = 0; i < (int) samples.size(); i++) {
if (!samples[i].open_profiles) {
@@ -30,7 +34,7 @@ void process_begin(SCP_vector<profile_sample>& samples, const trace_event& evt)
}
for (int i = 0; i < (int) samples.size(); i++) {
if (!strcmp(samples[i].name.c_str(), evt.category->getName()) && samples[i].parent == parent) {
if (samples[i].name == category_name && samples[i].parent == parent) {
// found the profile sample
samples[i].open_profiles++;
samples[i].profile_instances++;
@@ -43,7 +47,7 @@ void process_begin(SCP_vector<profile_sample>& samples, const trace_event& evt)
// create a new profile sample
profile_sample new_sample;
new_sample.name = SCP_string(evt.category->getName());
new_sample.name = category_name;
new_sample.open_profiles = 1;
new_sample.profile_instances = 1;
new_sample.accumulator = 0;
@@ -57,6 +61,9 @@ void process_begin(SCP_vector<profile_sample>& samples, const trace_event& evt)
}
void process_end(SCP_vector<profile_sample>& samples, const trace_event& evt) {
// Read the name through the id, not evt.category: the Category it names may already be dead
// (see trace_event::category_id).
const SCP_string category_name = Category::getNameById(evt.category_id);
uint num_parents = 0;
int child_of = -1;
@@ -69,7 +76,7 @@ void process_end(SCP_vector<profile_sample>& samples, const trace_event& evt) {
}
for (int i = 0; i < (int) samples.size(); i++) {
if (!strcmp(samples[i].name.c_str(), evt.category->getName()) && samples[i].parent == child_of) {
if (samples[i].name == category_name && samples[i].parent == child_of) {
int inner = 0;
int parent = -1;
uint64_t end_time = evt.timestamp;
@@ -120,7 +127,48 @@ void process_end(SCP_vector<profile_sample>& samples, const trace_event& evt) {
namespace tracing {
FrameProfiler::FrameProfiler() {
uint64_t accumulate_self_times(const SCP_vector<trace_event>& events, SCP_vector<uint64_t>& self_time_by_id) {
self_time_by_id.assign(Category::getCount(), 0);
if (events.empty()) {
return 0;
}
uint64_t total = 0;
SCP_vector<int> open_stack; // category ids of currently-open scopes
open_stack.reserve(32);
uint64_t last_ts = events.front().timestamp;
for (const auto& evt : events) {
if (!open_stack.empty()) {
const uint64_t delta = evt.timestamp - last_ts;
self_time_by_id[static_cast<size_t>(open_stack.back())] += delta;
total += delta;
}
last_ts = evt.timestamp;
if (evt.category_id < 0 || evt.category_id >= static_cast<int>(self_time_by_id.size())) {
// Can't happen today (processEvent filters out null-category events, and every
// recorded id is in range at record time), but the category this id names may have
// been destroyed since (see trace_event::category_id) -- never trust it blindly, and
// keep last_ts advanced above so a stray event could never cause the next delta to
// span it.
continue;
}
if (evt.type == EventType::Begin) {
open_stack.push_back(evt.category_id);
} else if (evt.type == EventType::End) {
if (!open_stack.empty()) {
open_stack.pop_back();
}
}
}
return total;
}
FrameProfiler::FrameProfiler(std::int64_t mainThreadID) : _mainThreadID(mainThreadID) {
}
FrameProfiler::~FrameProfiler() {
@@ -137,12 +185,6 @@ void FrameProfiler::processEvent(const trace_event* event) {
return;
}
if (_mainThreadID == -1) {
// We need the ID of the main thread to filter out multi threaded events. We just assume that the first event
// of the main process we see is from the main thread. That should be a safe assumption.
_mainThreadID = event->tid;
}
if (event->tid != _mainThreadID) {
// Multithreaded events don't have a deterministic sequence and that confuses the old profiling system
return;
@@ -279,47 +321,70 @@ void FrameProfiler::dump_output(SCP_stringstream& out,
SCP_string FrameProfiler::getContent() {
return content;
}
void FrameProfiler::build_overlay_snapshot(const SCP_vector<uint64_t>& self_time_by_id, uint64_t total) {
// Keep the id, not a Category pointer: a category that a script made through the Lua API
// tracing_category can be gone by now, but its name stays available through getNameById().
SCP_vector<std::pair<int, uint64_t>> sorted;
for (size_t id = 0; id < self_time_by_id.size(); id++) {
if (self_time_by_id[id] > 0) {
sorted.emplace_back(static_cast<int>(id), self_time_by_id[id]);
}
}
std::sort(sorted.begin(), sorted.end(),
[](const std::pair<int, uint64_t>& a, const std::pair<int, uint64_t>& b) {
return a.second > b.second;
});
overlaySnapshot.valid = true;
overlaySnapshot.total_nanosec = total;
overlaySnapshot.top_contributors.clear();
overlaySnapshot.other_nanosec = 0;
for (size_t i = 0; i < sorted.size(); i++) {
if (i < FRAME_OVERLAY_MAX_CONTRIBUTORS) {
overlaySnapshot.top_contributors.push_back({Category::getNameById(sorted[i].first), sorted[i].second});
} else {
overlaySnapshot.other_nanosec += sorted[i].second;
}
}
}
void FrameProfiler::processFrame() {
std::lock_guard<std::mutex> vectorGuard(_eventsMutex);
std::sort(_bufferedEvents.begin(), _bufferedEvents.end(), event_sorter);
SCP_stringstream stream;
// Overlay fast path: per-category self-time via a single stack walk (see accumulate_self_times).
// _selfTimeScratch is a member so the per-frame run reuses its allocation.
const uint64_t total = accumulate_self_times(_bufferedEvents, _selfTimeScratch);
build_overlay_snapshot(_selfTimeScratch, total);
SCP_vector<profile_sample> samples;
// Legacy on-screen text dump (-profile_frame_time). This still builds the full parent/child
// sample tree (process_begin/process_end) and the min/avg/max history, both O(n^2); only pay for
// it when that output is actually consumed, not for the overlay.
if (Cmdline_frame_profile) {
SCP_stringstream stream;
SCP_vector<profile_sample> samples;
bool start_found = false;
bool end_found = false;
uint64_t start_profile_time = 0;
uint64_t end_profile_time = 0;
for (auto& event : _bufferedEvents) {
if (!start_found) {
start_profile_time = event.timestamp;
start_found = true;
}
if (!end_found) {
end_profile_time = event.timestamp;
end_found = true;
for (auto& event : _bufferedEvents) {
switch (event.type) {
case EventType::Begin:
process_begin(samples, event);
break;
case EventType::End:
process_end(samples, event);
break;
default:
break;
}
}
switch (event.type) {
case EventType::Begin:
process_begin(samples, event);
break;
case EventType::End:
process_end(samples, event);
break;
default:
break;
}
dump_output(stream, 0, 0, samples);
content = stream.str();
}
_bufferedEvents.clear();
dump_output(stream, start_profile_time, end_profile_time, samples);
content = stream.str();
}
}
+26 -2
View File
@@ -13,6 +13,16 @@
namespace tracing {
/**
* Computes the self time of each category for one frame of Begin/End events, sorted by event_id.
*
* @c self_time_by_id is resized to Category::getCount() and indexed by Category::getId(). Pass the
* same vector every frame to reuse its allocation.
*
* @return the sum of all self times, which is the total traced frame time.
*/
uint64_t accumulate_self_times(const SCP_vector<trace_event>& events, SCP_vector<uint64_t>& self_time_by_id);
struct profile_sample_history {
bool valid;
//char name[256];
@@ -40,7 +50,13 @@ class FrameProfiler {
SCP_vector<profile_sample_history> history;
std::int64_t _mainThreadID = -1;
frame_overlay_snapshot overlaySnapshot;
// Reused across frames by accumulate_self_times() so the per-frame walk allocates nothing.
SCP_vector<uint64_t> _selfTimeScratch;
// The profiler keeps only events from this thread. Other threads have no deterministic event order.
std::int64_t _mainThreadID;
SCP_string content;
@@ -69,9 +85,15 @@ class FrameProfiler {
uint64_t end_profile_time,
SCP_vector<profile_sample>& samples);
/**
* Builds the structured overlay snapshot (see frame_overlay_snapshot) from this frame's
* per-category self-time. self_time_by_id is indexed by Category::getId(); total is the sum of
* all self-times (i.e. total traced frame time). Called once per processFrame().
*/
void build_overlay_snapshot(const SCP_vector<uint64_t>& self_time_by_id, uint64_t total);
public:
FrameProfiler();
explicit FrameProfiler(std::int64_t mainThreadID);
~FrameProfiler();
void processEvent(const trace_event* event);
@@ -79,6 +101,8 @@ class FrameProfiler {
void processFrame();
SCP_string getContent();
const frame_overlay_snapshot& getOverlaySnapshot() const { return overlaySnapshot; }
};
}
+454
View File
@@ -0,0 +1,454 @@
//
//
#include "ProfilerOverlay.h"
#include "tracing.h"
#include "graphics/2d.h"
#include "io/timer.h"
#include "model/model.h"
#include "object/object.h"
#include "osapi/osapi.h"
#include "parse/encrypt.h"
#include "scripting/scripting.h"
#include "sound/sound.h"
// ImPlot's Plot*() functions are templates that get instantiated here (unlike ImGui's ordinary
// function API), and their header-inline ImPool<ImPlotItem>::Add() uses memcpy on the
// non-trivially-copyable ImPlotItem -- which trips pstypes.h's memcpy-safety macro. That macro
// exists to catch accidental memcpy of non-POD engine types; ImPlotItem is third-party-internal
// and already safe, so suppress it for just these headers.
#pragma push_macro("memcpy")
#undef memcpy
#include "imgui.h"
#include "implot.h"
#pragma pop_macro("memcpy")
#include <algorithm>
#include <cstring>
#include <numeric>
namespace tracing {
namespace {
constexpr size_t HISTORY_SIZE = 300; // ~5s at 60 FPS
constexpr double NANOSEC_PER_MS = 1'000'000.0;
// std::queue does not permit iteration, but it keeps its container as a protected member. This
// subclass gives the overlay read access to that container.
class FrameTimeQueue : public SCP_queue<float> {
public:
const container_type& elements() const { return c; }
};
FrameTimeQueue History_ms;
using HistoryContainer = FrameTimeQueue::container_type;
// Wall-clock timestamp of the previous overlay frame, 0 when there is none yet. The frame time
// in the history is the time between two overlay frames. It is not the traced self-time, which
// leaves out every part of the frame that has no trace scope.
uint64_t Last_frame_ns = 0;
void push_history(float frame_ms) {
History_ms.push(frame_ms);
if (History_ms.size() > HISTORY_SIZE) {
History_ms.pop();
}
}
float history_average(const HistoryContainer& history) {
if (history.empty()) {
return 0.0f;
}
float sum = std::accumulate(history.begin(), history.end(), 0.0f);
return sum / static_cast<float>(history.size());
}
float history_median(const HistoryContainer& history) {
if (history.empty()) {
return 0.0f;
}
SCP_vector<float> sorted_copy(history.begin(), history.end());
size_t mid = sorted_copy.size() / 2;
std::nth_element(sorted_copy.begin(), sorted_copy.begin() + mid, sorted_copy.end());
float median = sorted_copy[mid];
if (sorted_copy.size() % 2 == 0) {
std::nth_element(sorted_copy.begin(), sorted_copy.begin() + mid - 1, sorted_copy.end());
median = (median + sorted_copy[mid - 1]) * 0.5f;
}
return median;
}
void draw_frametime_graph(const HistoryContainer& history) {
if (history.empty()) {
return;
}
if (ImPlot::BeginPlot("##frametime_ms",
ImVec2(-1, 90),
ImPlotFlags_NoMouseText | ImPlotFlags_NoLegend | ImPlotFlags_NoTitle)) {
ImPlot::SetupAxes(nullptr,
"ms",
ImPlotAxisFlags_NoTickLabels | ImPlotAxisFlags_NoGridLines,
ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, static_cast<double>(history.size()), ImPlotCond_Always);
// The getter reads the deque directly, so the graph needs no contiguous copy.
ImPlot::PlotLineG(
"frametime",
[](int idx, void* data) {
const auto& values = *static_cast<const HistoryContainer*>(data);
return ImPlotPoint(static_cast<double>(idx), static_cast<double>(values[static_cast<size_t>(idx)]));
},
const_cast<HistoryContainer*>(&history),
static_cast<int>(history.size()));
ImPlot::EndPlot();
}
}
/**
* Maps a category name to a stable color, so a given category keeps the same color across frames
* regardless of how its rank shifts in the breakdown. Uses an FNV-1a hash of the name to pick a
* hue; "Other" gets a fixed neutral grey (see draw_frame_budget_bar).
*/
ImU32 color_for_name(const char* name) {
const uint32_t hash = hash_fnv1a(name, strlen(name));
const float hue = static_cast<float>(hash % 360) / 360.0f;
return ImGui::ColorConvertFloat4ToU32(static_cast<ImVec4>(ImColor::HSV(hue, 0.55f, 0.95f)));
}
/**
* Draws a single 100%-stacked horizontal "frame budget" bar: one row whose width is split into
* segments proportional to each category's share of the frame's traced time (the snapshot's
* top contributors, see FRAME_OVERLAY_MAX_CONTRIBUTORS, + "Other"), followed by a legend with
* matching swatches, absolute ms and percentage. Cheaper than a pie chart -- it's a handful of
* ImDrawList rectangles with no ImPlot plot setup.
*/
void draw_frame_budget_bar(const frame_overlay_snapshot& snapshot) {
if (snapshot.total_nanosec == 0) {
return;
}
// +1 for "Other": snapshot.top_contributors holds at most FRAME_OVERLAY_MAX_CONTRIBUTORS
// entries (see tracing.h), so that's the real bound on how many segments can ever exist here.
constexpr int MAX_SEGMENTS = static_cast<int>(FRAME_OVERLAY_MAX_CONTRIBUTORS) + 1;
constexpr ImU32 OTHER_COLOR = IM_COL32(130, 130, 130, 255);
struct segment {
const char* name;
double pct;
float ms;
ImU32 color;
};
segment segments[MAX_SEGMENTS];
int count = 0;
const auto total = static_cast<double>(snapshot.total_nanosec);
// Self-defending against MAX_SEGMENTS regardless of caller bookkeeping, since this writes into
// a fixed-size array: silently drops anything past capacity rather than overflowing it.
auto add_segment = [&](const char* name, uint64_t self_nanosec, ImU32 color) {
if (count >= MAX_SEGMENTS) {
return;
}
segments[count].name = name;
segments[count].pct = 100.0 * static_cast<double>(self_nanosec) / total;
segments[count].ms = static_cast<float>(static_cast<double>(self_nanosec) / NANOSEC_PER_MS);
segments[count].color = color;
count++;
};
for (const auto& contributor : snapshot.top_contributors) {
add_segment(contributor.name.c_str(), contributor.self_nanosec, color_for_name(contributor.name.c_str()));
}
if (snapshot.other_nanosec > 0) {
add_segment("Other", snapshot.other_nanosec, OTHER_COLOR);
}
if (count == 0) {
return;
}
// The stacked bar.
ImDrawList* draw_list = ImGui::GetWindowDrawList();
const ImVec2 origin = ImGui::GetCursorScreenPos();
const float width = ImGui::GetContentRegionAvail().x;
constexpr float height = 22.0f;
draw_list->AddRectFilled(origin, ImVec2(origin.x + width, origin.y + height), IM_COL32(35, 35, 35, 255), 3.0f);
float x = origin.x;
for (int i = 0; i < count; i++) {
float seg_w = width * static_cast<float>(segments[i].pct / 100.0);
// Make sure the final segment reaches the right edge despite float rounding.
const float x_end = (i == count - 1) ? (origin.x + width) : (x + seg_w);
draw_list->AddRectFilled(ImVec2(x, origin.y), ImVec2(x_end, origin.y + height), segments[i].color);
x = x_end;
}
draw_list->AddRect(origin, ImVec2(origin.x + width, origin.y + height), IM_COL32(80, 80, 80, 255), 3.0f);
ImGui::Dummy(ImVec2(width, height));
// The legend.
for (int i = 0; i < count; i++) {
ImGui::ColorButton(segments[i].name,
ImGui::ColorConvertU32ToFloat4(segments[i].color),
ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoBorder,
ImVec2(12, 12));
ImGui::SameLine();
ImGui::Text("%s: %.2f ms (%.1f%%)", segments[i].name, segments[i].ms, segments[i].pct);
}
}
/**
* Draws whichever -gr_debug stat groups the active graphics backend collects (see
* gr_get_debug_stats()). Silently omits a group if its "valid" flag is unset, so this stays a
* no-op for backends (or builds) that don't populate a given group.
*/
void draw_graphics_debug_stats() {
gr_debug_stats stats = gr_get_debug_stats();
if (!stats.uniform_buffer_valid && !stats.draw_stats_valid) {
return;
}
ImGui::Separator();
ImGui::TextUnformatted("Graphics API stats (-gr_debug)");
if (stats.uniform_buffer_valid) {
ImGui::Text("Uniform buffer: " SIZE_T_ARG " / " SIZE_T_ARG " bytes used",
stats.uniform_buffer_used,
stats.uniform_buffer_size);
}
if (stats.draw_stats_valid) {
ImGui::Text("Draw calls: %d (%d indexed)", stats.draw_calls, stats.draw_indexed_calls);
ImGui::Text("Vertices: %d Indices: %d", stats.total_vertices, stats.total_indices);
ImGui::Text("Material applies: %d (%d failed, %d no pipeline)",
stats.apply_material_calls,
stats.apply_material_failures,
stats.no_pipeline_skips);
ImGui::Text("Descriptor sets: %d writes: %d pipelines: " SIZE_T_ARG,
stats.descriptor_sets_allocated,
stats.descriptor_writes,
stats.pipeline_count);
if (stats.on_demand_texture_uploads > 0) {
ImGui::Text("On-demand texture uploads: %d", stats.on_demand_texture_uploads);
}
}
}
// Memory stats describe level-scope state (loaded models, live object pools, GPU heaps), not
// per-frame state, so they're refreshed on an interval rather than walked every frame like the
// timing data above -- that avoids perturbing the very frame cost this overlay measures.
constexpr int MEMORY_STATS_REFRESH_INTERVAL_MS = 1000;
bool Memory_stats_initialized = false;
int Last_memory_stats_refresh_ms = 0;
object_memory_stats Cached_object_memory_stats;
model_memory_stats Cached_model_memory_stats;
gr_memory_stats Cached_gr_memory_stats;
os_memory_stats Cached_os_memory_stats;
sound_memory_stats Cached_sound_memory_stats;
scripting_memory_stats Cached_scripting_memory_stats;
void refresh_memory_stats_if_needed() {
int now_ms = timer_get_milliseconds();
if (Memory_stats_initialized && (now_ms - Last_memory_stats_refresh_ms) < MEMORY_STATS_REFRESH_INTERVAL_MS) {
return;
}
Cached_object_memory_stats = obj_get_memory_stats();
Cached_model_memory_stats = model_get_memory_stats();
Cached_gr_memory_stats = gr_get_memory_stats();
Cached_os_memory_stats = os_get_memory_stats();
Cached_sound_memory_stats = snd_get_memory_stats();
Cached_scripting_memory_stats = scripting_get_memory_stats();
Last_memory_stats_refresh_ms = now_ms;
Memory_stats_initialized = true;
}
/**
* Formats a byte count with the largest unit that keeps at least one digit before the decimal point
* (e.g. "12.3 MB").
*/
SCP_string format_bytes(size_t bytes) {
constexpr double KB = 1024.0;
constexpr double MB = KB * 1024.0;
constexpr double GB = MB * 1024.0;
char buf[32];
if (auto b = static_cast<double>(bytes); b >= GB) {
snprintf(buf, sizeof(buf), "%.2f GB", b / GB);
} else if (b >= MB) {
snprintf(buf, sizeof(buf), "%.2f MB", b / MB);
} else if (b >= KB) {
snprintf(buf, sizeof(buf), "%.2f KB", b / KB);
} else {
snprintf(buf, sizeof(buf), SIZE_T_ARG " B", bytes);
}
return { buf };
}
/**
* Draws pool occupancy (used / max) for the object, ship, and weapon pools. These are fixed-size
* static arrays, so their byte footprint is a compile-time constant -- occupancy is the only
* signal worth showing.
*/
void draw_object_memory_stats() {
const object_memory_stats& stats = Cached_object_memory_stats;
ImGui::Separator();
ImGui::TextUnformatted("Object Management");
ImGui::Text("Objects: %d / %d (peak %d)", stats.objects_used, stats.objects_max, stats.objects_peak);
ImGui::Text("Ships: %d / %d", stats.ships_used, stats.ships_max);
ImGui::Text("Weapons: %d / %d", stats.weapons_used, stats.weapons_max);
}
/**
* Draws model and texture memory usage. The GPU-heap numbers (live, reflects frees as models
* unload) and the per-model summed numbers (from currently loaded models) are two different views
* of related-but-not-identical data and are deliberately not added together into a single total.
*/
void draw_asset_memory_stats() {
const model_memory_stats& model_stats = Cached_model_memory_stats;
const gr_memory_stats& gr_stats = Cached_gr_memory_stats;
if (!model_stats.valid && !gr_stats.model_heap_valid && !gr_stats.locked_bitmap_ram_valid) {
return;
}
ImGui::Separator();
ImGui::TextUnformatted("Assets");
if (model_stats.valid) {
ImGui::Text("Models loaded: %d", model_stats.model_count);
ImGui::Text(" Vertex data: %s Index data: %s BSP/collision: %s",
format_bytes(model_stats.vertex_bytes).c_str(),
format_bytes(model_stats.index_bytes).c_str(),
format_bytes(model_stats.bsp_data_bytes).c_str());
}
if (gr_stats.locked_bitmap_ram_valid) {
ImGui::Text("Bitmaps (locked): %s", format_bytes(gr_stats.locked_bitmap_ram_bytes).c_str());
}
if (gr_stats.model_heap_valid) {
ImGui::Text("GPU model vertex heap: %s / %s",
format_bytes(gr_stats.model_vertex_heap_used).c_str(),
format_bytes(gr_stats.model_vertex_heap_size).c_str());
ImGui::Text("GPU model index heap: %s / %s",
format_bytes(gr_stats.model_index_heap_used).c_str(),
format_bytes(gr_stats.model_index_heap_size).c_str());
}
if (gr_stats.gpu_purpose_valid) {
ImGui::Text("GPU textures: %s geometry: %s render targets: %s",
format_bytes(gr_stats.gpu_texture_bytes).c_str(),
format_bytes(gr_stats.gpu_geometry_bytes).c_str(),
format_bytes(gr_stats.gpu_render_target_bytes).c_str());
}
}
/**
* Draws the process-wide resident set size (RSS), when the platform supports querying it. This
* already includes every other row in this panel -- the engine heap, the Lua heap, the OpenAL
* buffers below, and the driver's own mappings for the GPU rows above -- so it's shown on its own
* rather than folded into any subsystem total.
*/
void draw_process_memory_stats() {
const os_memory_stats& stats = Cached_os_memory_stats;
if (!stats.valid) {
return;
}
ImGui::Separator();
ImGui::Text("Process (RSS): %s", format_bytes(stats.resident_bytes).c_str());
}
/**
* Draws memory usage for the audio and scripting subsystems. The audio number is buffer bytes
* handed to OpenAL, not an FSO heap allocation, so it's a lower bound rather than an exact figure.
*/
void draw_subsystem_memory_stats() {
const sound_memory_stats& sound_stats = Cached_sound_memory_stats;
const scripting_memory_stats& script_stats = Cached_scripting_memory_stats;
ImGui::Separator();
ImGui::TextUnformatted("Subsystems");
ImGui::Text("Audio buffers: %s", format_bytes(sound_stats.audio_buffer_bytes).c_str());
if (script_stats.valid) {
ImGui::Text("Lua heap: %s", format_bytes(script_stats.lua_heap_bytes).c_str());
}
}
} // namespace
void profiler_overlay_frame() {
if (!frame_profiling_active()) {
History_ms = {};
Last_frame_ns = 0;
return;
}
frame_profile_process_frame();
const frame_overlay_snapshot& snapshot = get_frame_profiler_overlay_snapshot();
const uint64_t now_ns = timer_get_nanoseconds();
if (Last_frame_ns != 0) {
push_history(static_cast<float>(static_cast<double>(now_ns - Last_frame_ns) / NANOSEC_PER_MS));
}
Last_frame_ns = now_ns;
gr_imgui_begin_frame();
if (!gr_imgui_frame_active()) {
// No ImGui backend on this renderer (standalone server) — nothing to draw into.
return;
}
ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_FirstUseEver);
ImGui::Begin("Frame Profiler", nullptr, ImGuiWindowFlags_NoFocusOnAppearing);
if (History_ms.elements().empty()) {
ImGui::TextUnformatted("Collecting data...");
} else {
float avg_ms = history_average(History_ms.elements());
float median_ms = history_median(History_ms.elements());
ImGui::Text("Avg: %.2f ms (%.0f FPS) Median: %.2f ms",
avg_ms,
avg_ms > 0.0f ? 1000.0f / avg_ms : 0.0f,
median_ms);
draw_frametime_graph(History_ms.elements());
ImGui::Separator();
draw_frame_budget_bar(snapshot);
}
draw_graphics_debug_stats();
refresh_memory_stats_if_needed();
if (ImGui::CollapsingHeader("Memory", ImGuiTreeNodeFlags_DefaultOpen)) {
draw_process_memory_stats();
draw_object_memory_stats();
draw_asset_memory_stats();
draw_subsystem_memory_stats();
}
ImGui::End();
}
} // namespace tracing
+24
View File
@@ -0,0 +1,24 @@
#pragma once
/** @file
* @ingroup tracing
*/
namespace tracing {
/**
* Per-frame entry point for the ImGui frame profiler overlay, called once from gr_flip().
*
* Drains the frame profiler (see frame_profile_process_frame()), folds the wall-clock time since
* the previous overlay frame into the rolling history behind the graph/avg/median, and contributes the overlay window
* -- avg/median frametime, a scrolling frametime graph, a stacked frame-budget bar of the top
* traced categories by self-time, and the -gr_debug graphics stats -- to this frame's ImGui pass,
* opening one via gr_imgui_begin_frame() if no other consumer already has.
*
* No-op when frame profiling is disabled. Being the single per-frame driver is what keeps the
* profiler's event buffer bounded in every game state, so it must stay on the common flip path
* rather than being called per game state.
*/
void profiler_overlay_frame();
}
+5 -1
View File
@@ -88,7 +88,11 @@ void TraceEventWriter::processEvent(const trace_event* event) {
_out << ",\"id\":\"" << reinterpret_cast<const void*>(event->scope) << "\"";
}
_out << ",\"name\":\"" << event->category->getName() << "\",\"ph\":\"" << getTypeStr(event->type) << "\"";
// Read the name through the id, not event->category: this runs on the writer's background
// thread, well after the event was recorded, and the Category it names may already be dead
// (see trace_event::category_id).
_out << ",\"name\":\"" << Category::getNameById(event->category_id) << "\",\"ph\":\""
<< getTypeStr(event->type) << "\"";
switch (event->type) {
case EventType::Complete:
+46 -2
View File
@@ -1,13 +1,57 @@
#include "tracing/categories.h"
#include <algorithm>
#include <mutex>
namespace tracing {
Category::Category(const char* name, bool is_graphics) : _name(name), _graphics_category(is_graphics) {
namespace {
// Maps a category id to its name. A Meyers singleton, because the categories are global statics too.
// The mutex is necessary because the JSON writer thread reads names while Lua can add categories.
struct category_registry_t {
std::mutex mutex;
SCP_vector<SCP_string> names;
};
category_registry_t& category_registry()
{
static category_registry_t registry;
return registry;
}
// Categories with the same name share an id, so a script that makes a category each frame does not
// make the registry grow.
int register_category_name(const SCP_string& name)
{
auto& [mutex, names] = category_registry();
std::scoped_lock lock(mutex);
const auto it = std::find(names.begin(), names.end(), name);
if (it == names.end()) {
names.push_back(name);
return static_cast<int>(names.size()) - 1;
}
return static_cast<int>(std::distance(names.begin(), it));
}
} // namespace
Category::Category(const char* name, const bool is_graphics)
: _name(name), _graphics_category(is_graphics), _id(register_category_name(_name)) {
}
const char* Category::getName() const {
return _name.c_str();
}
size_t Category::getCount() {
auto& [mutex, names] = category_registry();
std::scoped_lock lock(mutex);
return names.size();
}
SCP_string Category::getNameById(const int id) {
auto& [mutex, names] = category_registry();
std::scoped_lock lock(mutex);
Assertion(id >= 0 && id < static_cast<int>(names.size()), "Category id %d is out of range!", id);
return names[id];
}
bool Category::usesGPUCounter() const {
return _graphics_category;
}
+21
View File
@@ -17,12 +17,33 @@ namespace tracing {
class Category {
const SCP_string _name;
bool _graphics_category;
int _id;
public:
Category(const char* name, bool is_graphics);
const char* getName() const;
bool usesGPUCounter() const;
/**
* @brief A dense id in the range [0, getCount()), one for each distinct category name.
*
* Categories with the same name share an id, and a copy keeps the id of the original. The id
* only selects a name, so it stays valid after the Category object is gone.
*/
int getId() const { return _id; }
/**
* @brief The number of distinct category names registered so far. A safe size for an array
* indexed by getId().
*/
static size_t getCount();
/**
* @brief The name of the category with the given id, which must be in [0, getCount()).
* Thread-safe, and it does not touch a Category object.
*/
static SCP_string getNameById(int id);
};
extern Category LuaOnFrame;
+114 -17
View File
@@ -9,6 +9,7 @@
#include "TraceEventWriter.h"
#include "MainFrameTimer.h"
#include "FrameProfiler.h"
#include "options/Option.h"
#include <cinttypes>
#include <fstream>
@@ -20,18 +21,18 @@
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
static int64_t get_tid() {
static int64_t query_tid() {
return (int64_t) GetCurrentThreadId();
}
#elif __LINUX__
#include <sys/syscall.h>
static int64_t get_tid() {
static int64_t query_tid() {
return (int64_t) syscall(SYS_gettid);
}
#else
#include <pthread.h>
static int64_t get_tid() {
static int64_t query_tid() {
// This is not a reliable way of getting the tid but it's better than nothing
return (int64_t) pthread_self();
}
@@ -39,17 +40,30 @@ static int64_t get_tid() {
// A function for getting the id of the current process
#ifdef WIN32
static int64_t get_pid() {
static int64_t query_pid() {
return (int64_t)GetCurrentProcessId();
}
#else
#include <unistd.h>
static int64_t get_pid() {
static int64_t query_pid() {
return (int64_t) getpid();
}
#endif
// Cached accessors: the thread/process id never changes for the lifetime of a thread, but the
// underlying queries are real syscalls on Linux (SYS_gettid) and glibc (getpid, uncached since
// 2.25). A trace event is emitted for every TRACE_SCOPE, so querying these per event dominated the
// tracing overhead -- cache them in thread-local storage so each thread pays the syscall only once.
static int64_t get_tid() {
thread_local const int64_t tid = query_tid();
return tid;
}
static int64_t get_pid() {
thread_local const int64_t pid = query_pid();
return pid;
}
namespace {
using namespace tracing;
@@ -57,6 +71,12 @@ using namespace tracing;
std::unique_ptr<ThreadedTraceEventWriter> traceEventWriter;
std::unique_ptr<ThreadedMainFrameTimer> mainFrameTimer;
std::unique_ptr<FrameProfiler> frameProfiler;
// Guards frameProfiler's lifetime. submit_event() reads it from whichever thread emits a trace
// event -- not just the main thread; e.g. the cutscene decode/audio threads (TRACE_SCOPE in
// cutscene/player.cpp) -- while set_frame_profiling_enabled() constructs/destroys it from the main
// thread whenever the "Frame Profiler Overlay" option is toggled at runtime. Without this, a toggle
// mid-cutscene can free the object out from under a concurrent processEvent() call.
std::mutex frameProfilerMutex;
SCP_vector<int> query_objects;
// Free list for backends where queries are immediately reusable (OpenGL).
@@ -139,8 +159,11 @@ void submit_event(trace_event* evt) {
mainFrameTimer->processEvent(evt);
}
if (frameProfiler) {
frameProfiler->processEvent(evt);
{
std::lock_guard<std::mutex> lock(frameProfilerMutex);
if (frameProfiler) {
frameProfiler->processEvent(evt);
}
}
}
@@ -216,6 +239,7 @@ void process_gpu_events() {
void init_event(const Category& category, trace_event* evt) {
evt->category = &category;
evt->category_id = category.getId();
evt->timestamp = timer_get_nanoseconds();
@@ -240,10 +264,12 @@ void init() {
mainFrameTimer.reset(new ThreadedMainFrameTimer());
do_async_events = true;
}
if (Cmdline_frame_profile) {
frameProfiler.reset(new FrameProfiler());
do_trace_events = true;
}
// -profile_frame_time turns the profiler on for the whole session (kept for backward
// compatibility). OR in the current state rather than overwriting it: the
// "Game.ProfilerOverlay" option's loadInitialValues() call (see OptionsManager) runs before
// tracing::init(), so a persisted "on" setting may already have enabled the profiler by the
// time we get here, and -profile_frame_time knows nothing about it.
set_frame_profiling_enabled(Cmdline_frame_profile || frame_profiling_active());
do_gpu_queries = gr_is_capable(gr_capability::CAPABILITY_TIMESTAMP_QUERY);
queries_reusable = gr_is_capable(gr_capability::CAPABILITY_QUERIES_REUSABLE);
@@ -265,17 +291,71 @@ void process_events() {
}
}
void frame_profile_process_frame() {
Assertion(frameProfiler, "Frame profiling must be enabled for this function!");
std::lock_guard<std::mutex> lock(frameProfilerMutex);
if (!frameProfiler) {
return;
}
return frameProfiler->processFrame();
frameProfiler->processFrame();
}
SCP_string get_frame_profile_output() {
std::lock_guard<std::mutex> lock(frameProfilerMutex);
Assertion(frameProfiler, "Frame profiling must be enabled for this function!");
return frameProfiler->getContent();
}
const frame_overlay_snapshot& get_frame_profiler_overlay_snapshot() {
std::lock_guard<std::mutex> lock(frameProfilerMutex);
Assertion(frameProfiler, "Frame profiling must be enabled for this function!");
return frameProfiler->getOverlaySnapshot();
}
bool frame_profiling_active() {
std::lock_guard<std::mutex> lock(frameProfilerMutex);
return frameProfiler != nullptr;
}
void set_frame_profiling_enabled(bool enable) {
std::lock_guard<std::mutex> lock(frameProfilerMutex);
// The profiler's existence is the single source of truth for "is the frame profiler
// collecting". submit_event() feeds it whenever it exists, and frame_profile_process_frame()
// (the only thing that drains its event buffer) is driven from gr_flip(), so collection and
// draining are gated on the same condition and cannot drift apart. Destroying it on disable
// is what stops collection -- leaving a live profiler behind while nothing drained it is how
// its buffer used to grow without bound.
if (enable != (frameProfiler != nullptr)) {
if (enable) {
// Only the main thread calls this function: init() and the option's change listener. The change
// listener can run before init() sets main_thread_id, so use the id of the calling thread then.
frameProfiler.reset(new FrameProfiler(main_thread_id != -1 ? main_thread_id : get_tid()));
} else {
frameProfiler = nullptr;
}
}
// Recomputed unconditionally, not just on a transition: init() clears do_trace_events before
// calling us, and the option's change listener may already have enabled the profiler by then.
do_trace_events = Cmdline_json_profiling || enable;
}
// coverity[GLOBAL_INIT_ORDER] -- safe; OptionBuilder::finish() uses Meyers singleton
static auto ProfilerOverlayOption = options::OptionBuilder<bool>("Game.ProfilerOverlay",
std::pair<const char*, int>{"Frame Profiler Overlay", 1940},
std::pair<const char*, int>{"Show an ImGui overlay with a frametime graph and a breakdown of what's taking up frame time", 1941})
.category(std::make_pair("Graphics", 1825))
.level(options::ExpertLevel::Advanced)
.default_val(false)
.change_listener([](const bool& val, bool) {
set_frame_profiling_enabled(val);
return true;
})
.importance(69)
.finish();
void shutdown() {
if (queries_reusable) {
while (!gpu_events.empty()) {
@@ -334,6 +414,7 @@ void start(const Category& category, trace_event* evt) {
gpu_trace_event gpu_event;
gpu_event.base_evt.category = &category;
gpu_event.base_evt.category_id = category.getId();
gpu_event.base_evt.tid = 1;
gpu_event.base_evt.pid = GPU_PID;
gpu_event.base_evt.type = EventType::Begin;
@@ -342,16 +423,31 @@ void start(const Category& category, trace_event* evt) {
// This does not need to be synchronized since GPU queries are only allowed on the main thread.
gpu_events.push(gpu_event);
evt->gpu_started = true;
}
}
void end(trace_event* evt) {
if (!do_trace_events) {
// No one to process the event is here
// A scope can be open when the profiler turns on, so start() may never have run for it.
if (evt->type != EventType::Complete) {
return;
}
if (!initialized) {
// The profiler can also turn off mid-scope. Close the GPU event that start() opened.
const bool close_gpu = evt->gpu_started && initialized && do_gpu_queries;
evt->gpu_started = false;
if (!do_trace_events || !initialized) {
if (close_gpu) {
gpu_trace_event gpu_event;
gpu_event.base_evt.category = evt->category;
gpu_event.base_evt.category_id = evt->category_id;
gpu_event.base_evt.tid = 1;
gpu_event.base_evt.pid = GPU_PID;
gpu_event.base_evt.type = EventType::End;
gpu_event.gpu_begin_query = get_gpu_timestamp_query();
gpu_events.push(gpu_event);
}
return;
}
@@ -365,11 +461,12 @@ void end(trace_event* evt) {
submit_event(evt);
// Create GPU events
if (do_gpu_queries && evt->category->usesGPUCounter()) {
if (close_gpu) {
Assertion(get_tid() == main_thread_id, "This function must be called from the main thread!");
gpu_trace_event gpu_event;
gpu_event.base_evt.category = evt->category;
gpu_event.base_evt.category_id = evt->category_id;
gpu_event.base_evt.tid = 1;
gpu_event.base_evt.pid = GPU_PID;
gpu_event.base_evt.type = EventType::End;
+73
View File
@@ -41,6 +41,12 @@ enum class EventType {
*/
struct trace_event {
const Category* category = nullptr;
// Category::getId() at record time. A trace_event can outlive the Category it names -- it may
// sit in a buffer (FrameProfiler) or cross a thread (ThreadedEventProcessor) before it is
// read, and a category made through the Lua API tracing_category can be gone by then. Code
// that reads a trace_event after the point where it was recorded must use category_id with
// Category::getNameById(), never dereference category.
int category_id = -1;
const Scope* scope = nullptr;
EventType type = EventType::Invalid;
@@ -54,6 +60,42 @@ struct trace_event {
std::int64_t pid = -1;
float value = -1.f;
// True when complete::start() pushed a GPU begin event. complete::end() pushes the matching
// GPU end event only in that case, so toggling the profiler mid-scope cannot unbalance them.
bool gpu_started = false;
};
/**
* A single named contributor to a frame's total traced time, used by the ImGui frame profiler
* overlay's pie chart.
*/
struct frame_overlay_contributor {
SCP_string name;
uint64_t self_nanosec;
};
/**
* Cap on frame_overlay_snapshot::top_contributors. The single place this is defined, so the
* producer (FrameProfiler::build_overlay_snapshot) and every consumer (currently the overlay's
* stacked frame-budget bar) can't drift apart on how many contributors get their own slot before
* folding into "Other".
*/
constexpr size_t FRAME_OVERLAY_MAX_CONTRIBUTORS = 5;
/**
* A structured, per-frame snapshot of profiling data meant for the ImGui overlay (as opposed to
* get_frame_profile_output()'s preformatted text dump). top_contributors holds up to
* FRAME_OVERLAY_MAX_CONTRIBUTORS categories by self-time, sorted descending; everything else is
* folded into other_nanosec. total_nanosec is the sum of every sample's self-time
* (top_contributors + other_nanosec). All times are in nanoseconds, matching
* trace_event::timestamp/duration (timer_get_nanoseconds()).
*/
struct frame_overlay_snapshot {
bool valid = false;
uint64_t total_nanosec = 0;
SCP_vector<frame_overlay_contributor> top_contributors;
uint64_t other_nanosec = 0;
};
/**
@@ -66,6 +108,13 @@ void init();
*/
void process_events();
/**
* @brief Folds this frame's buffered trace events into the overlay snapshot and clears the buffer
*
* This is the only thing that drains the profiler's event buffer, so it must run once per
* presented frame for as long as collection is enabled — gr_flip() drives it (via
* profiler_overlay_frame()) for exactly that reason. No-op when profiling is off.
*/
void frame_profile_process_frame();
/**
@@ -74,6 +123,30 @@ void frame_profile_process_frame();
*/
SCP_string get_frame_profile_output();
/**
* @brief Gets a structured snapshot of the current frame's profiling data, for the ImGui overlay.
* @return The frame profiler overlay snapshot
*/
const frame_overlay_snapshot& get_frame_profiler_overlay_snapshot();
/**
* @brief True if frame profiling data is currently being collected
*
* This is the single source of truth for "is the frame profiler on" — every consumer (the
* overlay, the lab's text dump, osapi's ImGui input forwarding) asks here rather than
* re-deriving it from the command line or the options system.
*/
bool frame_profiling_active();
/**
* @brief Enables or disables frame profiling collection at runtime
*
* Constructs the profiler on enable and destroys it on disable; that lifetime *is*
* frame_profiling_active(). Driven by the "Frame Profiler Overlay" option and seeded from
* -profile_frame_time at startup.
*/
void set_frame_profiling_enabled(bool enable);
/**
* @brief Deinitializes the tracing subsystem
*/
+10
View File
@@ -173,6 +173,16 @@ void HeapAllocator::free(size_t offset) {
size_t HeapAllocator::numAllocations() const {
return _allocatedRanges.size();
}
size_t HeapAllocator::usedBytes() const {
size_t total = 0;
for (const auto& range : _allocatedRanges) {
total += range.size;
}
return total;
}
size_t HeapAllocator::heapSize() const {
return _heapSize;
}
bool HeapAllocator::check_connected_range(const MemoryRange& left, const MemoryRange& right) {
return left.offset + left.size == right.offset;
}
+12
View File
@@ -69,6 +69,18 @@ class HeapAllocator {
* @return The active allocations in this heap.
*/
size_t numAllocations() const;
/**
* @brief Retrieves the total number of bytes currently allocated from this heap
* @return The sum of the sizes of all active allocations.
*/
size_t usedBytes() const;
/**
* @brief Retrieves the total size of the heap, including both allocated and free space
* @return The current heap size.
*/
size_t heapSize() const;
};
}
+4 -23
View File
@@ -212,6 +212,7 @@
#include <stdexcept>
#include "imgui.h"
#include "implot.h"
#ifdef WIN32
// According to AMD and NV, these _should_ force their drivers into high-performance mode
@@ -1886,6 +1887,7 @@ void game_init()
Random::seed(static_cast<unsigned int>(time(nullptr)));
ImGui::CreateContext();
ImPlot::CreateContext();
Framerate_delay = 0;
@@ -2353,28 +2355,10 @@ void game_show_framerate()
}
#endif
if ((Show_framerate && HUD_draw) || Cmdline_frame_profile || Cmdline_bmpman_usage) {
if ((Show_framerate && HUD_draw) || Cmdline_bmpman_usage) {
gr_set_color_fast(&HUD_color_debug);
if (Cmdline_frame_profile) {
// Split frame profile into two columns if necessary to avoid losing trace data
int fp_start_y = gr_screen.center_offset_y + 100 + line_height;
int fp_line_limit = (gr_screen.max_h - fp_start_y) / line_height;
size_t fp_column_break = 0;
auto fp_trace_str = tracing::get_frame_profile_output();
for (int i = 0; i < fp_line_limit && fp_column_break < fp_trace_str.length(); i++) {
fp_column_break = fp_trace_str.find_first_of('\n', fp_column_break+1);
}
gr_string(gr_screen.center_offset_x + 20, fp_start_y, fp_trace_str.substr(0,fp_column_break).c_str(), GR_RESIZE_NONE);
if (fp_column_break < fp_trace_str.length()) {
gr_string(gr_screen.max_w / 2, fp_start_y, fp_trace_str.substr(fp_column_break, fp_trace_str.npos).c_str(), GR_RESIZE_NONE);
}
}
if (Show_framerate) {
if (frametotal != 0.0f)
gr_printf_no_resize( gr_screen.center_offset_x + 20, gr_screen.center_offset_y + 100, "FPS: %0.1f", Framerate );
@@ -4447,10 +4431,6 @@ void game_frame(bool paused)
// process lightning (nebula only)
nebl_process();
if (Cmdline_frame_profile) {
tracing::frame_profile_process_frame();
}
DEBUG_GET_TIME( total_time2 )
#ifndef NDEBUG
@@ -7198,6 +7178,7 @@ void game_shutdown(void)
std_deinit_standalone();
}
ImPlot::DestroyContext();
ImGui::DestroyContext();
os_cleanup();
+1
View File
@@ -52,6 +52,7 @@ add_subdirectory(accidental-noise)
ADD_SUBDIRECTORY(lz4)
ADD_SUBDIRECTORY(imgui)
ADD_SUBDIRECTORY(implot)
if(FSO_BUILD_WITH_OPENXR)
add_subdirectory(openxr EXCLUDE_FROM_ALL)
+24
View File
@@ -0,0 +1,24 @@
SET(IMPLOT_SOURCES
implot.h
implot_internal.h
implot.cpp
implot_items.cpp
)
ADD_LIBRARY(implot STATIC ${IMPLOT_SOURCES})
target_include_directories(implot SYSTEM PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
# ImPlot uses imgui's ImVec2/ImVec4 math operators, which imgui only defines when
# IMGUI_DEFINE_MATH_OPERATORS is set before imgui.h is first included. implot_items.cpp
# pulls in imgui.h (via implot.h) before implot_internal.h's own #ifndef guard runs, so
# the guard loses the race -- define it on the target instead. PUBLIC so any consumer that
# includes implot.h after imgui.h stays consistent.
target_compile_definitions(implot PUBLIC IMGUI_DEFINE_MATH_OPERATORS)
# Disable warnings if building from source
suppress_warnings(implot)
set_target_properties(implot PROPERTIES FOLDER "3rdparty")
TARGET_LINK_LIBRARIES(implot PUBLIC imgui)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Evan Pezent
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+178
View File
@@ -0,0 +1,178 @@
# ImPlot
ImPlot is an immediate mode, GPU accelerated plotting library for [Dear ImGui](https://github.com/ocornut/imgui). It aims to provide a first-class API that ImGui fans will love. ImPlot is well suited for visualizing program data in real-time or creating interactive plots, and requires minimal code to integrate. Just like ImGui, it does not burden the end user with GUI state management, avoids STL containers and C++ headers, and has no external dependencies except for ImGui itself.
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/controls.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/dnd.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/pie.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/query.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/bars.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/rt.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/stem.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/markers.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/shaded.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/candle.gif" width="270"> <img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/heat.gif" width="270">
<img src="https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/tables.gif" width="270">
## Features
- GPU accelerated rendering
- multiple plot types:
- line plots
- shaded plots
- scatter plots
- vertical/horizontal/stacked bars graphs
- vertical/horizontal error bars
- stem plots
- stair plots
- pie charts
- heatmap charts
- 1D/2D histograms
- images
- and more likely to come
- mix/match multiple plot items on a single plot
- configurable axes ranges and scaling (linear/log)
- subplots
- time formatted x-axes (US formatted or ISO 8601)
- reversible and lockable axes
- multiple x-axes and y-axes
- controls for zooming, panning, box selection, and auto-fitting data
- controls for creating persistent query ranges (see demo)
- several plot styling options: 10 marker types, adjustable marker sizes, line weights, outline colors, fill colors, etc.
- 16 built-in colormaps and support for and user-added colormaps
- optional plot titles, axis labels, and grid labels
- optional and configurable legends with toggle buttons to quickly show/hide plot items
- default styling based on current ImGui theme, or completely custom plot styles
- customizable data getters and data striding (just like ImGui:PlotLine)
- accepts data as float, double, and 8, 16, 32, and 64-bit signed/unsigned integral types
- and more! (see Announcements [2022](https://github.com/epezent/implot/discussions/370)/[2021](https://github.com/epezent/implot/issues/168)/[2020](https://github.com/epezent/implot/issues/48))
## Usage
The API is used just like any other ImGui `BeginX`/`EndX` pair. First, start a new plot with `ImPlot::BeginPlot()`. Next, plot as many items as you want with the provided `PlotX` functions (e.g. `PlotLine()`, `PlotBars()`, `PlotScatter()`, etc). Finally, wrap things up with a call to `ImPlot::EndPlot()`. That's it!
```cpp
int bar_data[11] = ...;
float x_data[1000] = ...;
float y_data[1000] = ...;
ImGui::Begin("My Window");
if (ImPlot::BeginPlot("My Plot")) {
ImPlot::PlotBars("My Bar Plot", bar_data, 11);
ImPlot::PlotLine("My Line Plot", x_data, y_data, 1000);
...
ImPlot::EndPlot();
}
ImGui::End();
```
![Usage](https://raw.githubusercontent.com/wiki/epezent/implot/screenshots3/example.PNG)
Of course, there's much more you can do with ImPlot...
## Demos
A comprehensive example of ImPlot's features can be found in `implot_demo.cpp`. Add this file to your sources and call `ImPlot::ShowDemoWindow()` somewhere in your update loop. You are encouraged to use this file as a reference when needing to implement various plot types. The demo is always updated to show new plot types and features as they are added, so check back with each release!
An online version of the demo is hosted [here](https://pthom.github.io/imgui_explorer/?lib=implot). You can view the plots and the source code that generated them. Note that this demo may not always be up to date and is not as performant as a desktop implementation, but it should give you a general taste of what's possible with ImPlot. Special thanks to [pthom](https://github.com/pthom) for creating and hosting this!
More sophisticated demos requiring lengthier code and/or third-party libraries can be found in a separate repository: [implot_demos](https://github.com/epezent/implot_demos). Here, you will find advanced signal processing and ImPlot usage in action. Please read the `Contributing` section of that repository if you have an idea for a new demo!
## Integration
0) Set up an [ImGui](https://github.com/ocornut/imgui) environment if you don't already have one.
1) Add `implot.h`, `implot_internal.h`, `implot.cpp`, `implot_items.cpp` and optionally `implot_demo.cpp` to your sources. Alternatively, you can get ImPlot using [vcpkg](https://github.com/microsoft/vcpkg/tree/master/ports/implot).
2) Create and destroy an `ImPlotContext` wherever you do so for your `ImGuiContext`:
```cpp
ImGui::CreateContext();
ImPlot::CreateContext();
...
ImPlot::DestroyContext();
ImGui::DestroyContext();
```
You should be good to go!
## Installing ImPlot using vcpkg
You can download and install ImPlot using the [vcpkg](https://github.com/Microsoft/vcpkg) dependency manager:
```bash
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh
./vcpkg integrate install
./vcpkg install implot
```
The ImPlot port in vcpkg is kept up to date by Microsoft team members and community contributors. If the version is out of date, please [create an issue or pull request](https://github.com/Microsoft/vcpkg) on the vcpkg repository.
## Extremely Important Note
Dear ImGui uses **16-bit indexing by default**, so high-density ImPlot widgets like `ImPlot::PlotHeatmap()` may produce too many vertices into `ImDrawList`, which causes an assertion failure and will result in data truncation and/or visual glitches. Therefore, it is **HIGHLY** recommended that you EITHER:
- **Option 1:** Enable 32-bit indices by uncommenting `#define ImDrawIdx unsigned int` in your ImGui [`imconfig.h`](https://github.com/ocornut/imgui/blob/master/imconfig.h#L89) file.
- **Option 2:** Handle the `ImGuiBackendFlags_RendererHasVtxOffset` flag in your renderer if you must use 16-bit indices. Many of the default ImGui rendering backends already support `ImGuiBackendFlags_RendererHasVtxOffset`. Refer to [this issue](https://github.com/ocornut/imgui/issues/2591) for more information.
## FAQ
**Q: Why?**
A: ImGui is an incredibly powerful tool for rapid prototyping and development, but provides only limited mechanisms for data visualization. Two dimensional plots are ubiquitous and useful to almost any application. Being able to visualize your data in real-time will give you insight and better understanding of your application.
**Q: Is ImPlot the right plotting library for me?**
A: If you're looking to generate publication quality plots and/or export plots to a file, ImPlot is NOT the library for you! ImPlot is geared toward plotting application data at realtime speeds with high levels of interactivity. ImPlot does its best to create pretty plots (indeed, there are quite a few styling options available), but it will always favor function over form.
**Q: Where is the documentation?**
A: The API is thoroughly commented in `implot.h`, and the demo in `implot_demo.cpp` should be more than enough to get you started. Also take a look at the [implot_demos](https://github.com/epezent/implot_demos) repository.
**Q: Is ImPlot suitable for plotting large datasets?**
A: Yes, within reason. You can plot tens to hundreds of thousands of points without issue, but don't expect millions to be a buttery smooth experience. That said, you can always downsample extremely large datasets by telling ImPlot to stride your data at larger intervals if needed. Also try the experimental `backends` branch which aims to provide GPU acceleration support.
**Q: What data types can I plot?**
A: ImPlot plotting functions accept most scalar types:
`float`, `double`, `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64`. Arrays of custom structs or classes (e.g. `Vector2f` or similar) are easily passed to ImPlot functions using the built-in striding features (see `implot.h` for documentation), and many plotters provide a "getter" overload which accepts data generating callbacks. You can fully customize the list of accepted types by defining `IMPLOT_CUSTOM_NUMERIC_TYPES` at compile time: see doc in `implot_items.cpp`.
**Q: Can plot styles be modified?**
A: Yes. Three default styles are available, as well as an automatic style that attempts to match you ImGui style. You can define any custom style as well. Plot items are generally styled for you based on the current colormap, but can be customized on an individual basis.
**Q: Does ImPlot support non-linear axis scaling? Time formatting?**
A: Yes. Logscale and symmetric logscale are provided out of the box, and you can define custom axis scales as well. Time scale with microsecond precision is also available out of the box.
**Q: Does ImPlot support multiple y-axes? x-axes?**
A: Yes. Up to three x-axes and three y-axes can be enabled.
**Q: Does ImPlot support [insert plot type]?**
A: Maybe. Check the demo, gallery, or Announcements ([2020](https://github.com/epezent/implot/issues/48)/[2021](https://github.com/epezent/implot/issues/168)/[2022](https://github.com/epezent/implot/discussions/370)) to see if your desired plot type is shown. If not, consider submitting an issue or better yet, a PR!
**Q: Does ImPlot support 3D plots?**
A: An experimental extension to ImPlot, [ImPlot3D](https://github.com/brenocq/implot3d), provides a similar API for plotting and interacting with 3D data.
**Q: Does ImPlot provide analytic tools?**
A: Not exactly, but it does give you the ability to query plot sub-ranges, with which you can process your data however you like.
**Q: Can plots be exported/saved to image?**
A: Not currently. Use your OS's screen capturing mechanisms if you need to capture a plot. ImPlot is not suitable for rendering publication quality plots; it is only intended to be used as a visualization tool. Post-process your data with MATLAB or matplotlib for these purposes.
**Q: Why are my plot lines showing aliasing?**
A: You probably need to enable `ImGuiStyle::AntiAliasedLinesUseTex` (or possibly `ImGuiStyle:AntiAliasedLines`). If those settings are already enabled, then you must ensure your backend supports texture based anti-aliasing (i.e. uses bilinear sampling). Most of the default ImGui backends support this feature out of the box. Learn more [here](https://github.com/ocornut/imgui/issues/3245). Alternatively, you can enable MSAA at the application level if your hardware supports it (4x should do).
**Q: Can I compile ImPlot as a dynamic library?**
A: Like ImGui, it is recommended that you compile and link ImPlot as a *static* library or directly as a part of your sources. However, if you must and are compiling ImPlot and ImGui as separate DLLs, make sure you set the current *ImGui* context with `ImPlot::SetImGuiContext(ImGuiContext* ctx)`. This ensures that global ImGui variables are correctly shared across the DLL boundary.
**Q: Can ImPlot be used with other languages/bindings?**
A: Yes, you can use the generated C binding, [cimplot](https://github.com/cimgui/cimplot) with most high level languages. [DearPyGui](https://github.com/hoffstadt/DearPyGui) provides a Python wrapper, among other things. [DearImGui/DearImPlot](https://github.com/aybe/DearImGui) provides bindings for .NET. [imgui-java](https://github.com/SpaiR/imgui-java) provides bindings for Java. [ImPlot.jl](https://github.com/wsphillips/ImPlot.jl) provides bindings for Julia. A Rust binding, [implot-rs](https://github.com/4bb4/implot-rs), is currently in the works. An example using Emscripten can be found [here](https://github.com/pthom/implot_demo).
+102
View File
@@ -0,0 +1,102 @@
The list below represents a combination of high-priority work, nice-to-have features, and random ideas. We make no guarantees that all of this work will be completed or even started. If you see something that you need or would like to have, let us know, or better yet consider submitting a PR for the feature.
## API
## Axes
- add flag to remove weekends on Time axis
- pixel space scale (`ImPlotTransform_Display`), normalized space scale (`ImPlotTransform_Axes`), data space scale (`ImPlotTransform_Data`)
- make ImPlotFlags_Equal not a flag -> `SetupEqual(ImPlotAxis x, ImPlotAxis y)`
- allow inverted arguments `SetAxes` to transpose data?
- `SetupAxisColors()`
- `SetupAxisHome()`
## Plot Items
- add non-zero references for `PlotBars` etc.
- fix appearance of `PlotBars` spacing
## Styling
- support gradient and/or colormap sampled fills (e.g. ImPlotFillStyle_)
- API for setting different fonts for plot elements
## Colormaps
- gradient editing tool
- `RemoveColormap`
- `enum ImPlotColorRule_ { Solid, Faded, XValue, YValue, ZValue }`
## Legend
- improve legend icons (e.g. adopt markers, gradients, etc)
- generalize legend rendering for plots and subplots
- add draggable scroll bar if users need it
## Tools / Misc.
- add `IsPlotChanging` to detect change in limits
- add ability to extend plot/axis context menus
- add LTTB downsampling for lines
- add box selection to axes
- first frame render delay might fix "fit pop" effect
- move some code to new `implot_tools.cpp`
- ColormapSlider (see metrics)
- FillAlpha should not affect markers?
- fix mouse text for time axes
## Optimizations
- find faster way to buffer data into ImDrawList (very slow)
- reduce number of calls to `PushClipRect`
- explore SIMD operations for high density plot items
## Plotter Pipeline
Ideally every `PlotX` function should use our faster rendering pipeline when it is applicable.
` User Data > Getter > Fitter > Renderer > RenderPrimitives`
|Plotter|Getter|Fitter|Renderer|RenderPrimitives|
|---|:-:|:-:|:-:|:-:|
|PlotLine|Yes|Yes|Yes|Yes|
|PlotScatter|Yes|Yes|Yes|Yes|
|PlotStairs|Yes|Yes|Yes|Yes|
|PlotShaded|Yes|Yes|Yes|Yes|
|PlotBars|Yes|Yes|Yes|Yes|
|PlotBarGroups|:|:|:|:|
|PlotHistogram|:|:|:|:|
|PlotErrorBars|Yes|Yes|No|No|
|PlotStems|Yes|Yes|Yes|Yes|
|PlotInfLines|Yes|Yes|Yes|Yes|
|PlotPieChart|No|No|No|No|
|PlotHeatmap|Yes|No|Yes|Mixed|
|PlotHistogram2D|:|:|:|:|
|PlotDigital|Yes|No|No|No|
|PlotImage|-|-|-|-|
|PlotText|-|-|-|-|
|PlotDummy|-|-|-|-|
## Completed
- add `PlotBubbles` (see MATLAB bubble chart)
- add exploding to `PlotPieChart` (on legend hover)
- make BeginPlot take fewer args:
- make query a tool -> `DragRect`
- rework DragLine/Point to use ButtonBehavior
- add support for multiple x-axes and don't limit count to 3
- make axis side configurable (top/left, right/bottom) via new flag `ImPlotAxisFlags_Opposite`
- add support for setting tick label strings via callback
- give each axis an ID, remove ad-hoc DND solution
- allow axis to be drag to opposite side (ala ImGui Table headers)
- legend items can be hovered even if plot is not
- fix frame delay on DragX tools
- remove tag from drag line/point -> add `Tag` tool
- add shortcut/legacy overloads for BeginPlot
- `SetupAxisConstraints()`
- `SetupAxisScale()`
- add `ImPlotLineFlags`, `ImPlotBarsFlags`, etc. for each plot type
- add `PlotBarGroups` wrapper that makes rendering groups of bars easier, with stacked bar support
- `PlotBars` restore outlines
- add hover/active color for plot axes
- make legend frame use ButtonBehavior
- `ImPlotLegendFlags_Scroll` (default behavior)
File diff suppressed because it is too large. Load diff
+1408
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+4
View File
@@ -103,6 +103,10 @@ add_file_folder("Test Util"
util/test_util.h
)
add_file_folder("Tracing"
tracing/test_frame_profiler.cpp
)
add_file_folder("Utils"
utils/HeapAllocatorTest.cpp
)
+371
View File
@@ -0,0 +1,371 @@
//
//
#include <gtest/gtest.h>
#include "graphics/2d.h"
#include "tracing/FrameProfiler.h"
#include "tracing/categories.h"
#include "io/timer.h"
#include "tracing/tracing.h"
#include "util/FSTestFixture.h"
using namespace tracing;
namespace {
trace_event make_event(const Category& category, EventType type, uint64_t timestamp) {
trace_event evt;
evt.category = &category;
evt.category_id = category.getId();
evt.type = type;
evt.timestamp = timestamp;
return evt;
}
trace_event make_complete_event(const Category& category,
uint64_t timestamp,
uint64_t duration,
uint64_t event_id,
uint64_t end_event_id) {
trace_event evt;
evt.category = &category;
evt.category_id = category.getId();
evt.type = EventType::Complete;
evt.timestamp = timestamp;
evt.duration = duration;
evt.event_id = event_id;
evt.end_event_id = end_event_id;
evt.pid = 1;
evt.tid = 1;
return evt;
}
// Small helper to run the function under test and look results up by category.
struct self_time_result {
SCP_vector<uint64_t> by_id;
uint64_t total = 0;
explicit self_time_result(const SCP_vector<trace_event>& events) {
total = accumulate_self_times(events, by_id);
}
uint64_t self(const Category& c) const { return by_id[static_cast<size_t>(c.getId())]; }
};
} // namespace
// Parent fully contains a child: the child's interval is exclusive to the child, the rest is the
// parent's self-time.
TEST(FrameProfilerSelfTime, nested_scope)
{
SCP_vector<trace_event> events{
make_event(Physics, EventType::Begin, 0),
make_event(PostMove, EventType::Begin, 5),
make_event(PostMove, EventType::End, 8),
make_event(Physics, EventType::End, 10),
};
self_time_result r(events);
EXPECT_EQ(r.self(Physics), 7u); // [0,5) + [8,10)
EXPECT_EQ(r.self(PostMove), 3u); // [5,8)
EXPECT_EQ(r.total, 10u);
}
// Two top-level scopes with an idle gap between them: the gap belongs to no scope and must be
// excluded from both the per-category totals and the frame total.
TEST(FrameProfilerSelfTime, gap_between_top_level_scopes)
{
SCP_vector<trace_event> events{
make_event(Physics, EventType::Begin, 0),
make_event(Physics, EventType::End, 3),
// gap [3, 10)
make_event(PostMove, EventType::Begin, 10),
make_event(PostMove, EventType::End, 15),
};
self_time_result r(events);
EXPECT_EQ(r.self(Physics), 3u);
EXPECT_EQ(r.self(PostMove), 5u);
EXPECT_EQ(r.total, 8u); // gap of 7 excluded
}
// The same category nested within itself (recursion). All time is still attributed to that one
// category; the old tree-based path would have tripped its "max 1 open" assert here.
TEST(FrameProfilerSelfTime, recursion_same_category)
{
SCP_vector<trace_event> events{
make_event(Physics, EventType::Begin, 0),
make_event(Physics, EventType::Begin, 3),
make_event(Physics, EventType::End, 7),
make_event(Physics, EventType::End, 10),
};
self_time_result r(events);
EXPECT_EQ(r.self(Physics), 10u);
EXPECT_EQ(r.total, 10u);
}
// A parent with two sibling children. Verifies the parent's self-time correctly excludes both
// children and that the frame total equals the sum of every category's self-time.
TEST(FrameProfilerSelfTime, siblings_sum_to_total)
{
SCP_vector<trace_event> events{
make_event(MoveObjects, EventType::Begin, 0),
make_event(Physics, EventType::Begin, 2),
make_event(Physics, EventType::End, 7),
make_event(PostMove, EventType::Begin, 10),
make_event(PostMove, EventType::End, 14),
make_event(MoveObjects, EventType::End, 20),
};
self_time_result r(events);
EXPECT_EQ(r.self(Physics), 5u);
EXPECT_EQ(r.self(PostMove), 4u);
EXPECT_EQ(r.self(MoveObjects), 11u); // [0,2) + [7,10) + [14,20)
uint64_t sum = 0;
for (uint64_t v : r.by_id) {
sum += v;
}
EXPECT_EQ(sum, r.total);
EXPECT_EQ(r.total, 20u);
}
// Empty input must be handled gracefully.
TEST(FrameProfilerSelfTime, empty_input)
{
SCP_vector<trace_event> events;
self_time_result r(events);
EXPECT_EQ(r.total, 0u);
}
// The output buffer is meant to be reused across frames, so a run must not see the previous run's
// totals. (processFrame() keeps one buffer alive for the life of the profiler.)
TEST(FrameProfilerSelfTime, reused_buffer_is_cleared)
{
SCP_vector<uint64_t> by_id;
SCP_vector<trace_event> first{
make_event(Physics, EventType::Begin, 0),
make_event(Physics, EventType::End, 10),
};
EXPECT_EQ(accumulate_self_times(first, by_id), 10u);
EXPECT_EQ(by_id[static_cast<size_t>(Physics.getId())], 10u);
SCP_vector<trace_event> second{
make_event(Physics, EventType::Begin, 0),
make_event(Physics, EventType::End, 3),
};
EXPECT_EQ(accumulate_self_times(second, by_id), 3u);
EXPECT_EQ(by_id[static_cast<size_t>(Physics.getId())], 3u);
// An empty frame must zero it rather than leave the previous frame's numbers behind.
SCP_vector<trace_event> empty;
EXPECT_EQ(accumulate_self_times(empty, by_id), 0u);
EXPECT_EQ(by_id[static_cast<size_t>(Physics.getId())], 0u);
}
// accumulate_self_times must not dereference a Category through the event: FrameProfiler buffers
// events across frames (see processFrame()), so a category made through the Lua API
// tracing_category can die between processEvent() and the processFrame() that finally walks this
// event. Reading evt.category->getId() on that dangling pointer, and indexing self_time_by_id with
// whatever garbage id came back, is the out-of-bounds write that used to corrupt the heap (see
// trace_event::category_id).
TEST(FrameProfilerSelfTime, category_can_die_before_self_time_is_accumulated)
{
SCP_vector<trace_event> events;
int id;
{
Category temporary("Frame profiler self-time test category", false);
id = temporary.getId();
events = {
make_event(temporary, EventType::Begin, 0),
make_event(Physics, EventType::Begin, 5),
make_event(Physics, EventType::End, 8),
make_event(temporary, EventType::End, 10),
};
}
// temporary is dead here; events still carries its address, but accumulate_self_times must
// use category_id, not that address.
self_time_result r(events);
EXPECT_EQ(r.by_id[static_cast<size_t>(id)], 7u); // [0,5) + [8,10)
EXPECT_EQ(r.self(Physics), 3u); // [5,8)
EXPECT_EQ(r.total, 10u);
}
// Ids are dense and the registry round-trips them to a name, which is what lets the overlay
// snapshot keep ids alone instead of a parallel id -> name array.
TEST(TracingCategory, id_round_trips_through_registry)
{
ASSERT_GT(Category::getCount(), 0u);
for (size_t id = 0; id < Category::getCount(); id++) {
EXPECT_FALSE(Category::getNameById(static_cast<int>(id)).empty());
}
EXPECT_EQ(Category::getNameById(Physics.getId()), SCP_string(Physics.getName()));
EXPECT_EQ(Category::getNameById(PostMove.getId()), SCP_string(PostMove.getName()));
}
// The overlay snapshot must name its contributors through the registry, not through a Category
// pointer. This drives the full path that crashed: processEvent -> processFrame ->
// build_overlay_snapshot -> getNameById.
TEST(FrameProfilerOverlaySnapshot, contributors_are_named_and_sorted)
{
// A category made at run time, like the one the Lua API tracing_category makes.
Category runtime_category("Overlay snapshot test category", false);
// make_complete_event() gives every event thread id 1.
FrameProfiler profiler(1);
// The outer scope runs for 10 ns and contains the inner scope, which runs for 3 ns.
trace_event outer = make_complete_event(runtime_category, 0, 10, 1, 4);
trace_event inner = make_complete_event(Physics, 2, 3, 2, 3);
profiler.processEvent(&outer);
profiler.processEvent(&inner);
profiler.processFrame();
const frame_overlay_snapshot& snapshot = profiler.getOverlaySnapshot();
EXPECT_TRUE(snapshot.valid);
EXPECT_EQ(snapshot.total_nanosec, 10u);
ASSERT_EQ(snapshot.top_contributors.size(), 2u);
// The largest self time comes first.
EXPECT_EQ(snapshot.top_contributors[0].name, SCP_string("Overlay snapshot test category"));
EXPECT_EQ(snapshot.top_contributors[0].self_nanosec, 7u);
EXPECT_EQ(snapshot.top_contributors[1].name, SCP_string(Physics.getName()));
EXPECT_EQ(snapshot.top_contributors[1].self_nanosec, 3u);
EXPECT_EQ(snapshot.other_nanosec, 0u);
}
// Not every Category is a global static: the Lua API tracing_category constructs one and copies it
// into a script-owned object, so the constructed object dies while its id stays in the trace
// events. The registry must keep the name alive on its own.
TEST(TracingCategory, name_outlives_the_category_object)
{
int id = -1;
{
Category temporary("Temporary test category", false);
id = temporary.getId();
// A copy keeps the id of the original, which is how the Lua object gets a usable id.
const Category copy = temporary;
EXPECT_EQ(copy.getId(), id);
}
EXPECT_EQ(Category::getNameById(id), SCP_string("Temporary test category"));
}
// Categories with the same name share an id, so a script that makes a category each frame does not
// make the registry grow.
TEST(TracingCategory, same_name_reuses_the_id)
{
Category first("Repeated test category", false);
const size_t count = Category::getCount();
Category second("Repeated test category", false);
EXPECT_EQ(second.getId(), first.getId());
EXPECT_EQ(Category::getCount(), count);
}
// Runs the real tracing system on the stub renderer, so complete::start() and complete::end() run.
// The stub renderer reports timestamp queries as supported, so scopes of GPU categories also make
// GPU queries. The fixture counts the queries.
class TracingProfilerToggleTest : public test::FSTestFixture {
public:
TracingProfilerToggleTest() : test::FSTestFixture(INIT_CFILE | INIT_GRAPHICS) {}
protected:
static int timestamp_query_count;
void SetUp() override {
test::FSTestFixture::SetUp();
_oldIsCapable = gr_screen.gf_is_capable;
_oldQueryValue = gr_screen.gf_query_value;
gr_screen.gf_is_capable = [](gr_capability capability) {
return capability == gr_capability::CAPABILITY_TIMESTAMP_QUERY;
};
gr_screen.gf_query_value = [](int, QueryType type) {
if (type == QueryType::Timestamp) {
++timestamp_query_count;
}
};
timestamp_query_count = 0;
tracing::init();
tracing::set_frame_profiling_enabled(false);
}
void TearDown() override {
tracing::set_frame_profiling_enabled(false);
tracing::shutdown();
gr_screen.gf_is_capable = _oldIsCapable;
gr_screen.gf_query_value = _oldQueryValue;
test::FSTestFixture::TearDown();
}
private:
decltype(gr_screen.gf_is_capable) _oldIsCapable = nullptr;
decltype(gr_screen.gf_query_value) _oldQueryValue = nullptr;
};
int TracingProfilerToggleTest::timestamp_query_count = 0;
// A scope that is open when the profiler turns on never started. Its end() must do nothing. Before
// the fix, Debug builds asserted and Release builds read a null category.
TEST_F(TracingProfilerToggleTest, enable_inside_open_scope)
{
{
TRACE_SCOPE(Physics);
tracing::set_frame_profiling_enabled(true);
}
ASSERT_TRUE(tracing::frame_profiling_active());
tracing::frame_profile_process_frame();
EXPECT_EQ(tracing::get_frame_profiler_overlay_snapshot().total_nanosec, 0u);
// A scope that starts after the profiler is on is recorded as usual.
{
TRACE_SCOPE(PostMove);
// The profiler drops events with a duration of zero.
const auto start = timer_get_nanoseconds();
while (timer_get_nanoseconds() == start) {
}
}
tracing::frame_profile_process_frame();
const auto& snapshot = tracing::get_frame_profiler_overlay_snapshot();
ASSERT_EQ(snapshot.top_contributors.size(), 1u);
EXPECT_EQ(snapshot.top_contributors[0].name, SCP_string(PostMove.getName()));
}
// A scope that is open when the profiler turns off must close without an error. If the scope
// started a GPU query, its end() must also make the GPU query that closes it.
TEST_F(TracingProfilerToggleTest, disable_inside_open_scope)
{
ASSERT_TRUE(DrawSceneTexture.usesGPUCounter());
tracing::set_frame_profiling_enabled(true);
const int queries_before = timestamp_query_count;
{
TRACE_SCOPE(DrawSceneTexture);
EXPECT_EQ(timestamp_query_count, queries_before + 1);
tracing::set_frame_profiling_enabled(false);
}
EXPECT_FALSE(tracing::frame_profiling_active());
EXPECT_EQ(timestamp_query_count, queries_before + 2);
}
+34
View File
@@ -24,6 +24,40 @@ TEST(HeapAllocatorTests, simpleAllocate) {
ASSERT_EQ((size_t)0, allocator.numAllocations());
}
TEST(HeapAllocatorTests, usedBytesAndHeapSize) {
HeapAllocator allocator(dummyResizer);
ASSERT_EQ((size_t)0, allocator.usedBytes());
ASSERT_GT(allocator.heapSize(), (size_t)0);
auto initialHeapSize = allocator.heapSize();
auto offsetA = allocator.allocate(200);
ASSERT_EQ((size_t)200, allocator.usedBytes());
auto offsetB = allocator.allocate(300);
ASSERT_EQ((size_t)500, allocator.usedBytes());
// Allocations within the initial heap size shouldn't have triggered a resize
ASSERT_EQ(initialHeapSize, allocator.heapSize());
allocator.free(offsetA);
ASSERT_EQ((size_t)300, allocator.usedBytes());
allocator.free(offsetB);
ASSERT_EQ((size_t)0, allocator.usedBytes());
// Force at least one resize and make sure usedBytes tracks the live allocation, not the
// (now larger) heap size
auto offsetC = allocator.allocate(30 * 1024 * 1024);
ASSERT_EQ((size_t)30 * 1024 * 1024, allocator.usedBytes());
ASSERT_GT(allocator.heapSize(), initialHeapSize);
ASSERT_GE(allocator.heapSize(), allocator.usedBytes());
allocator.free(offsetC);
ASSERT_EQ((size_t)0, allocator.usedBytes());
}
TEST(HeapAllocatorTests, manySmallAllocations) {
HeapAllocator allocator(dummyResizer);