@@ -553,12 +553,6 @@ std::array<bool, ImGuiKey_NamedKey_COUNT> s_pressedNamedKeyState = {};
553553}
554554
555555void EntityDebugger::UpdateKeyboardControls () {
556- // update mouse state depending on if the window is focused
557- ImGui::GetIO ().AddMouseButtonEvent (0 , GetAsyncKeyState (VK_LBUTTON) & 0x8000 );
558- ImGui::GetIO ().AddMouseButtonEvent (1 , GetAsyncKeyState (VK_RBUTTON) & 0x8000 );
559- ImGui::GetIO ().AddMouseButtonEvent (2 , GetAsyncKeyState (VK_MBUTTON) & 0x8000 );
560-
561-
562556 // capture keyboard input
563557 ImGui::GetIO ().KeyAlt = GetAsyncKeyState (VK_MENU) & 0x8000 ;
564558 ImGui::GetIO ().KeyCtrl = GetAsyncKeyState (VK_CONTROL) & 0x8000 ;
@@ -623,4 +617,142 @@ void EntityDebugger::UpdateKeyboardControls() {
623617 else if (!isBackspaceKeyDown && wasBackspaceKeyDown) {
624618 ImGui::GetIO ().AddKeyEvent (ImGuiKey_Backspace, false );
625619 }
620+ }
621+
622+
623+ void EntityDebugger::DrawFPSOverlay (RND_Renderer* renderer) {
624+ ImGui::SetNextWindowBgAlpha (0 .6f );
625+
626+ // Use DisplaySize/FramebufferScale so positioning matches the same coordinate space as the overlay.
627+ ImVec2 windowSize = ImGui::GetIO ().DisplaySize ;
628+ windowSize.x = windowSize.x / ImGui::GetIO ().DisplayFramebufferScale .x ;
629+ windowSize.y = windowSize.y / ImGui::GetIO ().DisplayFramebufferScale .y ;
630+
631+ const ImVec2 pad (10 .0f , 10 .0f );
632+ ImGui::SetNextWindowPos (ImVec2 (windowSize.x - pad.x , pad.y ), ImGuiCond_Always, ImVec2 (1 .0f , 0 .0f ));
633+
634+ if (ImGui::Begin (" AppMS Overlay" , nullptr , ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoMove)) {
635+ const float predictedDisplayPeriodMs = (float )renderer->GetPredictedDisplayPeriodMs ();
636+ const float predictedHz = predictedDisplayPeriodMs > 0 .0f ? (1000 .0f / predictedDisplayPeriodMs) : 0 .0f ;
637+
638+ const float appMs = (float )renderer->GetLastFrameTimeMs (); // Total frame time (includes wait)
639+ const float workMs = (float )renderer->GetLastFrameWorkTimeMs (); // GPU Work time only (excludes wait)
640+ const float waitMs = (float )renderer->GetLastWaitTimeMs ();
641+ const float overheadMs = (float )renderer->GetLastOverheadMs ();
642+
643+ // --- 2. Convert to FPS ---
644+ const float appFps = appMs > 0 .0000001f ? (1000 .0f / appMs) : 0 .0f ;
645+
646+ // "Theoretical FPS": How fast you COULD run if you didn't have to wait for V-Sync/OpenXR
647+ const float workFps = workMs >= 0 .0000001f ? (1000 .0f / workMs) : 0 .0f ;
648+
649+ // Calculate percentage of the frame budget used (still useful in % terms)
650+ const float workPct = predictedDisplayPeriodMs > 0 .0f ? (workMs / predictedDisplayPeriodMs) * 100 .0f : 0 .0f ;
651+
652+ // --- 3. Text Summary ---
653+ ImGui::Text (" Your headset is %.0f Hz" , predictedHz);
654+ ImGui::Text (" Currently Running At %.1f FPS" , appFps);
655+ ImGui::Text (" " );
656+ ImGui::Text (" OpenXR waited %.1f ms so that it can interpolate/have low latency." , waitMs);
657+ ImGui::Text (" Theoretically, it'd run at %.1f FPS if that didn't matter" , workFps);
658+
659+ if (predictedHz > 0 .0f && workFps > 0 .0f ) {
660+ auto rateForDivisor = [predictedHz](int divisor) -> double {
661+ return divisor > 0 ? (predictedHz / (double )divisor) : 0.0 ;
662+ };
663+
664+ auto chooseBestDivisor = [&](double fps) -> int {
665+ // Pick the closest *supported* refresh divisor (1x, 1/2x, 1/3x, 1/4x).
666+ int bestDiv = 1 ;
667+ double bestErr = std::abs (fps - rateForDivisor (1 ));
668+ for (int div = 2 ; div <= 4 ; ++div) {
669+ const double err = std::abs (fps - rateForDivisor (div));
670+ if (err < bestErr) {
671+ bestErr = err;
672+ bestDiv = div;
673+ }
674+ }
675+ return bestDiv;
676+ };
677+
678+ // Use theoretical FPS (GPU work time) as the basis for which step we're closest to.
679+ const int currentDiv = chooseBestDivisor (workFps);
680+ const double currentTarget = rateForDivisor (currentDiv);
681+ const int nextDiv = std::max (1 , currentDiv - 1 );
682+ const double nextTarget = rateForDivisor (nextDiv);
683+ const double missingNext = std::max (0.0 , nextTarget - (double )workFps);
684+
685+ if (currentDiv == 1 ) {
686+ ImGui::Text (" Its reaching the full refresh rate you've set (%.0f hz)" , currentTarget);
687+ ImGui::Text (" You've got ~%.1f FPS of headroom to spare" , (float )std::max (0.0 , (double )workFps - nextTarget));
688+ }
689+ else {
690+ ImGui::Text (" It is however able to reliably reach %.0f FPS (%.0f Hz / %d)" , currentTarget, predictedHz, currentDiv);
691+ ImGui::Text (" You'd need to get %.1f FPS more to get it to switch to %.0f FPS" , missingNext, nextTarget);
692+ }
693+ }
694+
695+ // --- 4. History Buffers (Storing FPS now) ---
696+ static float history_app_fps[120 ] = {};
697+ static float history_work_fps[120 ] = {};
698+ static int offset = 0 ;
699+
700+ history_app_fps[offset] = appFps;
701+ history_work_fps[offset] = workFps;
702+ offset = (offset + 1 ) % 120 ;
703+
704+ // --- 5. Plotting ---
705+ const double targetFps = predictedHz;
706+ const double halfFps = predictedHz / 2.0 ;
707+ const double thirdFps = predictedHz / 3.0 ;
708+ const double fourthFps = predictedHz / 4.0 ;
709+ if (ImPlot::BeginPlot (" ##Frametime" , ImVec2 (420 , 150 ), ImPlotFlags_NoFrame | ImPlotFlags_NoTitle | ImPlotFlags_NoMouseText | ImPlotFlags_NoMenus | ImPlotFlags_NoBoxSelect | ImPlotFlags_NoInputs)) {
710+ ImPlot::SetupAxes (nullptr , " ##FPS" , ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoInitialFit);
711+ ImPlot::SetupAxisLimits (ImAxis_X1, 0 , 120 , ImPlotCond_Always);
712+
713+ if (targetFps >= 0 .000000001f ) {
714+ ImPlot::SetupAxisLimits (ImAxis_Y1, 0.0 , predictedHz * 1 .5f , ImPlotCond_Always);
715+
716+ // 1. Target Refresh Rate (Green)
717+ ImPlot::SetNextLineStyle (ImVec4 (0 , 1 , 0 , 0 .5f ));
718+ ImPlot::PlotInfLines (" ##Target" , &targetFps, 1 , ImPlotInfLinesFlags_Horizontal);
719+ ImPlot::TagY (targetFps, ImVec4 (0 , 1 , 0 , 0 .5f ), " %.0f Hz" , targetFps);
720+
721+ // 2. Half Rate (ASW/Reprojection threshold) (Yellow)
722+ ImPlot::SetNextLineStyle (ImVec4 (1 , 1 , 0 , 0 .5f ));
723+ ImPlot::PlotInfLines (" ##1/2 Rate" , &halfFps, 1 , ImPlotInfLinesFlags_Horizontal);
724+ ImPlot::TagY (halfFps, ImVec4 (1 , 1 , 0 , 0 .5f ), " %.0f Hz" , halfFps);
725+
726+ // 3. Third Rate (Red)
727+ ImPlot::SetNextLineStyle (ImVec4 (1 , 0 , 0 , 0 .5f ));
728+ ImPlot::PlotInfLines (" ##1/3 Rate" , &thirdFps, 1 , ImPlotInfLinesFlags_Horizontal);
729+ }
730+ else {
731+ ImPlot::SetupAxisLimits (ImAxis_Y1, 0.0 , 144.0 , ImPlotCond_Always);
732+ }
733+
734+ // --- Draw Graphs ---
735+ // 1. Theoretical Max FPS (Work Time) - Purple/Pink
736+ ImPlot::SetNextLineStyle (ImVec4 (1 .0f , 0 .4f , 1 .0f , 1 .0f ));
737+ ImPlot::PlotLine (" Theoretical Max" , history_work_fps, 120 , 1.0 , 0.0 , 0 , offset);
738+
739+ // 2. Actual FPS (App Time) - Blue
740+ // This represents what is actually hitting the screen (capped by Wait).
741+ ImPlot::SetNextFillStyle (ImVec4 (0 .4f , 0 .4f , 1 .0f , 0 .50f ));
742+ ImPlot::SetNextLineStyle (ImVec4 (0 .4f , 0 .4f , 1 .0f , 1 .0f ));
743+ ImPlot::PlotShaded (" Actual" , history_app_fps, 120 , 0.0 , 1.0 , 0.0 , 0 , offset);
744+ ImPlot::PlotLine (" ##TotalLine" , history_app_fps, 120 , 1.0 , 0.0 , 0 , offset);
745+
746+ // Current FPS Tag
747+ if (appFps > 0 .0f ) {
748+ const double currentFps = appFps;
749+ ImPlot::SetNextLineStyle (ImVec4 (1 , 1 , 1 , 0 .65f ));
750+ ImPlot::PlotInfLines (" ##Current" , ¤tFps, 1 , ImPlotInfLinesFlags_Horizontal);
751+ ImPlot::TagY (currentFps, ImVec4 (1 , 1 , 1 , 0 .8f ), " %.1f FPS" , currentFps);
752+ }
753+
754+ ImPlot::EndPlot ();
755+ }
756+ }
757+ ImGui::End ();
626758}
0 commit comments