Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions patches/smooth_patch_precise.cpp
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};
Copy link
Contributor

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 Install method?
In case the user disables tickOnce while frameSimulate is false, and then re-enables it.


static bool tickOnceSettingStorage;
static bool tickOnce;

static float tickRateLimitSettingStorage;
static float tickRateLimit;

Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's at 0xc08?

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
Copy link
Contributor

@just-harry just-harry Mar 12, 2026

Choose a reason for hiding this comment

The 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If BusyWaitForFrame was defined as an instance method of ScriptHostBase, this could be a tail-call; a very minor optimisation, albeit.

}

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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down