Skip to content

ImGui frame profiler overlay, and Vulkan per-draw descriptor set churn fix#7652

Open
The-E wants to merge 10 commits into
scp-fs2open:masterfrom
The-E:imgui-profiler
Open

ImGui frame profiler overlay, and Vulkan per-draw descriptor set churn fix#7652
The-E wants to merge 10 commits into
scp-fs2open:masterfrom
The-E:imgui-profiler

Conversation

@The-E

@The-E The-E commented Jul 24, 2026

Copy link
Copy Markdown
Member

This branch contains two related pieces of work: an in-game ImGui frame profiler
overlay, and a Vulkan descriptor-set optimisation that the overlay was used to
find and verify.

1. ImGui frame profiler overlay

Vendors ImPlot (lib/implot/, updated to the v1.1 WIP) and adds a runtime frame
profiler overlay on top of the existing tracing infrastructure:

  • code/tracing/ProfilerOverlay.{h,cpp} — the overlay itself: a frametime history
    plot plus a breakdown of the biggest per-category contributors to the frame.
  • FrameProfiler gains accumulate_self_times(), computing per-category
    exclusive (self) time for a frame's trace events in a single pass over the
    event stream, rather than building a tree and comparing strings. Split out so it
    can be unit-tested directly — see test/src/tracing/test_frame_profiler.cpp.
  • Toggleable at runtime via the Game.ProfilerOverlay option; -profile_frame_time
    still works and now seeds the same toggle.
  • Adds graphics/Vulkan debug counters (draw calls, descriptor sets and writes,
    pipeline count) to the overlay.

2. Vulkan: stop per-draw descriptor set churn

The Material and PerDraw descriptor sets were being 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.

The fix declares the bindings whose offset moves per draw
(MaterialBinding::ModelData, MaterialBinding::ShadowMapData,
PerDrawBinding::GenericData, PerDrawBinding::Matrices) as
eUniformBufferDynamic. DescriptorWriter::setBuffer 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. That part is required rather than an optimisation: 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.

Also switches 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 is deliberately left at 1024 — the per-frame pool
growth it used to log was a symptom of the churn, and one chunk now covers a dense
scene outright.

Measured

Solaris mission 21 (large asteroid field, ~1370 model draws/frame), RTX 4080 SUPER
at 1080p. Debug build, A/B on the same tree with and without the change:

before after
descriptor sets / frame 2566 250–345
descriptor writes / frame 19107 ~1700–2300
median frametime (2 runs each) 92.9 / 83.6 ms 72.0 / 66.5 ms
Render Buffer (overlay) 24.68 ms (30.8%) 12.73 ms (18.4%)
Build Shadow Map (overlay) 5.44 ms off the top-5 entirely
frame pool exhausted log lines every frame 0

Release builds already inlined most of this per-draw work away and are unchanged
within run-to-run noise, which is the expected result — Render Buffer costs only
~1.4 µs/draw there. Worth noting: neither Release run happened to reach the heavy

600-draw stretch of the mission, so Release behaviour at very high draw counts is
untested; the draw-count bins that do overlap show no regression.

Testing

  • Renders identically before/after at the same mission point (models, lighting,
    HUD, shadows) — no wrong-uniform corruption.
  • Run under -gr_sync_validation -gr_debug: the set of reported VUIDs is identical
    to the same build without the change. No descriptor or dynamic-offset VUIDs.
  • unittests 233/233 pass.
  • Compiles warning-clean with -DFSO_FATAL_WARNINGS=ON (GCC).
  • OpenGL is untouched by part 2.

@The-E
The-E force-pushed the imgui-profiler branch 2 times, most recently from 8687175 to 4020b5e Compare July 25, 2026 06:33
@notimaginative

Copy link
Copy Markdown
Contributor

This just renders a black screen for me on Mac. The one exception is that text renders normally, though only if the font is TTF, otherwise it too is black/missing.

No apparent errors or log info of note that I can see compared to a build from master that works fine.
fs2_open.log

I should also note that the lab appears to render normally. Until you load a model, at which point everything disappears except for the text.

@The-E The-E added enhancement A new feature or upgrade of an existing feature to add additional functionality. graphics A feature or issue related to graphics (2d and 3d) labels Jul 26, 2026
The-E and others added 7 commits July 26, 2026 21:22
Adds ImPlot as a build dependency, to be used for the upcoming ImGui
frame profiler overlay's frametime graph and pie chart.
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.
- 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.
- 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.
- 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.
…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 <noreply@anthropic.com>
- 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.
The-E and others added 3 commits July 26, 2026 21:44
- 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.
…dings

- 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.
@notimaginative

Copy link
Copy Markdown
Contributor

It's working for me now, though I had to clear the cached shaders first. I'd tried that previously without success.

The [MoltenVK]: VK_ERROR_FEATURE_NOT_PRESENT: Metal does not support disabling primitive restart. message is gone now as well. Doesn't show up in the logs or on the console.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement A new feature or upgrade of an existing feature to add additional functionality. graphics A feature or issue related to graphics (2d and 3d)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants