diff --git a/Common/GPU/OpenGL/GLFrameData.cpp b/Common/GPU/OpenGL/GLFrameData.cpp index fa5a051d30..07d3f4a2d9 100644 --- a/Common/GPU/OpenGL/GLFrameData.cpp +++ b/Common/GPU/OpenGL/GLFrameData.cpp @@ -1,3 +1,4 @@ + #include "Common/GPU/OpenGL/GLCommon.h" #include "Common/GPU/OpenGL/GLFrameData.h" #include "Common/GPU/OpenGL/GLRenderManager.h" diff --git a/Common/GPU/Vulkan/VulkanFrameData.h b/Common/GPU/Vulkan/VulkanFrameData.h index a4638bb618..b9d037cb23 100644 --- a/Common/GPU/Vulkan/VulkanFrameData.h +++ b/Common/GPU/Vulkan/VulkanFrameData.h @@ -8,6 +8,8 @@ #include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/Data/Collections/Hashmaps.h" +#define USE_PRESENT_WAIT 0 + enum { MAX_TIMESTAMP_QUERIES = 128, }; @@ -30,6 +32,7 @@ struct QueueProfileContext { double descWriteTime; }; + class VKRFramebuffer; struct ReadbackKey { diff --git a/Common/GPU/Vulkan/VulkanRenderManager.cpp b/Common/GPU/Vulkan/VulkanRenderManager.cpp index ff53b04ef9..44e2ed1baa 100644 --- a/Common/GPU/Vulkan/VulkanRenderManager.cpp +++ b/Common/GPU/Vulkan/VulkanRenderManager.cpp @@ -11,6 +11,7 @@ #include "Common/GPU/Vulkan/VulkanAlloc.h" #include "Common/GPU/Vulkan/VulkanContext.h" #include "Common/GPU/Vulkan/VulkanRenderManager.h" +#include "Common/GPU/Vulkan/VulkanFrameData.h" #include "Common/Thread/ThreadUtil.h" #include "Common/VR/PPSSPPVR.h" @@ -25,9 +26,6 @@ #define UINT64_MAX 0xFFFFFFFFFFFFFFFFULL #endif - -#define USE_PRESENT_WAIT 0 - using namespace PPSSPP_VK; // renderPass is an example of the "compatibility class" or RenderPassType type. diff --git a/Common/TimeUtil.cpp b/Common/TimeUtil.cpp index d7ed5631f3..e22b225828 100644 --- a/Common/TimeUtil.cpp +++ b/Common/TimeUtil.cpp @@ -149,6 +149,14 @@ void sleep_ms(int ms) { #endif } +void sleep_s(double s) { +#if defined(_WIN32) || defined(__EMSCRIPTEN__) || defined(HAVE_LIBNX) + sleep_ms((int)(s * 1000.0)); +#else + usleep((int)(s * 1000000)); +#endif +} + // Return the current time formatted as Minutes:Seconds:Milliseconds // in the form 00:00:000. void GetTimeFormatted(char formattedTime[13]) { diff --git a/Common/TimeUtil.h b/Common/TimeUtil.h index a0701ae097..49ab7789ae 100644 --- a/Common/TimeUtil.h +++ b/Common/TimeUtil.h @@ -16,6 +16,10 @@ double from_time_raw_relative(uint64_t raw_time); // Sleep. Does not necessarily have millisecond granularity, especially on Windows. void sleep_ms(int ms); +// Sleep with floating point second wait. Can have higher precision than millisections if the +// platform supports it. +void sleep_s(double ms); + // Yield. Signals that this thread is busy-waiting but wants to allow other hyperthreads to run. void yield(); diff --git a/Core/FrameTiming.cpp b/Core/FrameTiming.cpp index 8a9b10e63d..c61430eddb 100644 --- a/Core/FrameTiming.cpp +++ b/Core/FrameTiming.cpp @@ -94,3 +94,51 @@ Draw::PresentMode ComputePresentMode(Draw::DrawContext *draw, int *interval) { *interval = (mode == Draw::PresentMode::FIFO) ? 1 : 0; return mode; } + +void FrameTiming::BeforeCPUSlice() { + cpuSliceStartTime = time_now_d(); +} + +void FrameTiming::EndOfCPUSlice(float scaledTimeStep) { + _dbg_assert_(usePresentTiming); + + double now = time_now_d(); + + cpuTime = now - cpuSliceStartTime; + this->timeStep = scaledTimeStep; + + // Sync up lastPresentTime with the current time if it's way off. + if (lastPresentTime < now - 0.5f) { + lastPresentTime = now; + } +} + +void FrameTiming::BeforePresent() { + if (!usePresentTiming) + return; + + // Wait until we hit the next present time. Ideally we'll be fairly close here due to the previous AfterPresent wait. + nextPresentTime = lastPresentTime + this->timeStep; + while (true) { + double remaining = nextPresentTime - time_now_d(); + if (remaining <= 0.0) + break; + sleep_s(remaining); + } + + lastPresentTime = nextPresentTime; +} + +void FrameTiming::AfterPresent() { + // Sleep slightly less time than all of the available room, in case of a CPU spike. + // This should be a tweakable. + const double margin = 2.0 * 0.001; // 4 ms + postSleep = this->timeStep - margin - cpuTime; + + if (postSleep > 0.0) { + sleep_s(postSleep); + } + + if (!usePresentTiming) + return; +} diff --git a/Core/FrameTiming.h b/Core/FrameTiming.h index 04e938e898..c068e01f1e 100644 --- a/Core/FrameTiming.h +++ b/Core/FrameTiming.h @@ -13,7 +13,23 @@ struct FrameTiming { Draw::PresentMode presentMode; int presentInterval; + // The new timing method. + bool usePresentTiming; + + double cpuSliceStartTime; + double cpuTime; + double timeStep; + double postSleep; + + double lastPresentTime; + double nextPresentTime; + void Reset(Draw::DrawContext *draw); + + void BeforeCPUSlice(); + void EndOfCPUSlice(float scaledTimeStep); + void BeforePresent(); + void AfterPresent(); }; extern FrameTiming g_frameTiming; diff --git a/Core/HLE/sceDisplay.cpp b/Core/HLE/sceDisplay.cpp index 1112279ba6..f49353c936 100644 --- a/Core/HLE/sceDisplay.cpp +++ b/Core/HLE/sceDisplay.cpp @@ -454,6 +454,7 @@ static void DoFrameTiming(bool throttle, bool *skipFrame, float scaledTimestep) wasPaused = false; } +// This is used even with the old timing method, for now. static void DoFrameIdleTiming() { PROFILE_THIS_SCOPE("timing"); if (!FrameTimingThrottled() || !g_Config.bEnableSound || wasPaused) { @@ -602,6 +603,7 @@ void __DisplayFlip(int cyclesLate) { bool needFlip = fbDirty || noRecentFlip || postEffectRequiresFlip; if (!needFlip) { + // TODO: Maybe ignore this path and flip anyway, unless Skip Buffer Effects is on? // Okay, there's no new frame to draw, game might be sitting in a static loading screen // or similar, and not long enough to trigger noRecentFlip. But audio may be playing, so we need to time still. DoFrameIdleTiming(); @@ -617,8 +619,9 @@ void __DisplayFlip(int cyclesLate) { bool forceNoFlip = false; float refreshRate = System_GetPropertyFloat(SYSPROP_DISPLAY_REFRESH_RATE); + float frameTimingLimit = FrameTimingLimit(); // Avoid skipping on devices that have 58 or 59 FPS, except when alternate speed is set. - bool refreshRateNeedsSkip = FrameTimingLimit() != framerate && FrameTimingLimit() > refreshRate; + bool refreshRateNeedsSkip = frameTimingLimit != framerate && frameTimingLimit > refreshRate; // Alternative to frameskip fast-forward, where we draw everything. // Useful if skipping a frame breaks graphics or for checking drawing speed. if (fastForwardSkipFlip && (!FrameTimingThrottled() || refreshRateNeedsSkip)) { @@ -631,6 +634,30 @@ void __DisplayFlip(int cyclesLate) { } } + bool throttle = FrameTimingThrottled(); + int fpsLimit = FrameTimingLimit(); + float scaledTimestep = (float)numVBlanksSinceFlip * timePerVblank; + if (fpsLimit > 0 && fpsLimit != framerate) { + scaledTimestep *= (float)framerate / fpsLimit; + } + + // If the ideal case, use the new timing path. + g_frameTiming.usePresentTiming = g_Config.iFrameSkip == 0 && !refreshRateNeedsSkip; + + if (g_frameTiming.usePresentTiming) { + if (Core_NextFrame()) { + gpu->CopyDisplayToOutput(true); + DisplayFireActualFlip(); + g_frameTiming.EndOfCPUSlice(scaledTimestep); + gpuStats.numFlips++; + goto finishUp; + } else { + // This should only happen if we're stepping in the debugger. + // Go to the old path. + g_frameTiming.usePresentTiming = false; + } + } + // Setting CORE_NEXTFRAME (which Core_NextFrame does) causes a swap. const bool fbReallyDirty = gpu->FramebufferReallyDirty(); if (fbReallyDirty || noRecentFlip || postEffectRequiresFlip) { @@ -647,13 +674,6 @@ void __DisplayFlip(int cyclesLate) { gpuStats.numFlips++; } - bool throttle = FrameTimingThrottled(); - - int fpsLimit = FrameTimingLimit(); - float scaledTimestep = (float)numVBlanksSinceFlip * timePerVblank; - if (fpsLimit > 0 && fpsLimit != framerate) { - scaledTimestep *= (float)framerate / fpsLimit; - } bool skipFrame; DoFrameTiming(throttle, &skipFrame, scaledTimestep); @@ -680,6 +700,8 @@ void __DisplayFlip(int cyclesLate) { // Right after, we regain control for a little bit in hleAfterFlip. I think that's a great // place to do housekeeping. +finishUp: + CoreTiming::ScheduleEvent(0 - cyclesLate, afterFlipEvent, 0); numVBlanksSinceFlip = 0; diff --git a/UI/DebugOverlay.cpp b/UI/DebugOverlay.cpp index f48f1bf826..efa9467619 100644 --- a/UI/DebugOverlay.cpp +++ b/UI/DebugOverlay.cpp @@ -115,9 +115,16 @@ static void DrawFrameTiming(UIContext *ctx, const Bounds &bounds) { ctx->Draw()->SetFontScale(0.5f, 0.5f); snprintf(statBuf, sizeof(statBuf), - "Mode (interval): %s (%d)", + "Mode (interval): %s (%d)" + "CPU time: %0.1fms\n" + "Timestep: %0.1fms\n" + "Postsleep: %0.1fms\n", Draw::PresentModeToString(g_frameTiming.presentMode), - g_frameTiming.presentInterval); + g_frameTiming.presentInterval, + g_frameTiming.cpuTime * 1000.0, + g_frameTiming.timeStep * 1000.0, + g_frameTiming.postSleep * 1000.0 + ); ctx->Draw()->DrawTextRect(ubuntu24, statBuf, bounds.x + 10, bounds.y + 50, bounds.w - 20, bounds.h - 30, 0xFFFFFFFF, FLAG_DYNAMIC_ASCII); diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index db9b8a5518..74062aac47 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -1165,6 +1165,7 @@ void NativeFrame(GraphicsContext *graphicsContext) { g_screenManager->getUIContext()->SetTintSaturation(g_Config.fUITint, g_Config.fUISaturation); // All actual rendering happen in here. + g_frameTiming.BeforeCPUSlice(); g_screenManager->render(); if (g_screenManager->getUIContext()->Text()) { g_screenManager->getUIContext()->Text()->OncePerFrame(); @@ -1175,14 +1176,15 @@ void NativeFrame(GraphicsContext *graphicsContext) { g_draw->EndFrame(); - // This, between EndFrame and Present, is where we should actually wait to do present time management. - // There might not be a meaningful distinction here for all backends.. - if (renderCounter < 10 && ++renderCounter == 10) { // We're rendering fine, clear out failure info. ClearFailedGPUBackends(); } + // This, between EndFrame and Present, is where we should actually wait to do present time management. + // There might not be a meaningful distinction here for all backends.. + g_frameTiming.BeforePresent(); + int interval; Draw::PresentMode presentMode = ComputePresentMode(g_draw, &interval); g_draw->Present(presentMode, interval); @@ -1218,6 +1220,9 @@ void NativeFrame(GraphicsContext *graphicsContext) { // INFO_LOG(G3D, "Polling graphics context"); graphicsContext->Poll(); } + + // TODO: Possibly this should move elsewhere, depending on the platform. + g_frameTiming.AfterPresent(); } void HandleGlobalMessage(const std::string &msg, const std::string &value) {