mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -10:00
- 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]>
455 lines
15 KiB
C++
455 lines
15 KiB
C++
//
|
|
//
|
|
|
|
#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
|