// // #include "ProfilerOverlay.h" #include "tracing.h" #include "graphics/2d.h" // ImPlot's Plot*() functions are templates that get instantiated here (unlike ImGui's ordinary // function API), and their header-inline ImPool::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 "backends/imgui_impl_sdl3.h" #include #include namespace tracing { namespace { constexpr size_t HISTORY_SIZE = 300; // ~5s at 60 FPS constexpr double NANOSEC_PER_MS = 1'000'000.0; SCP_vector History_ms; void push_history(float frame_ms) { History_ms.push_back(frame_ms); if (History_ms.size() > HISTORY_SIZE) { History_ms.erase(History_ms.begin()); } } float history_average() { if (History_ms.empty()) { return 0.0f; } float sum = std::accumulate(History_ms.begin(), History_ms.end(), 0.0f); return sum / static_cast(History_ms.size()); } float history_median() { if (History_ms.empty()) { return 0.0f; } SCP_vector sorted_copy(History_ms.begin(), History_ms.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() { if (History_ms.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(History_ms.size()), ImPlotCond_Always); ImPlot::PlotLine("frametime", History_ms.data(), static_cast(History_ms.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) { uint32_t hash = 2166136261u; // FNV-1a for (const char* p = name; *p != '\0'; ++p) { hash ^= static_cast(*p); hash *= 16777619u; } const float hue = static_cast(hash % 360) / 360.0f; return ImGui::ColorConvertFloat4ToU32(static_cast(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 (top 5 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; } constexpr int MAX_SEGMENTS = 6; // top 5 contributors + "Other" 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(snapshot.total_nanosec); auto add_segment = [&](const char* name, uint64_t self_nanosec, ImU32 color) { segments[count].name = name; segments[count].pct = 100.0 * static_cast(self_nanosec) / total; segments[count].ms = static_cast(static_cast(self_nanosec) / NANOSEC_PER_MS); segments[count].color = color; count++; }; for (const auto& contributor : snapshot.top_contributors) { if (count >= MAX_SEGMENTS) { break; } add_segment(contributor.name.c_str(), contributor.self_nanosec, color_for_name(contributor.name.c_str())); } if (snapshot.other_nanosec > 0 && count < MAX_SEGMENTS) { 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(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); } } } } // namespace void profiler_overlay_record_frame() { const frame_overlay_snapshot& snapshot = get_frame_profiler_overlay_snapshot(); if (!snapshot.valid) { return; } push_history(static_cast(static_cast(snapshot.total_nanosec) / NANOSEC_PER_MS)); } void profiler_overlay_draw() { gr_imgui_new_frame(); ImGui_ImplSDL3_NewFrame(); ImGui::NewFrame(); ImGui::SetNextWindowPos(ImVec2(20, 20), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_FirstUseEver); ImGui::Begin("Frame Profiler", nullptr, ImGuiWindowFlags_NoFocusOnAppearing); if (!frame_profiling_active() || History_ms.empty()) { ImGui::TextUnformatted("Collecting data..."); } else { float avg_ms = history_average(); float median_ms = history_median(); 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(); ImGui::Separator(); draw_frame_budget_bar(get_frame_profiler_overlay_snapshot()); } draw_graphics_debug_stats(); ImGui::End(); ImGui::Render(); gr_imgui_render_draw_data(); } } // namespace tracing