Skip to content
Draft
Changes from 4 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
36 changes: 30 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{false};

static bool tickOnceSettingStorage;
static bool tickOnce;

static float tickRateLimitSettingStorage;
static float tickRateLimit;

Expand Down Expand Up @@ -63,25 +69,40 @@ uint64_t ScriptHostBase::HookedIdleSimulationCycle() {
// so it's very important that we read this now so as to avoid a potential division-by-zero.
uint64_t idealTime = idealSimulationCycleTime;

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

uint64_t now;

// 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))) {
if (!tickOnce) return previousSimulationCycleTime;
goto tickOnceBusyWait;
}

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.
tickOnceBusyWait:
if (tickOnce) {
while (frameSimulate.load() == false) {}
frameSimulate.store(false);
QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&now));
}

// 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 +157,8 @@ class SmoothPatchPrecise : public OptimizationPatch {

public:
SmoothPatchPrecise() : OptimizationPatch("SmoothPatchPrecise", nullptr) {
RegisterBoolSetting(&tickOnceSettingStorage, "tickOnce", false, "Tick only once per frame (couples tick-rate to framerate)");

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 +229,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 +245,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