Skip to content

Commit ea29b44

Browse files
authored
feat: Add frame time performance graph imgui overlay (#319)
A overlay window showing the a frame time graph with the current frame time, ~10 seconds of history as well as reference lines for the current avg. and a configurable target frame time. This is considered a debug tool to support in any efforts that are related to understanding current frame times of the games. More specifically this can be used to asses impact of any bemanitools hooking to the game’s main (render) loop.
1 parent 3090b9a commit ea29b44

6 files changed

Lines changed: 365 additions & 0 deletions

File tree

Module.mk

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ include src/main/iidxio/Module.mk
162162
include src/main/iidxiotest/Module.mk
163163
include src/main/imgui/Module.mk
164164
include src/main/imgui-bt/Module.mk
165+
include src/main/imgui-debug/Module.mk
165166
include src/main/inject/Module.mk
166167
include src/main/jbio-magicbox/Module.mk
167168
include src/main/jbio-p4io/Module.mk

src/main/imgui-debug/Module.mk

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
libs += imgui-debug
2+
3+
libs_imgui-debug := \
4+
imgui-bt \
5+
imgui \
6+
util \
7+
8+
src_imgui-debug := \
9+
frame-perf-graph.c \
10+
time-history.c \
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#include <math.h>
2+
3+
#include "imgui-bt/cimgui.h"
4+
5+
#include "imgui-debug/frame-perf-graph.h"
6+
#include "imgui-debug/time-history.h"
7+
8+
#include "util/log.h"
9+
10+
typedef struct imgui_debug_frame_perf_graph {
11+
float target_time_ms;
12+
float y_axis_min_time_ms;
13+
float y_axis_max_time_ms;
14+
} imgui_debug_frame_perf_graph_t;
15+
16+
static const ImVec2 WINDOW_MIN_SIZE = {320, 240};
17+
18+
static imgui_debug_time_history_t _imgui_debug_frame_perf_graph_history;
19+
static imgui_debug_frame_perf_graph_t _imgui_debug_frame_perf_graph;
20+
21+
static void _imgui_debug_frame_perf_graph_draw(
22+
imgui_debug_frame_perf_graph_t *graph,
23+
const imgui_debug_time_history_t *history)
24+
{
25+
float current_value;
26+
ImVec2 window_size;
27+
static bool show_labels = true;
28+
static bool show_target_line = true;
29+
static bool show_avg_line = true;
30+
31+
log_assert(history);
32+
33+
current_value = imgui_debug_time_history_recent_value_get(history);
34+
35+
igSetNextWindowSize(WINDOW_MIN_SIZE, ImGuiCond_FirstUseEver);
36+
igSetNextWindowSizeConstraints(WINDOW_MIN_SIZE, igGetIO()->DisplaySize, NULL, NULL);
37+
38+
igBegin("Frame Time Graph", NULL, ImGuiWindowFlags_MenuBar);
39+
40+
// Add menu bar
41+
if (igBeginMenuBar()) {
42+
if (igBeginMenu("Settings", true)) {
43+
igPushItemWidth(110);
44+
45+
float min_time_slider = graph->y_axis_min_time_ms;
46+
float max_time_slider = graph->y_axis_max_time_ms;
47+
float target_time_input = graph->target_time_ms;
48+
49+
if (igDragFloat("y-axis min time (ms)", &min_time_slider, 0.1f, 0.1f, max_time_slider - 0.1f, "%.1f", ImGuiSliderFlags_None)) {
50+
graph->y_axis_min_time_ms = min_time_slider;
51+
}
52+
53+
if (igDragFloat("y-axis max time (ms)", &max_time_slider, 0.1f, min_time_slider + 0.1f, 100.0f, "%.1f", ImGuiSliderFlags_None)) {
54+
graph->y_axis_max_time_ms = max_time_slider;
55+
}
56+
57+
if (igInputFloat("Target time reference (ms)", &target_time_input, 0.0f, 0.0f, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)) {
58+
if (target_time_input >= 0.1f && target_time_input <= 100.0f) {
59+
graph->target_time_ms = target_time_input;
60+
} else {
61+
target_time_input = graph->target_time_ms;
62+
}
63+
}
64+
65+
igCheckbox("Show reference line labels", &show_labels);
66+
igCheckbox("Show target reference line", &show_target_line);
67+
igCheckbox("Show average reference line", &show_avg_line);
68+
69+
if (igButton("Focus on average", (ImVec2){0, 0})) {
70+
// Convert +/- 10 fps around average to milliseconds
71+
float avg_fps = 1000.0f / history->avg_time_ms;
72+
graph->y_axis_min_time_ms = 1000.0f / (avg_fps + 10.0f);
73+
graph->y_axis_max_time_ms = 1000.0f / fmaxf(avg_fps - 10.0f, 1.0f);
74+
}
75+
76+
igSameLine(0, -1);
77+
78+
if (igButton("Focus on target", (ImVec2){0, 0})) {
79+
// Convert +/- 10 fps around target to milliseconds
80+
float target_fps = 1000.0f / graph->target_time_ms;
81+
graph->y_axis_min_time_ms = 1000.0f / (target_fps + 10.0f);
82+
graph->y_axis_max_time_ms = 1000.0f / fmaxf(target_fps - 10.0f, 1.0f);
83+
}
84+
85+
igPopItemWidth();
86+
igEndMenu();
87+
}
88+
igEndMenuBar();
89+
}
90+
91+
igGetContentRegionAvail(&window_size);
92+
93+
igPushStyleColor_Vec4(ImGuiCol_Text, (ImVec4){1, 1, 0, 1});
94+
igText("Now %.3f ms", current_value);
95+
igPopStyleColor(1);
96+
igSameLine(0, -1);
97+
igPushStyleColor_Vec4(ImGuiCol_Text, (ImVec4){1, 0, 0, 1});
98+
igText("Target %.3f ms", graph->target_time_ms);
99+
igPopStyleColor(1);
100+
igPushStyleColor_Vec4(ImGuiCol_Text, (ImVec4){0, 1, 0, 1});
101+
igText("Avg %.3f ms", history->avg_time_ms);
102+
igPopStyleColor(1);
103+
igSameLine(0, -1);
104+
igText(" Min %.3f ms Max %.3f ms", history->min_time_ms, history->max_time_ms);
105+
106+
// Setup plot area using actual window size, with extra space at top for "ms" label
107+
ImVec2 plot_pos;
108+
igGetCursorScreenPos(&plot_pos);
109+
plot_pos.y += 15; // Add space at top for "ms" label
110+
ImVec2 plot_size = {window_size.x - 50, window_size.y - 65}; // Adjusted for extra top space
111+
112+
// Plot frame times in ms
113+
ImVec2 plot_pos_offset = {plot_pos.x + 50, plot_pos.y};
114+
igSetCursorScreenPos(plot_pos_offset);
115+
116+
igPlotLines_FloatPtr("##framegraph",
117+
history->time_values_ms,
118+
history->size,
119+
history->current_index,
120+
"",
121+
graph->y_axis_min_time_ms,
122+
graph->y_axis_max_time_ms,
123+
plot_size,
124+
sizeof(float));
125+
126+
// Draw Y axis (ms)
127+
ImDrawList* draw_list = igGetWindowDrawList();
128+
char y_label[32];
129+
ImDrawList_AddLine(draw_list,
130+
(ImVec2){plot_pos.x + 50, plot_pos.y},
131+
(ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y},
132+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), 1.0f);
133+
134+
// Draw "ms" label at top of y-axis
135+
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, plot_pos.y - 15},
136+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), "ms", NULL);
137+
138+
// Scale number of reference points based on plot height
139+
int num_reference_points = (int)(plot_size.y / 25); // One point per ~40 pixels
140+
if (num_reference_points < 4) num_reference_points = 4;
141+
142+
float time_min = graph->y_axis_min_time_ms;
143+
float time_max = graph->y_axis_max_time_ms;
144+
float time_step = (time_max - time_min) / (num_reference_points + 1);
145+
146+
// Draw min time value at top of y-axis and reference line
147+
snprintf(y_label, sizeof(y_label), "%.2f", time_min);
148+
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, plot_pos.y + plot_size.y - 10},
149+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), y_label, NULL);
150+
ImDrawList_AddLine(draw_list,
151+
(ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y},
152+
(ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y + plot_size.y},
153+
igColorConvertFloat4ToU32((ImVec4){1,1,1,0.3f}), 1.0f);
154+
155+
// Draw reference points and lines on side of y-axis
156+
for (int i = 1; i <= num_reference_points; i++) {
157+
float value = time_max - (time_step * i);
158+
float normalized_pos = (time_max - value) / (time_max - time_min);
159+
float y_pos = plot_pos.y + (plot_size.y * normalized_pos);
160+
snprintf(y_label, sizeof(y_label), "%.2f", value);
161+
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, y_pos - 5},
162+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), y_label, NULL);
163+
ImDrawList_AddLine(draw_list,
164+
(ImVec2){plot_pos.x + 50, y_pos},
165+
(ImVec2){plot_pos.x + plot_size.x + 50, y_pos},
166+
igColorConvertFloat4ToU32((ImVec4){1,1,1,0.2f}), 1.0f);
167+
}
168+
169+
// Draw max time value at bottom of y-axis and reference line
170+
snprintf(y_label, sizeof(y_label), "%.2f", time_max);
171+
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 5, plot_pos.y},
172+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), y_label, NULL);
173+
ImDrawList_AddLine(draw_list,
174+
(ImVec2){plot_pos.x + 50, plot_pos.y},
175+
(ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y},
176+
igColorConvertFloat4ToU32((ImVec4){1,1,1,0.3f}), 1.0f);
177+
178+
// Draw target frame time reference line if within plot area
179+
if (show_target_line && graph->target_time_ms >= time_min && graph->target_time_ms <= time_max) {
180+
float normalized_target = (time_max - graph->target_time_ms) / (time_max - time_min);
181+
float y_pos_target = plot_pos.y + (plot_size.y * normalized_target);
182+
ImDrawList_AddLine(draw_list,
183+
(ImVec2){plot_pos.x + 50, y_pos_target},
184+
(ImVec2){plot_pos.x + plot_size.x + 50, y_pos_target},
185+
igColorConvertFloat4ToU32((ImVec4){1,0,0,1.0f}), 1.0f);
186+
if (show_labels) {
187+
snprintf(y_label, sizeof(y_label), "%.3f", graph->target_time_ms);
188+
ImDrawList_AddText_Vec2(draw_list,
189+
(ImVec2){plot_pos.x + 50 + plot_size.x/2 - 10, y_pos_target + 5},
190+
igColorConvertFloat4ToU32((ImVec4){1,0,0,1}), y_label, NULL);
191+
}
192+
}
193+
194+
// Draw reference line for current average if within plot area
195+
if (show_avg_line && history->avg_time_ms >= time_min && history->avg_time_ms <= time_max) {
196+
float normalized_avg = (time_max - history->avg_time_ms) / (time_max - time_min);
197+
float y_pos_avg = plot_pos.y + (plot_size.y * normalized_avg);
198+
ImDrawList_AddLine(draw_list,
199+
(ImVec2){plot_pos.x + 50, y_pos_avg},
200+
(ImVec2){plot_pos.x + plot_size.x + 50, y_pos_avg},
201+
igColorConvertFloat4ToU32((ImVec4){0,1,0,1.0f}), 1.0f);
202+
if (show_labels) {
203+
snprintf(y_label, sizeof(y_label), "%.3f", history->avg_time_ms);
204+
ImDrawList_AddText_Vec2(draw_list,
205+
(ImVec2){plot_pos.x + 50 + plot_size.x/2 - 10, y_pos_avg + 5},
206+
igColorConvertFloat4ToU32((ImVec4){0,1,0,1}), y_label, NULL);
207+
}
208+
}
209+
210+
// Draw X axis (time in frames)
211+
ImDrawList_AddLine(draw_list,
212+
(ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y},
213+
(ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y + plot_size.y},
214+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), 1.0f);
215+
216+
// Draw "frames" label centered on x-axis
217+
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 50 + (plot_size.x / 2) - 20, plot_pos.y + plot_size.y + 5},
218+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), "frames ago", NULL);
219+
220+
char x_label[32];
221+
snprintf(x_label, sizeof(x_label), "%d", history->size);
222+
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + 50, plot_pos.y + plot_size.y + 5},
223+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), x_label, NULL);
224+
ImDrawList_AddText_Vec2(draw_list, (ImVec2){plot_pos.x + plot_size.x + 50, plot_pos.y + plot_size.y + 5},
225+
igColorConvertFloat4ToU32((ImVec4){1,1,1,1}), "0", NULL);
226+
227+
igEnd();
228+
}
229+
230+
static void _imgui_debug_frame_perf_graph_component_frame_update(ImGuiContext *ctx)
231+
{
232+
ImGuiIO *io;
233+
234+
log_assert(ctx);
235+
236+
igSetCurrentContext(ctx);
237+
io = igGetIO();
238+
239+
imgui_debug_time_history_update(&_imgui_debug_frame_perf_graph_history, 1000.0f / io->Framerate);
240+
241+
_imgui_debug_frame_perf_graph_draw(&_imgui_debug_frame_perf_graph, &_imgui_debug_frame_perf_graph_history);
242+
}
243+
244+
void imgui_debug_frame_perf_graph_init(
245+
float target_fps,
246+
imgui_bt_component_t *component)
247+
{
248+
log_assert(target_fps > 0.0f);
249+
log_assert(component);
250+
251+
imgui_debug_time_history_init(ceilf(10 * target_fps), &_imgui_debug_frame_perf_graph_history);
252+
253+
_imgui_debug_frame_perf_graph.target_time_ms = 1000.0f / target_fps;
254+
_imgui_debug_frame_perf_graph.y_axis_min_time_ms = 1000.0f / fmaxf(0.0f, target_fps - 20.0f);
255+
_imgui_debug_frame_perf_graph.y_axis_max_time_ms = 1000.0f / fminf(target_fps + 20.0f, 1000.0f);
256+
257+
component->frame_update = _imgui_debug_frame_perf_graph_component_frame_update;
258+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#ifndef IMGUI_DEBUG_FRAME_PERF_GRAPH_H
2+
#define IMGUI_DEBUG_FRAME_PERF_GRAPH_H
3+
4+
#include "imgui-bt/component.h"
5+
6+
void imgui_debug_frame_perf_graph_init(
7+
float target_fps,
8+
imgui_bt_component_t *component);
9+
10+
#endif
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#include <stdlib.h>
2+
#include <string.h>
3+
4+
#include "time-history.h"
5+
6+
#include "util/log.h"
7+
#include "util/mem.h"
8+
9+
void imgui_debug_time_history_init(uint32_t size, imgui_debug_time_history_t *history)
10+
{
11+
log_assert(size > 0);
12+
log_assert(history);
13+
14+
memset(history, 0, sizeof(imgui_debug_time_history_t));
15+
16+
history->size = size;
17+
history->time_values_ms = (float *) xmalloc(size * sizeof(float));
18+
}
19+
20+
void imgui_debug_time_history_update(imgui_debug_time_history_t *history, float time_ms)
21+
{
22+
log_assert(history);
23+
24+
history->time_values_ms[history->current_index] = time_ms;
25+
history->current_index = (history->current_index + 1) % history->size;
26+
27+
history->min_time_ms = history->time_values_ms[0];
28+
history->max_time_ms = history->time_values_ms[0];
29+
history->avg_time_ms = 0;
30+
31+
for (uint32_t i = 0; i < history->size; i++) {
32+
if (history->time_values_ms[i] < history->min_time_ms) {
33+
history->min_time_ms = history->time_values_ms[i];
34+
}
35+
36+
if (history->time_values_ms[i] > history->max_time_ms) {
37+
history->max_time_ms = history->time_values_ms[i];
38+
}
39+
40+
history->avg_time_ms += history->time_values_ms[i];
41+
}
42+
43+
history->avg_time_ms /= history->size;
44+
}
45+
46+
float imgui_debug_time_history_recent_value_get(const imgui_debug_time_history_t *history)
47+
{
48+
log_assert(history);
49+
50+
if (history->current_index == 0) {
51+
return history->time_values_ms[history->size - 1];
52+
} else {
53+
return history->time_values_ms[history->current_index - 1];
54+
}
55+
}
56+
57+
void imgui_debug_time_history_free(imgui_debug_time_history_t *history)
58+
{
59+
log_assert(history);
60+
log_assert(history->time_values_ms);
61+
62+
free(history->time_values_ms);
63+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#ifndef IMGUI_DEBUG_TIME_HISTORY_H
2+
#define IMGUI_DEBUG_TIME_HISTORY_H
3+
4+
#include <stdint.h>
5+
6+
typedef struct imgui_debug_time_history {
7+
uint32_t size;
8+
float *time_values_ms;
9+
uint32_t current_index;
10+
float min_time_ms;
11+
float max_time_ms;
12+
float avg_time_ms;
13+
} imgui_debug_time_history_t;
14+
15+
void imgui_debug_time_history_init(uint32_t size, imgui_debug_time_history_t *history);
16+
17+
void imgui_debug_time_history_update(imgui_debug_time_history_t *history, float time_ms);
18+
19+
float imgui_debug_time_history_recent_value_get(const imgui_debug_time_history_t *history);
20+
21+
void imgui_debug_time_history_free(imgui_debug_time_history_t *history);
22+
23+
#endif

0 commit comments

Comments
 (0)