mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-13 09:39:33 +01:00
Add a "Limit framerate to 30 fps" setting, and a compat flag for it
Many games drop to 30fps when they miss a vblank, and on the PSP some of them nearly always do. In PPSSPP the GE is fast enough that they spend much of their time at 60 instead, and some things misbehave (Midnight Club). With the limit on, the GE takes until just after the vblank two on from the one the previous frame was done in, so the frame goes up every other vblank whether the game flips as soon as it's done, waits for a vblank first, or latches the flip. A new frame starts at the first list after a flip, or at a list that starts after a vblank, for games that enqueue their lists from the vblank interrupt and never flip (Midnight Club: LA Remix). Auto applies it to games with the LimitFrameRateTo30 compat flag, the two Midnight Club games for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1549195eb
commit
8fbc3adfed
11 files changed
+109
No files matched your search
@@ -113,6 +113,7 @@ void Compatibility::CheckSettings(IniFile &iniFile, const std::string &gameID) {
|
||||
CheckSetting(iniFile, gameID, "ForceMax60FPS", &flags_.ForceMax60FPS);
|
||||
CheckSetting(iniFile, gameID, "GoWFramerateHack60", &flags_.GoWFramerateHack60);
|
||||
CheckSetting(iniFile, gameID, "FramerateHack30", &flags_.FramerateHack30);
|
||||
CheckSetting(iniFile, gameID, "LimitFrameRateTo30", &flags_.LimitFrameRateTo30);
|
||||
CheckSetting(iniFile, gameID, "HideISOFiles", &flags_.HideISOFiles);
|
||||
CheckSetting(iniFile, gameID, "ForceSoftwareRenderer", &flags_.ForceSoftwareRenderer);
|
||||
CheckSetting(iniFile, gameID, "DarkStalkersPresentHack", &flags_.DarkStalkersPresentHack);
|
||||
|
||||
@@ -79,6 +79,7 @@ struct CompatFlags {
|
||||
bool ForceMax60FPS;
|
||||
bool GoWFramerateHack60;
|
||||
bool FramerateHack30;
|
||||
bool LimitFrameRateTo30;
|
||||
bool HideISOFiles;
|
||||
bool ForceSoftwareRenderer;
|
||||
bool DarkStalkersPresentHack;
|
||||
|
||||
@@ -899,6 +899,7 @@ static constexpr const char *androidHwScaleNames[] = { "Native device resolution
|
||||
static constexpr const char *frameSkipNames[] = { "Off", "1", "2", "3", "4" };
|
||||
static constexpr const char *skipGPUReadbackModeNames[] = { "No", "Skip", "Copy to texture" };
|
||||
static constexpr const char *depthRasterModeNames[] = { "Auto", "Low", "Off", "Always on" };
|
||||
static constexpr const char *frameRateLimit30Names[] = { "Auto", "Always", "Off" };
|
||||
static constexpr const char *bloomHackNames[] = { "Off", "Safe", "Balanced", "Aggressive" };
|
||||
static constexpr const char *inflightFramesNames[] = { "No buffer", "Up to 1", "Up to 2" };
|
||||
static constexpr const char *texScalingTypeNames[] = { "xBRZ", "Hybrid", "Bicubic", "Hybrid + Bicubic" };
|
||||
@@ -1043,6 +1044,8 @@ static constexpr ConfigSetting graphicsSettings[] = {
|
||||
.Label(I18NCat::GRAPHICS, "Buffer graphics commands").Choices(inflightFramesNames, 1),
|
||||
ConfigSetting("RenderDuplicateFrames", SETTING(g_Config, bRenderDuplicateFrames), false, CfgFlag::PER_GAME)
|
||||
.Label(I18NCat::GRAPHICS, "Render duplicate frames to 60hz"),
|
||||
ConfigSetting("LimitFrameRateTo30", SETTING(g_Config, iLimitFrameRateTo30), (int)FrameRateLimit30::AUTO, CfgFlag::PER_GAME | CfgFlag::REPORT)
|
||||
.Label(I18NCat::GRAPHICS, "Limit framerate to 30 fps").Choices(frameRateLimit30Names),
|
||||
|
||||
ConfigSetting("MultiThreading", SETTING(g_Config, bRenderMultiThreading), true, CfgFlag::DEFAULT)
|
||||
.Label(I18NCat::DEVELOPER, "Multi-threaded rendering"),
|
||||
|
||||
@@ -425,6 +425,7 @@ public:
|
||||
bool bGfxDebugOutput;
|
||||
int iInflightFrames;
|
||||
bool bRenderDuplicateFrames;
|
||||
int iLimitFrameRateTo30; // FrameRateLimit30
|
||||
bool bRenderMultiThreading;
|
||||
|
||||
// HW debug
|
||||
|
||||
@@ -301,6 +301,12 @@ enum class DebugOverlay : int {
|
||||
JIT_STATS,
|
||||
};
|
||||
|
||||
enum class FrameRateLimit30 : int {
|
||||
AUTO = 0, // Games with the LimitFrameRateTo30 compat flag.
|
||||
ALWAYS = 1,
|
||||
OFF = 2,
|
||||
};
|
||||
|
||||
// Android-only for now
|
||||
enum class DisplayFramerateMode : int {
|
||||
DEFAULT,
|
||||
|
||||
@@ -172,6 +172,14 @@ static int lastFlipsTooFrequent = 0;
|
||||
static u64 lastFlipCycles = 0;
|
||||
static u64 nextFlipCycles = 0;
|
||||
|
||||
// For limiting the frame rate to 30fps. Not saved: after a load, pacing starts over.
|
||||
static u64 lastVblankTicks = 0;
|
||||
// When the GE was done with the last frame, paced or not. 0 when there's none yet.
|
||||
static u64 pacedFrameDoneTicks = 0;
|
||||
static bool flippedSincePacedFrame = false;
|
||||
// How far past a vblank a paced frame is done.
|
||||
static const double PACE_MARGIN_MS = 1.0;
|
||||
|
||||
void hleEnterVblank(u64 userdata, int cyclesLate);
|
||||
void hleLeaveVblank(u64 userdata, int cyclesLate);
|
||||
static void hleVblankWake(u64 userdata, int cyclesLate);
|
||||
@@ -227,6 +235,9 @@ void __DisplayInit() {
|
||||
lastFlipsTooFrequent = 0;
|
||||
lastFlipCycles = 0;
|
||||
nextFlipCycles = 0;
|
||||
lastVblankTicks = 0;
|
||||
pacedFrameDoneTicks = 0;
|
||||
flippedSincePacedFrame = false;
|
||||
wasPaused = false;
|
||||
|
||||
enterVblankEvent = CoreTiming::RegisterEvent("EnterVBlank", &hleEnterVblank);
|
||||
@@ -351,6 +362,9 @@ void __DisplayDoState(PointerWrap &p) {
|
||||
if (p.mode == p.MODE_READ) {
|
||||
// Not saved. Start counting again rather than carry over the session before the load.
|
||||
lastFlipsTooFrequent = 0;
|
||||
lastVblankTicks = 0;
|
||||
pacedFrameDoneTicks = 0;
|
||||
flippedSincePacedFrame = false;
|
||||
}
|
||||
|
||||
gpu->DoState(p);
|
||||
@@ -588,6 +602,8 @@ void hleEnterVblank(u64 userdata, int cyclesLate) {
|
||||
|
||||
VERBOSE_LOG(Log::sceDisplay, "Enter VBlank %i", vbCount);
|
||||
|
||||
lastVblankTicks = CoreTiming::GetTicks(currentMIPS) - cyclesLate;
|
||||
|
||||
__PowerCheckLockedClock();
|
||||
|
||||
DisplayFireVblankStart();
|
||||
@@ -1031,6 +1047,10 @@ int sceDisplaySetFramebuf(MIPSState *mips, u32 topaddr, int linesize, int pixelf
|
||||
|
||||
__DisplaySetFramebuf(topaddr, linesize, pixelformat, sync);
|
||||
|
||||
if (topaddr != 0) {
|
||||
flippedSincePacedFrame = true;
|
||||
}
|
||||
|
||||
// No delaying while inside an interrupt. It'll cause idle threads to starve.
|
||||
if (delayCycles > 0 && !__IsInInterrupt()) {
|
||||
// Okay, the game is going at too high a frame rate. God of War and Fat Princess both do this.
|
||||
@@ -1046,6 +1066,44 @@ int sceDisplaySetFramebuf(MIPSState *mips, u32 topaddr, int linesize, int pixelf
|
||||
}
|
||||
}
|
||||
|
||||
// The start of the vblank that ticks falls in (it can be in the future, for a list the GE isn't done with).
|
||||
static s64 VblankAtOrBefore(u64 ticks, s64 frameCycles) {
|
||||
s64 offset = (s64)(ticks - lastVblankTicks);
|
||||
s64 frames = offset >= 0 ? offset / frameCycles : -((-offset + frameCycles - 1) / frameCycles);
|
||||
return (s64)lastVblankTicks + frames * frameCycles;
|
||||
}
|
||||
|
||||
bool __DisplayLimitTo30FPS() {
|
||||
switch ((FrameRateLimit30)g_Config.iLimitFrameRateTo30) {
|
||||
case FrameRateLimit30::ALWAYS: return true;
|
||||
case FrameRateLimit30::OFF: return false;
|
||||
default: return PSP_CoreParameter().compat.flags().LimitFrameRateTo30;
|
||||
}
|
||||
}
|
||||
|
||||
u64 __DisplayPaceFrameEnd(u64 listStartTicks, u64 doneTicks) {
|
||||
if (lastVblankTicks == 0) {
|
||||
return doneTicks;
|
||||
}
|
||||
const s64 frameCycles = msToCycles(frameMs);
|
||||
// The first list after a flip starts a new frame. Some games (Midnight Club) never flip during
|
||||
// gameplay, and enqueue each frame's list from the vblank interrupt instead, so a list that starts
|
||||
// after a vblank does too. Other lists are part of the frame before.
|
||||
if (pacedFrameDoneTicks != 0 && !flippedSincePacedFrame && (s64)listStartTicks < VblankAtOrBefore(pacedFrameDoneTicks, frameCycles) + frameCycles) {
|
||||
return doneTicks;
|
||||
}
|
||||
u64 endTicks = doneTicks;
|
||||
if (pacedFrameDoneTicks != 0) {
|
||||
// Just after the vblank two on from the one the previous frame was done in. Whether the game flips
|
||||
// as soon as it's done or waits for a vblank, latched or not, the flip lands every other vblank.
|
||||
const s64 paceTicks = VblankAtOrBefore(pacedFrameDoneTicks, frameCycles) + 2 * frameCycles + msToCycles(PACE_MARGIN_MS);
|
||||
endTicks = std::max(endTicks, (u64)paceTicks);
|
||||
}
|
||||
pacedFrameDoneTicks = endTicks;
|
||||
flippedSincePacedFrame = false;
|
||||
return endTicks;
|
||||
}
|
||||
|
||||
bool __DisplayGetFramebuf(PSPPointer<u8> *topaddr, u32 *linesize, GEBufferFormat *pixelFormat, int latchedMode) {
|
||||
const FrameBufferState &fbState = latchedMode == PSP_DISPLAY_SETBUF_NEXTFRAME ? latchedFramebuf : framebuf;
|
||||
if (topaddr != nullptr)
|
||||
|
||||
@@ -31,6 +31,12 @@ void Register_sceDisplay();
|
||||
bool __DisplayGetFramebuf(PSPPointer<u8> *topaddr, u32 *linesize, GEBufferFormat *pixelFormat, int mode);
|
||||
void __DisplaySetFramebuf(u32 topaddr, int linesize, int pixelformat, int sync);
|
||||
|
||||
// Whether to limit the frame rate to 30fps, from the setting and the compat flag.
|
||||
bool __DisplayLimitTo30FPS();
|
||||
// For that: when the GE should be done with a list that started at listStartTicks and would be done
|
||||
// at doneTicks, so its frame goes up every other vblank. Returns doneTicks or later.
|
||||
u64 __DisplayPaceFrameEnd(u64 listStartTicks, u64 doneTicks);
|
||||
|
||||
// Call this when resuming to avoid a small speedup burst
|
||||
void __DisplaySetWasPaused();
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "Core/HLE/ErrorCodes.h"
|
||||
#include "Core/HLE/sceKernelMemory.h"
|
||||
#include "Core/HLE/sceKernelInterrupt.h"
|
||||
#include "Core/HLE/sceDisplay.h"
|
||||
#include "Core/HLE/sceGe.h"
|
||||
#include "Core/HLE/scePower.h"
|
||||
#include "Core/Util/PPGeDraw.h"
|
||||
@@ -996,6 +997,9 @@ DLResult GPUCommon::ProcessDLQueue() {
|
||||
if (!list.started && list.context.IsValid()) {
|
||||
gstate.Save(list.context);
|
||||
}
|
||||
if (!list.started) {
|
||||
paceListStartTicks_ = startingTicks + cyclesExecuted;
|
||||
}
|
||||
list.started = true;
|
||||
|
||||
gstate_c.offsetAddr = list.offsetAddr;
|
||||
@@ -1426,6 +1430,14 @@ void GPUCommon::Execute_End(u32 op, u32 diff) {
|
||||
currentList->subIntrToken = prev & 0xFFFF;
|
||||
UpdateState(GPUSTATE_DONE);
|
||||
|
||||
if (__DisplayLimitTo30FPS()) {
|
||||
// Make the GE look slow enough that the frame misses the next vblank. Games that drop
|
||||
// to 30fps when they can't make 60 then do. Everything that follows (interrupt, list
|
||||
// and draw sync, busy time) is timed from here.
|
||||
const u64 doneTicks = startingTicks + cyclesExecuted;
|
||||
cyclesExecuted += (int)(__DisplayPaceFrameEnd(paceListStartTicks_, doneTicks) - doneTicks);
|
||||
}
|
||||
|
||||
if (currentList->interruptsEnabled && __GeTriggerInterrupt(currentList->id, currentList->pc, startingTicks + cyclesExecuted)) {
|
||||
// The context is restored once the finish callback has run, which gets to see the state
|
||||
// the list left behind. Nothing else runs before then, see InterruptEnd().
|
||||
@@ -2083,6 +2095,9 @@ void GPUCommon::DoState(PointerWrap &p) {
|
||||
Do(p, isbreak);
|
||||
Do(p, drawCompleteTicks);
|
||||
Do(p, busyTicks);
|
||||
if (p.mode == PointerWrap::MODE_READ) {
|
||||
paceListStartTicks_ = 0;
|
||||
}
|
||||
|
||||
if (s >= 5) {
|
||||
Do(p, matrixVisible.all);
|
||||
|
||||
@@ -430,6 +430,8 @@ protected:
|
||||
u64 startingTicks;
|
||||
u32 cycleLastPC;
|
||||
int cyclesExecuted;
|
||||
// For the 30fps limit: when the last list started. Not saved.
|
||||
u64 paceListStartTicks_ = 0;
|
||||
|
||||
bool resumingFromDebugBreak_ = false;
|
||||
bool dumpNextFrame_ = false;
|
||||
|
||||
@@ -317,6 +317,7 @@ void BuildGraphicsSettings(SettingsPage *page) {
|
||||
if (KeyMap::InputMappingsFromPspButton(VIRTKEY_SPEED_ANALOG, nullptr, true)) {
|
||||
page->Setting(&g_Config.iAnalogAltSpeedPercent);
|
||||
}
|
||||
page->Setting(&g_Config.iLimitFrameRateTo30).Hint({I18NCat::GRAPHICS, "LimitFrameRateTo30 Tip", "Some games run better if limited to 30 fps. Auto is recommended, Always can cause problems in some games."});
|
||||
|
||||
page->Header();
|
||||
page->Link({I18NCat::GRAPHICS, "Speed Hacks"}, I18NCat::GRAPHICS, &BuildSpeedHacksPage).Hint({I18NCat::GRAPHICS, "SpeedHacksHint", "These settings can cause rendering errors"});
|
||||
|
||||
@@ -2320,3 +2320,18 @@ ULJM05395 = true
|
||||
ULJM05884 = true
|
||||
NPJH50827 = true
|
||||
ULET00417 = true
|
||||
|
||||
[LimitFrameRateTo30]
|
||||
# Makes each frame's GE work finish just after the vblank following the previous flip, as if the GE
|
||||
# were slower. Games that fall back to 30fps when they miss a vblank then run at 30fps throughout,
|
||||
# like they mostly do on hardware, instead of jumping between 30 and 60.
|
||||
# Midnight Club: LA Remix
|
||||
ULUS10383 = true
|
||||
ULES01144 = true
|
||||
ULJS00180 = true
|
||||
ULJS00267 = true
|
||||
ULJM05904 = true
|
||||
NPJH50440 = true
|
||||
# Midnight Club 3: DUB Edition
|
||||
ULUS10021 = true
|
||||
ULES00108 = true
|
||||
Reference in new issue
Block a user