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+ }
0 commit comments