feat: Add frame time performance graph imgui overlay #319

Merged
icex2 merged 2 commits from pr319 into master 2025-02-09 01:21:27 +03:00
6 changed files with 365 additions and 0 deletions
+1
View File
@@ -162,6 +162,7 @@ include src/main/iidxio/Module.mk
include src/main/iidxiotest/Module.mk
include src/main/imgui/Module.mk
include src/main/imgui-bt/Module.mk
include src/main/imgui-debug/Module.mk
include src/main/inject/Module.mk
include src/main/jbio-magicbox/Module.mk
include src/main/jbio-p4io/Module.mk
+10
View File
@@ -0,0 +1,10 @@
libs += imgui-debug
libs_imgui-debug := \
imgui-bt \
imgui \
util \
src_imgui-debug := \
frame-perf-graph.c \
time-history.c \
+258
View File
@@ -0,0 +1,258 @@
#include <math.h>
#include "imgui-bt/cimgui.h"
#include "imgui-debug/frame-perf-graph.h"
#include "imgui-debug/time-history.h"
#include "util/log.h"
typedef struct imgui_debug_frame_perf_graph {
float target_time_ms;
float y_axis_min_time_ms;
float y_axis_max_time_ms;
} imgui_debug_frame_perf_graph_t;
static const ImVec2 WINDOW_MIN_SIZE = {320, 240};
static imgui_debug_time_history_t _imgui_debug_frame_perf_graph_history;
static imgui_debug_frame_perf_graph_t _imgui_debug_frame_perf_graph;
static void _imgui_debug_frame_perf_graph_draw(
imgui_debug_frame_perf_graph_t *graph,
const imgui_debug_time_history_t *history)
{
float current_value;
ImVec2 window_size;
static bool show_labels = true;
static bool show_target_line = true;
static bool show_avg_line = true;
log_assert(history);
current_value = imgui_debug_time_history_recent_value_get(history);
igSetNextWindowSize(WINDOW_MIN_SIZE, ImGuiCond_FirstUseEver);
igSetNextWindowSizeConstraints(WINDOW_MIN_SIZE, igGetIO()->DisplaySize, NULL, NULL);
igBegin("Frame Time Graph", NULL, ImGuiWindowFlags_MenuBar);
// Add menu bar
if (igBeginMenuBar()) {
if (igBeginMenu("Settings", true)) {
igPushItemWidth(110);
float min_time_slider = graph->y_axis_min_time_ms;
float max_time_slider = graph->y_axis_max_time_ms;
float target_time_input = graph->target_time_ms;
if (igDragFloat("y-axis min time (ms)", &min_time_slider, 0.1f, 0.1f, max_time_slider - 0.1f, "%.1f", ImGuiSliderFlags_None)) {
graph->y_axis_min_time_ms = min_time_slider;
}
if (igDragFloat("y-axis max time (ms)", &max_time_slider, 0.1f, min_time_slider + 0.1f, 100.0f, "%.1f", ImGuiSliderFlags_None)) {
graph->y_axis_max_time_ms = max_time_slider;
}
if (igInputFloat("Target time reference (ms)", &target_time_input, 0.0f, 0.0f, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)) {
if (target_time_input >= 0.1f && target_time_input <= 100.0f) {
graph->target_time_ms = target_time_input;
} else {
target_time_input = graph->target_time_ms;
}
}
igCheckbox("Show reference line labels", &show_labels);
igCheckbox("Show target reference line", &show_target_line);
igCheckbox("Show average reference line", &show_avg_line);
if (igButton("Focus on average", (ImVec2){0, 0})) {
// Convert +/- 10 fps around average to milliseconds
float avg_fps = 1000.0f / history->avg_time_ms;
graph->y_axis_min_time_ms = 1000.0f / (avg_fps + 10.0f);
graph->y_axis_max_time_ms = 1000.0f / fmaxf(avg_fps - 10.0f, 1.0f);
}
igSameLine(0, -1);
if (igButton("Focus on target", (ImVec2){0, 0})) {
// Convert +/- 10 fps around target to milliseconds
float target_fps = 1000.0f / graph->target_time_ms;
graph->y_axis_min_time_ms = 1000.0f / (target_fps + 10.0f);
graph->y_axis_max_time_ms = 1000.0f / fmaxf(target_fps - 10.0f, 1.0f);
}
igPopItemWidth();
igEndMenu();
}
igEndMenuBar();
}
igGetContentRegionAvail(&window_size);
igPushStyleColor_Vec4(ImGuiCol_Text, (ImVec4){1, 1, 0, 1});
igText("Now %.3f ms", current_value);
igPopStyleColor(1);
igSameLine(0, -1);
igPushStyleColor_Vec4(ImGuiCol_Text, (ImVec4){1, 0, 0, 1});
igText("Target %.3f ms", graph->target_time_ms);
igPopStyleColor(1);
igPushStyleColor_Vec4(ImGuiCol_Text, (ImVec4){0, 1, 0, 1});
igText("Avg %.3f ms", history->avg_time_ms);
igPopStyleColor(1);
igSameLine(0, -1);
igText(" Min %.3f ms Max %.3f ms", history->min_time_ms, history->max_time_ms);
// Setup plot area using actual window size, with extra space at top for "ms" label
ImVec2 plot_pos;
igGetCursorScreenPos(&plot_pos);
plot_pos.y += 15; // Add space at top for "ms" label
ImVec2 plot_size = {window_size.x - 50, window_size.y - 65}; // Adjusted for extra top space
// Plot frame times in ms
ImVec2 plot_pos_offset = {plot_pos.x + 50, plot_pos.y};
igSetCursorScreenPos(plot_pos_offset);
igPlotLines_FloatPtr("##framegraph",
history->time_values_ms,
history->size,
history->current_index,
"",
graph->y_axis_min_time_ms,
graph->y_axis_max_time_ms,
plot_size,
sizeof(float));
// Draw Y axis (ms)
ImDrawList* draw_list = igGetWindowDrawList();
char y_label[32];
ImDrawList_AddLine(draw_list,
(ImVec2){plot_pos.x + 50, plot_pos.y},
(ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), 1.0f);
// Draw "ms" label at top of y-axis
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, plot_pos.y - 15},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), "ms", NULL);
// Scale number of reference points based on plot height
int num_reference_points = (int)(plot_size.y / 25); // One point per ~40 pixels
if (num_reference_points < 4) num_reference_points = 4;
float time_min = graph->y_axis_min_time_ms;
float time_max = graph->y_axis_max_time_ms;
float time_step = (time_max - time_min) / (num_reference_points + 1);
// Draw min time value at top of y-axis and reference line
snprintf(y_label, sizeof(y_label), "%.2f", time_min);
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, plot_pos.y + plot_size.y - 10},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), y_label, NULL);
ImDrawList_AddLine(draw_list,
(ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y},
(ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y + plot_size.y},
igColorConvertFloat4ToU32((ImVec4){1,1,1,0.3f}), 1.0f);
// Draw reference points and lines on side of y-axis
for (int i = 1; i <= num_reference_points; i++) {
float value = time_max - (time_step * i);
float normalized_pos = (time_max - value) / (time_max - time_min);
float y_pos = plot_pos.y + (plot_size.y * normalized_pos);
snprintf(y_label, sizeof(y_label), "%.2f", value);
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, y_pos - 5},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), y_label, NULL);
ImDrawList_AddLine(draw_list,
(ImVec2){plot_pos.x + 50, y_pos},
(ImVec2){plot_pos.x + plot_size.x + 50, y_pos},
igColorConvertFloat4ToU32((ImVec4){1,1,1,0.2f}), 1.0f);
}
// Draw max time value at bottom of y-axis and reference line
snprintf(y_label, sizeof(y_label), "%.2f", time_max);
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, plot_pos.y},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), y_label, NULL);
ImDrawList_AddLine(draw_list,
(ImVec2){plot_pos.x + 50, plot_pos.y},
(ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y},
igColorConvertFloat4ToU32((ImVec4){1,1,1,0.3f}), 1.0f);
// Draw target frame time reference line if within plot area
if (show_target_line && graph->target_time_ms >= time_min && graph->target_time_ms <= time_max) {
float normalized_target = (time_max - graph->target_time_ms) / (time_max - time_min);
float y_pos_target = plot_pos.y + (plot_size.y * normalized_target);
ImDrawList_AddLine(draw_list,
(ImVec2){plot_pos.x + 50, y_pos_target},
(ImVec2){plot_pos.x + plot_size.x + 50, y_pos_target},
igColorConvertFloat4ToU32((ImVec4){1,0,0,1.0f}), 1.0f);
if (show_labels) {
snprintf(y_label, sizeof(y_label), "%.3f", graph->target_time_ms);
ImDrawList_AddText_Vec2(draw_list,
(ImVec2){plot_pos.x + 50 + plot_size.x/2 - 10, y_pos_target + 5},
igColorConvertFloat4ToU32((ImVec4){1,0,0,1}), y_label, NULL);
}
}
// Draw reference line for current average if within plot area
if (show_avg_line && history->avg_time_ms >= time_min && history->avg_time_ms <= time_max) {
float normalized_avg = (time_max - history->avg_time_ms) / (time_max - time_min);
float y_pos_avg = plot_pos.y + (plot_size.y * normalized_avg);
ImDrawList_AddLine(draw_list,
(ImVec2){plot_pos.x + 50, y_pos_avg},
(ImVec2){plot_pos.x + plot_size.x + 50, y_pos_avg},
igColorConvertFloat4ToU32((ImVec4){0,1,0,1.0f}), 1.0f);
if (show_labels) {
snprintf(y_label, sizeof(y_label), "%.3f", history->avg_time_ms);
ImDrawList_AddText_Vec2(draw_list,
(ImVec2){plot_pos.x + 50 + plot_size.x/2 - 10, y_pos_avg + 5},
igColorConvertFloat4ToU32((ImVec4){0,1,0,1}), y_label, NULL);
}
}
// Draw X axis (time in frames)
ImDrawList_AddLine(draw_list,
(ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y},
(ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y + plot_size.y},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), 1.0f);
// Draw "frames" label centered on x-axis
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 50 + (plot_size.x / 2) - 20, plot_pos.y + plot_size.y + 5},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), "frames ago", NULL);
char x_label[32];
snprintf(x_label, sizeof(x_label), "%d", history->size);
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y + 5},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), x_label, NULL);
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y + plot_size.y + 5},
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), "0", NULL);
igEnd();
}
static void _imgui_debug_frame_perf_graph_component_frame_update(ImGuiContext *ctx)
{
ImGuiIO *io;
log_assert(ctx);
igSetCurrentContext(ctx);
io = igGetIO();
imgui_debug_time_history_update(&_imgui_debug_frame_perf_graph_history, 1000.0f / io->Framerate);
_imgui_debug_frame_perf_graph_draw(&_imgui_debug_frame_perf_graph, &_imgui_debug_frame_perf_graph_history);
}
void imgui_debug_frame_perf_graph_init(
float target_fps,
imgui_bt_component_t *component)
{
log_assert(target_fps > 0.0f);
log_assert(component);
imgui_debug_time_history_init(ceilf(10 * target_fps), &_imgui_debug_frame_perf_graph_history);
_imgui_debug_frame_perf_graph.target_time_ms = 1000.0f / target_fps;
_imgui_debug_frame_perf_graph.y_axis_min_time_ms = 1000.0f / fmaxf(0.0f, target_fps - 20.0f);
_imgui_debug_frame_perf_graph.y_axis_max_time_ms = 1000.0f / fminf(target_fps + 20.0f, 1000.0f);
component->frame_update = _imgui_debug_frame_perf_graph_component_frame_update;
}
+10
View File
@@ -0,0 +1,10 @@
#ifndef IMGUI_DEBUG_FRAME_PERF_GRAPH_H
#define IMGUI_DEBUG_FRAME_PERF_GRAPH_H
#include "imgui-bt/component.h"
void imgui_debug_frame_perf_graph_init(
float target_fps,
imgui_bt_component_t *component);
#endif
+63
View File
@@ -0,0 +1,63 @@
#include <stdlib.h>
#include <string.h>
#include "time-history.h"
#include "util/log.h"
#include "util/mem.h"
void imgui_debug_time_history_init(uint32_t size, imgui_debug_time_history_t *history)
{
log_assert(size > 0);
log_assert(history);
memset(history, 0, sizeof(imgui_debug_time_history_t));
history->size = size;
history->time_values_ms = (float *) xmalloc(size * sizeof(float));
}
void imgui_debug_time_history_update(imgui_debug_time_history_t *history, float time_ms)
{
log_assert(history);
history->time_values_ms[history->current_index] = time_ms;
history->current_index = (history->current_index + 1) % history->size;
history->min_time_ms = history->time_values_ms[0];
history->max_time_ms = history->time_values_ms[0];
history->avg_time_ms = 0;
for (uint32_t i = 0; i < history->size; i++) {
if (history->time_values_ms[i] < history->min_time_ms) {
history->min_time_ms = history->time_values_ms[i];
}
if (history->time_values_ms[i] > history->max_time_ms) {
history->max_time_ms = history->time_values_ms[i];
}
history->avg_time_ms += history->time_values_ms[i];
}
history->avg_time_ms /= history->size;
}
float imgui_debug_time_history_recent_value_get(const imgui_debug_time_history_t *history)
{
log_assert(history);
if (history->current_index == 0) {
return history->time_values_ms[history->size - 1];
} else {
return history->time_values_ms[history->current_index - 1];
}
}
void imgui_debug_time_history_free(imgui_debug_time_history_t *history)
{
log_assert(history);
log_assert(history->time_values_ms);
free(history->time_values_ms);
}
+23
View File
@@ -0,0 +1,23 @@
#ifndef IMGUI_DEBUG_TIME_HISTORY_H
#define IMGUI_DEBUG_TIME_HISTORY_H
#include <stdint.h>
typedef struct imgui_debug_time_history {
uint32_t size;
float *time_values_ms;
uint32_t current_index;
float min_time_ms;
float max_time_ms;
float avg_time_ms;
} imgui_debug_time_history_t;
void imgui_debug_time_history_init(uint32_t size, imgui_debug_time_history_t *history);
void imgui_debug_time_history_update(imgui_debug_time_history_t *history, float time_ms);
float imgui_debug_time_history_recent_value_get(const imgui_debug_time_history_t *history);
void imgui_debug_time_history_free(imgui_debug_time_history_t *history);
#endif