Skip to content
Open
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
98 changes: 87 additions & 11 deletions code/components/gta-core-five/src/PatchVehiclePhysicsFrameRate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <mutex>
#include <unordered_map>

#include "CrossBuildRuntime.h"

static bool g_wheelFpsFixEnabled = false;

static constexpr float kPhysicsStep = 1.0f / 60.0f;
Expand All @@ -33,15 +35,27 @@ struct IntegrationState
// Last step's output force; skipped frames report this held value
float lastOutput[4] = { 0.0f, 0.0f, 0.0f, 0.0f };

// m_Force/m_Torque deltas the last run added to a rigid collider; skipped frames re-apply them
float heldForce[8] = {};
uint32_t heldForceFrame = 0;

// Audio/vfx slip readouts (eff/fwd/side)
float slipTarget[kMaxIntegrationWheels][3] = {};
float slipDisplay[kMaxIntegrationWheels][3] = {};

bool hasRunOnce = false;
uint32_t lastFrame = 0;
};

constexpr uint32_t kSlipFieldOffsets[3] = { 0, 8, 12 };
}

static uint32_t g_wheelCompressionOffset;

static char kColliderTypeOffset;
static uint32_t kColliderForceOffset;
static uint32_t kWheelDynamicFlagsOffset;
static uint32_t g_wheelEffSlipOffset;

static std::mutex g_stateMutex;
static std::unordered_map<void*, IntegrationState> g_state;
Expand Down Expand Up @@ -97,6 +111,48 @@ static void ProcessIntegrationTask(void* collider, void* internalForce, void* in
locals->outputVel[2] = state->lastOutput[2];
locals->outputVel[3] = state->lastOutput[3];

// Re-apply the last run's force so the step's impulse spreads across the window
if ((frameCount - state->heldForceFrame) <= 4 && *reinterpret_cast<const int*>(static_cast<const char*>(collider) + kColliderTypeOffset) == 0)
{
auto forceAccum = reinterpret_cast<float*>(static_cast<char*>(collider) + kColliderForceOffset);

for (int lane = 0; lane < 8; lane++)
{
if (lane != 3 && lane != 7)
{
forceAccum[lane] += state->heldForce[lane];
}
}
}

auto wheels = static_cast<char* const*>(ppWheels);
int trackedWheels = std::min(numWheels, kMaxIntegrationWheels);

// Contact-flag shuffle the skipped run would have done (WF_HIT_PREV = WF_HIT, WF_HIT
// cleared); stale WF_HIT makes the still-vehicle contact filter reject ground contacts
for (int i = 0; i < trackedWheels; i++)
{
auto flags = reinterpret_cast<uint32_t*>(wheels[i] + kWheelDynamicFlagsOffset);
uint32_t value = *flags;
*flags = (value & ~3u) | ((value & 1u) << 1);
}

// Chase the slip readouts toward the last runs values
if (state->hasRunOnce)
{
float alpha = std::min(timeStep / (2.0f * kPhysicsStep), 1.0f);

for (int i = 0; i < trackedWheels; i++)
{
for (int j = 0; j < 3; j++)
{
float& display = state->slipDisplay[i][j];
display += (state->slipTarget[i][j] - display) * alpha;
*reinterpret_cast<float*>(wheels[i] + g_wheelEffSlipOffset + kSlipFieldOffsets[j]) = display;
}
}
}

return;
}

Expand All @@ -112,14 +168,14 @@ static void ProcessIntegrationTask(void* collider, void* internalForce, void* in
{
auto compression = reinterpret_cast<float*>(wheels[i] + g_wheelCompressionOffset);
float trueDelta = compression[0] - state->lastCompression[i];
compression[1] = compression[0] - trueDelta * (kPhysicsStep / state->sinceLastRun);
float damperDelta = trueDelta * (kPhysicsStep / state->sinceLastRun);

compression[1] = compression[0] - damperDelta;
}
}

// Rigid colliders only accumulate force into m_Force/m_Torque; the simulator
// later integrates it with the real frame dt, so the 1/60 dt would deliver only a
// fraction of the impulse. Snapshot now and rescale what the run adds by 1/60 / frame dt
// Other collider types apply the impulse immediately with our dt and don't need fixup
// Rigid colliders accumulate into m_Force/m_Torque, integrated by the simulator with each
// slice's real dt; snapshot to capture what the run adds (other types apply immediately)
int colliderType = *reinterpret_cast<const int*>(static_cast<const char*>(collider) + kColliderTypeOffset);
auto forceAccum = reinterpret_cast<float*>(static_cast<char*>(collider) + kColliderForceOffset);

Expand All @@ -138,23 +194,41 @@ static void ProcessIntegrationTask(void* collider, void* internalForce, void* in

if (colliderType == 0)
{
float scale = kPhysicsStep / timeStep;

// xyz of m_Force (0-2) and m_Torque (4-6); leave the W lanes alone.
for (int lane = 0; lane < 8; lane++)
{
if (lane != 3 && lane != 7)
{
forceAccum[lane] = forceBefore[lane] + (forceAccum[lane] - forceBefore[lane]) * scale;
}
state->heldForce[lane] = (lane != 3 && lane != 7) ? forceAccum[lane] - forceBefore[lane] : 0.0f;
}

state->heldForceFrame = frameCount;
}

for (int i = 0; i < trackedWheels; i++)
{
state->lastCompression[i] = *reinterpret_cast<float*>(wheels[i] + g_wheelCompressionOffset);
}

for (int i = 0; i < trackedWheels; i++)
{
for (int j = 0; j < 3; j++)
{
auto field = reinterpret_cast<float*>(wheels[i] + g_wheelEffSlipOffset + kSlipFieldOffsets[j]);
state->slipTarget[i][j] = *field;

if (state->hasRunOnce)
{
float alpha = std::min(timeStep / (2.0f * kPhysicsStep), 1.0f);
float& display = state->slipDisplay[i][j];
display += (state->slipTarget[i][j] - display) * alpha;
*field = display;
}
else
{
state->slipDisplay[i][j] = *field;
}
}
}

state->lastOutput[0] = locals->outputVel[0];
state->lastOutput[1] = locals->outputVel[1];
state->lastOutput[2] = locals->outputVel[2];
Expand All @@ -171,5 +245,7 @@ static HookFunction hookFunction([]()
g_wheelCompressionOffset = *hook::get_pattern<uint32_t>("45 0F 57 ? F3 0F 11 ? ? ? 00 00 F3 0F 5C", 8);
kColliderTypeOffset = *hook::get_pattern<char>("8B 4B ? 8D 41 ? 83 F8 ? 76 ? 0F 28 43", 2);
kColliderForceOffset = *hook::get_pattern<uint32_t>("0F 28 AB ? ? ? ? F3 0F 10 B3", 3);
kWheelDynamicFlagsOffset = *hook::get_pattern<uint32_t>(xbr::IsGameBuildOrGreater<2802>() ? "83 A3 ? ? ? ? ? F6 83 ? ? ? ? ? 74 ? 83 8B" : "83 A3 ? ? ? ? ? 44 84 B3", 2);
g_wheelEffSlipOffset = *hook::get_pattern<uint32_t>("0F 2F 81 ? ? ? ? 0F 28 F1", 3);
g_origProcessIntegrationTask = hook::trampoline(hook::get_call(hook::get_pattern("E8 ? ? ? ? 4C 8D 9C 24 ? ? ? ? B0 ? F3 0F 10 45")), ProcessIntegrationTask);
});
Loading