1
0
mirror of https://github.com/djhackersdev/bemanitools.git synced 2025-02-26 22:49:28 +01:00

feat: Add frame time performance graph imgui overlay

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.
This commit is contained in:
icex2 2025-02-07 22:41:37 +01:00
parent 1800f75f3d
commit aa3e7d307d
6 changed files with 365 additions and 0 deletions

View File

@ -161,6 +161,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

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 \

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;
}

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

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);
}

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