-
Notifications
You must be signed in to change notification settings - Fork 4
Add option to only tick once per frame to Smooth Patch Precise #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
56151c2
4ea7208
67d0047
199fb0d
ec7b3cf
3eb0d8e
504a511
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,19 @@ | ||
| #include "../patch_system.h" | ||
| #include "../patch_helpers.h" | ||
| #include "../logger.h" | ||
| #include <atomic> | ||
| #include <bit> | ||
|
|
||
| #pragma comment(lib, "ntdll.lib") | ||
|
|
||
| extern "C" __declspec(dllimport) NTSTATUS __stdcall NtDelayExecution(BOOLEAN Alertable, LARGE_INTEGER* Interval); | ||
| extern "C" __declspec(dllimport) NTSTATUS __stdcall NtQueryTimerResolution(ULONG* MaximumTime, ULONG* MinimumTime, ULONG* CurrentTime); | ||
|
|
||
| static std::atomic<bool> frameSimulate{true}; | ||
|
|
||
| static bool tickOnceSettingStorage; | ||
| static bool tickOnce; | ||
|
|
||
| static float tickRateLimitSettingStorage; | ||
| static float tickRateLimit; | ||
|
|
||
|
|
@@ -58,30 +64,58 @@ uint64_t WaitUntilPrecisely(uint64_t time, uint64_t now) { | |
| return now; | ||
| } | ||
|
|
||
| uint64_t BusyWaitForFrame(ScriptHostBase* scriptHost) { | ||
| uint64_t now = 0; | ||
| uint64_t beginWaitTime = 0; | ||
| QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&beginWaitTime)); | ||
| now = beginWaitTime; | ||
| while (frameSimulate.exchange(false) == false) | ||
| { | ||
| if (*reinterpret_cast<const uint32_t*>((reinterpret_cast<uintptr_t>(scriptHost) + 0xc08)) != 1) break; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's at |
||
| if (!*reinterpret_cast<const uint8_t*>((reinterpret_cast<uintptr_t>(scriptHost) + 0xa60))) break; | ||
| // Fallback in case the frame signal takes too long. | ||
| // TODO: find a reliable way to detect if the game is trying to close therefore not rendering. | ||
| QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&now)); | ||
| double elapsed = (double)(now - beginWaitTime) / (double)performanceFrequency; | ||
| if (elapsed >= 1.0) { break; } | ||
|
Comment on lines
+79
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I'm reading this correctly, this code bails out of the wait if a second-or-more has elapsed? If so, the floating-point calculations could be elided like so: uint64_t elapsed = now - beginWaitTime;
if (elapsed >= performanceFrequency) { break; } |
||
| } | ||
| return now; | ||
| } | ||
|
|
||
| uint64_t ScriptHostBase::HookedIdleSimulationCycle() { | ||
| // Note that idealSimulationCycleTime may change during the sleep performed later on, | ||
| // so it's very important that we read this now so as to avoid a potential division-by-zero. | ||
| uint64_t idealTime = idealSimulationCycleTime; | ||
|
|
||
| uint64_t idealTimeForThisCycle = previousSimulationCycleTime + idealTime; | ||
|
|
||
| uint64_t now = 0; | ||
|
|
||
| // I don't know what the boolean at 0xa60 is, but the game's code skips sleeping if it's zero, | ||
| // so if it's zero we'll avoid sleeping. | ||
| if ((idealTime == 0) | !*reinterpret_cast<const uint8_t*>((reinterpret_cast<uintptr_t>(this) + 0xa60))) { return previousSimulationCycleTime; } | ||
|
|
||
| uint64_t idealTimeForThisCycle = previousSimulationCycleTime + idealTime; | ||
| if ((idealTime == 0) || !*reinterpret_cast<const uint8_t*>((reinterpret_cast<uintptr_t>(this) + 0xa60))) { | ||
| if (!tickOnce) return previousSimulationCycleTime; | ||
| return BusyWaitForFrame(this); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
| } | ||
|
|
||
| uint64_t now; | ||
| QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&now)); | ||
|
|
||
| // If we're on time, we'll wait for the ideal cycle-time to elapse. | ||
| if (now < idealTimeForThisCycle) { now = WaitUntilPrecisely(idealTimeForThisCycle, now); } | ||
|
|
||
| // Busy wait for render thread. | ||
| if (tickOnce) { now = BusyWaitForFrame(this); } | ||
|
|
||
| // We'll round down the time so that if we're running late the next cycle will occur earlier. | ||
| previousSimulationCycleTime = (now / idealTime) * idealTime; | ||
|
|
||
| return previousSimulationCycleTime; | ||
| } | ||
|
|
||
| void __stdcall DelayAfterFramePresentation(uintptr_t graphicsDeviceStructure) { | ||
| // Tell sim thread we're good to simulate. | ||
| if (tickOnce) frameSimulate.store(true); | ||
|
|
||
| // This might actually denote if the graphics device is lost instead, I'm not sure. | ||
| bool gameWindowIsNotForeground = *reinterpret_cast<const uint8_t*>(graphicsDeviceStructure + 0x8d); | ||
|
|
||
|
|
@@ -136,6 +170,8 @@ class SmoothPatchPrecise : public OptimizationPatch { | |
|
|
||
| public: | ||
| SmoothPatchPrecise() : OptimizationPatch("SmoothPatchPrecise", nullptr) { | ||
| RegisterBoolSetting(&tickOnceSettingStorage, "Tick at most once per frame", false, "Avoids ticking more than once per frame, potentially alleviating hitching?"); | ||
|
|
||
| RegisterFloatSetting(&tickRateLimitSettingStorage, "tickRateLimit", SettingUIType::InputBox, | ||
| 480.0f, // Most people will be using a 60 Hz display, so we default to a multiple of 60, | ||
| // 480 TPS should be fine for weaker processors. | ||
|
|
@@ -206,6 +242,7 @@ class SmoothPatchPrecise : public OptimizationPatch { | |
| lastError.clear(); | ||
| LOG_INFO("[SmoothPatchPrecise] Installing..."); | ||
|
|
||
| tickOnce = tickOnceSettingStorage; | ||
| tickRateLimit = tickRateLimitSettingStorage; | ||
| frameRateLimit = frameRateLimitSettingStorage; | ||
| frameRateLimitInactive = frameRateLimitInactiveSettingStorage < 0.0f ? frameRateLimit : frameRateLimitInactiveSettingStorage; | ||
|
|
@@ -221,9 +258,9 @@ class SmoothPatchPrecise : public OptimizationPatch { | |
|
|
||
| UpdateTimerFrequency(); | ||
|
|
||
| LOG_INFO(std::format("[SmoothPatchPrecise] tickRateLimit: {}; frameRateLimit: {}; frameRateLimitInactive: {}; performanceFrequency: {}; idealSimulationCycleTime: {}; idealPresentationFrameTime: {}; " | ||
| LOG_INFO(std::format("[SmoothPatchPrecise] tickOnce: {}; tickRateLimit: {}; frameRateLimit: {}; frameRateLimitInactive: {}; performanceFrequency: {}; idealSimulationCycleTime: {}; idealPresentationFrameTime: {}; " | ||
| "idealPresentationFrameInactiveTime: {}; timerResolution: {}, timerFrequency: {}; qpcToHectonanosecondsMultiplier: {}; hectonanosecondsToQPCMultiplier: {}", | ||
| tickRateLimit, frameRateLimit, frameRateLimitInactive, performanceFrequency, idealSimulationCycleTime, idealPresentationFrameTime, idealPresentationFrameInactiveTime, timerResolution, timerFrequency, | ||
| tickOnce, tickRateLimit, frameRateLimit, frameRateLimitInactive, performanceFrequency, idealSimulationCycleTime, idealPresentationFrameTime, idealPresentationFrameInactiveTime, timerResolution, timerFrequency, | ||
| qpcToHectonanosecondsMultiplier, hectonanosecondsToQPCMultiplier)); | ||
|
|
||
| auto idleSimulationCycleCallAddress = idleSimulationCycleCallAddressInfo.Resolve(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be on the safe side, it might be worthwhile to set this to true also in the
Installmethod?In case the user disables
tickOncewhileframeSimulateis false, and then re-enables it.