<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Brawl's Stuff]]></title><description><![CDATA[Brawl's Stuff]]></description><link>https://brawltendo.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 14:55:52 GMT</lastBuildDate><atom:link href="https://brawltendo.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[30 FPS is my Rival: How I unlocked NFS Rivals’ framerate]]></title><description><![CDATA[With 2023 marking 10 years of Need for Speed Rivals, it’s the perfect time to finally tackle the game’s infamous 30 FPS lock.
So, first things first. Why not use those launch commands that people usually suggest? If you’re a fan of Rivals (as we all ...]]></description><link>https://brawltendo.hashnode.dev/30-fps-is-my-rival-how-i-unlocked-nfs-rivals-framerate</link><guid isPermaLink="true">https://brawltendo.hashnode.dev/30-fps-is-my-rival-how-i-unlocked-nfs-rivals-framerate</guid><category><![CDATA[game modding]]></category><category><![CDATA[Games]]></category><category><![CDATA[Game Development]]></category><dc:creator><![CDATA[Brawltendo]]></dc:creator><pubDate>Wed, 29 Nov 2023 15:11:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1701274274335/f8ab77ea-4279-4077-a0e8-460f47d47c88.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>With 2023 marking 10 years of Need for Speed Rivals, it’s the perfect time to finally tackle the game’s infamous 30 FPS lock.</p>
<p>So, first things first. Why not use those launch commands that people usually suggest? If you’re a fan of Rivals (as we all definitely are), you’re likely aware that the commands that are usually recommended to “unlock” the framerate end up causing more problems than they solve. This is because those commands just force the physics simulation to run at a faster rate (e.g. 60 times per second rather than 30). Since a few core parts of the game assume that the physics engine will consistently run at 30hz, several mechanics will fall apart the higher you push the sim rate. For example, a chunk of the vehicle sim will run 2 substeps per tick (only for non-traffic cars) to match the 60hz sim rate of the games it was inherited from. Weapon and event timers are also affected, as they use a hardcoded 30hz delta time, making them count up or down faster the higher the framerate is. These are major issues, as the entire game is centered around racing cars and battling cops or racers with said weapons. Due to this and the complexity of a modern game, we need to find a way to unlock the framerate that:</p>
<ol>
<li><p>Maintains consistent behavior above 30 FPS.</p>
</li>
<li><p>Doesn’t break anything else in the process.</p>
</li>
<li><p>Avoids the need to rewrite parts of the engine or large chunks of game code.</p>
</li>
</ol>
<h3 id="heading-a-long-road-awaits">A long road awaits...</h3>
<p>I first started seriously working on this a couple of months ago, while earlier attempts mainly involved navigating around Frostbite's internals to get a better grasp of how it worked and what exactly I should be looking for to properly unlock the framerate. To get the first concern out of the way, yes, the engine <em>already</em> natively supported a variable framerate with a fixed sim rate at this time, making the decision to cap the game at 30 FPS even more puzzling. Much like other game engines, Frostbite ticks objects in passes/groups at different points during a frame. However, at this time the engine had something interesting, which was an update pass specifically for interpolation. This pass seems to have been phased out in later versions of the engine (around 2016 or so?) in favor of a more standardized way to handle interpolation, but luckily it still exists in NFS 2015, which will prove to be useful moving forward.</p>
<h3 id="heading-unlocked-potential"><strong>Unlocked potential</strong></h3>
<p>Now that I was aware of Frostbite’s interpolation pass, I had to figure out how the engine actually made use of it. Thankfully, this was fairly straightforward; the <code>Client.VisualFrameInterpolation</code> option just needed to be enabled along with setting <code>GameTime.MaxVariableFps</code> to a value greater than 30. As soon as this was done, I was immediately presented with something that, at first glance, was unlocked beyond 30 FPS without using the currently known method. That feeling wouldn’t even last a minute though, because as soon as I started driving, a new problem arose: every car, whether player, AI, or traffic, was moving at 30hz. What was going on here? Well, it turns out that most things were set up for interpolating object transforms <em>except</em> the component class that implements most vehicle logic, <code>ClientRaceVehicleComponent</code>. This was done deliberately as well, as in Rivals this class very clearly borrows code from <code>ClientChassisComponent</code> (although it doesn't inherit from this class), which <em>does</em> have code to handle this. Instead, the <code>ClientRaceVehicleComponentPrediction::worldTransform</code> function just spits out whatever the current transform is without any interpolation applied. Since <code>ClientChassisComponentPrediction's</code> equivalent function still exists, this should be an easy fix and everything should be done now, right?</p>
<p>Wrong. Patching <code>ClientRaceVehicleComponentPrediction's</code> virtual function table to use <code>ClientChassisComponentPrediction's</code> function was as simple as it gets, but now we’re left with a new problem. It now seems like every car <em>except</em> ours has interpolation applied. How is this even possible? It took a bit of messing around with other things, but I ended up having the idea to detach the camera from the car and discovered that it, in fact, <em>was</em> the problem. In Rivals, <code>CinebotCamera</code> (a custom camera class likely created by Black Box, although BioWare has also used it) doesn’t interpolate the final transform it passes into the camera view manager. This class is set up to update every frame but usually its transform is built using the 30hz vehicle transform, which is what’s passed into the <code>Anchor</code> property of camera entities in blueprint graphs. Every camera that was attached to a car would exhibit varying degrees of brokenness thanks to this. To fix this, I just extended the <code>CinebotCamera</code> class to store the previous and current orientation, position, and FOV, then interpolate between those values in my overridden <code>PostFrameUpdate</code> function, and finally pass them to the main transform and FOV fields that are used later on in the camera view manager.</p>
<p>The next problem had to do with the driver and steering wheel position. Driving at any speed would cause both to desync from the car, albeit in different ways. The driver would lag behind the car, while the steering wheel would stay in place until doing a hard turn, which would cause it to pivot far away from its origin. I spent a bit of time trying to track this down because the driver’s animations were already interpolated, so I assumed this had to be down to where and how the driver's position was updated. This assumption was partially correct; the points in the frame at which they were updating the driver were too early to get an interpolated transform, so I changed them to update passes that would get around that. Doing this seemingly changed nothing and I was left scratching my head with no choice but to go into ANT code (EA’s animation library used in Frostbite). There was an engine setting that disabled ANT pose interpolation, but I was stubborn and determined to figure out some way to keep the animations interpolated while fixing the desyncing. A couple hours later, after poking through all the relevant pose interpolation code and not spotting anything odd, I gave in and just disabled pose interpolation outright. As it turns out, this is exactly what Ghost did in NFS 2015 anyway and unless you have the camera close to the driver somehow, it’s not very noticeable in the first place. I might come back to that later though and attempt to figure out some way to get it working properly.</p>
<p>Next up, we have the “augmented reality” UI elements, which are the popups you get next to the car during certain free roam activities, as well as the vehicle name tags. These, much like the camera, take their position straight from the vehicle sim, which causes some flickering and desyncing to happen. The way NFS Edge solved this was by adding a new property to <code>ClientRaceVehicleComponent</code> that stores the interpolated transform, which is updated by 2 new functions available to <code>GameComponents</code>. Unfortunately, these functions didn't exist yet in Rivals' version of Frostbite, so I had to find another way. The first approach I tried was simply hooking the <code>onPostPhysicsUpdateSync</code> and <code>onFrameInterpolationUpdate</code> functions, and adding logic to them to grab the right transform. This made a slight difference, but it didn't fix the problem. By the time my new property was updated, it had already gone back to using the sim transform, so it was time to go back to the drawing board.</p>
<p>My next approach was an attempt to backport those newer Frostbite functions, except localized only to <code>ClientRaceVehicleComponent</code>. I did this using Frostbite’s updater pattern, which is essentially its equivalent of Unreal’s tick functions (and honestly, I find it much more intuitive than setting up multiple tick functions for an <code>Actor</code> in UE). Once I set this updater up with 2 passes (<code>PostSim</code> and <code>FrameInterpolation</code>), I decided to take a different approach with what I would feed back out to blueprint graphs. This time I took the sim transform, interpolated it, and only updated the property when the new value wasn’t equal to the current sim value. This worked flawlessly once I adjusted all the old blueprint connections to use my new property, so I figured I could kill two birds with one stone and went ahead and added an interpolated RPM property so the tach would look less jarring. Those two birds quickly turned into three because I was easily able to fix up the name tag/health displays by giving <code>ClientUIVehicleDisplayEntity::update</code> the offsets to my new field instead of the sim transform. However, there was just one little problem: I never even had to spend time setting up the updater. This whole time, I could’ve just used the functions I had used during my first attempt. I guess that’s what I get for trying to follow others (NFS Edge) too closely.</p>
<p>Even with that little misstep, I felt unstoppable after I finally fixed the activity pop-ups, so I moved right over to the HUD shake, which makes use of values provided by <code>ClientUIScreenEffectEntity</code>. This time, I could put my newfound updater knowledge to the test because Entity classes like this one make heavy use of this pattern. My approach was essentially the same as it was for my 2nd crack at the pop-ups, and it worked perfectly as expected.</p>
<p>To keep the UI fixes going, I moved onto the map. As you push above 30 FPS with the old method, the slower the map cursor will move. This can get <em>painfully</em> slow, so even though it’s a much more minor issue in comparison to everything else that’s been covered, it bugged me enough that I felt it could use a little QOL upgrade (<em>you</em> try constantly restarting the game and moving that cursor to events). Thankfully, this was pretty straightforward to patch up, and anyone familiar with game programming will be likely to correctly assume that it was a classic case of not using delta time where it probably should have been used (in Ghost/Criterion’s defense, this was hardly an issue when the game was set to a locked framerate anyway).</p>
<p>After all that, I thought I was done with the UI/HUD, but of course, there had to be one last thing: the EMP targeting display and reticle. This one was trickier to solve because these elements make use of two different transforms which both come from different classes. I did <em>not</em> want to touch those classes because I wanted to avoid any chance of breaking weapon behavior or anything else that happens to use them, so I came up with a workaround. Both of these transforms are passed through a <code>TransformMultiplier</code> node, so I decided to use this class to add a property that outputs an interpolated transform. In all honesty, this was a pretty horrible solution, as I had to modify one of Frostbite’s built-in Schematics nodes to run every frame for the sake of interpolation, but this was also the easiest and least risky option. I <em>was</em> able to make it more efficient, however, by coming up with a pretty hacky workaround using a magic number to opt entities into the updater rather than just updating them all. If I were working with the game’s original codebase rather than reverse engineering it, I would certainly do this another way. The most obvious solution would be to add a new node in one of Ghost’s extra modules strictly for this purpose, extending the engine instead of making unnecessary changes to what’s already provided.</p>
<p>Finally, we have the infamous engine audio desync which also plagues NFS The Run, Medal of Honor Warfighter's driving missions, and from what I've heard, possibly even Criterion's games and older Black Box games at unlocked framerates. I was dreading this one, since the fact that it desynced even when the framerate and sim rate were equal immediately told me that this wasn't the usual "oh, it just needs to be interpolated" bit that I had gotten used to. In addition, NFS 2015 and later all handle Ginsu playback (by that point renamed to Octane) much differently, so those games would not help in the slightest. Thankfully, I'm <em>very</em> familiar with this older iteration of Ginsu, as I did plenty of research and reverse engineering to recreate the system for the Core X Most Wanted project. Since Frostbite makes use of a node graph to process audio, more often than not, external values will be passed into the graph from a <code>Blueprint</code>. Thanks to that, I could track exactly where the problem was coming from. I started from the physics RPM, which is updated at 30hz along with the rest of the vehicle sim. This is then normalized to a 0-1 range and used in several pieces of logic that calculate a fake RPM to be passed into the audio graph.</p>
<p>After tracking this value down within the vehicle audio Blueprint, I didn’t find anything unusual. Surely this means that the audio graph is the problem point, right? Well, after spending some time learning how the audio graph works internally, I once again found nothing out of the ordinary. My last resort was to go directly into the Ginsu player itself, which is where all the problems were immediately exposed. Under normal circumstances, the Ginsu synth uses a slew function on the current frequency to soften harsh transitions. The value that tracks the remaining slew samples becomes increasingly more desynced the higher you push the framerate and prevents the playback position from advancing regularly. To solve this, I just clamped the value to zero to stop negative values from breaking the synth. Just this alone was enough to fix it, but I added a few other small bits that help make it sound better, like changing the hardcoded 33ms (essentially 30hz) slew time to use the <code>GinsuPlayer's</code> otherwise unused parameter meant specifically for that purpose. This change allowed me to go into the car engine sound patch and hook up the current delta time to the slew time parameter, making it framerate-independent.</p>
<h3 id="heading-final-thoughts">Final thoughts</h3>
<p>Unlike what many (including myself) have assumed over the years, this experiment was <em>not</em> impossible, nor was it even that difficult. It <em>did</em> take a deeper understanding of Frostbite than most outside of EA, but the implementation itself was much more straightforward than I had ever imagined. Getting this done was a massive relief since it’s one of those “holy grail” types of mods that's haunted the NFS community for a whole decade. If you're interested in checking the mod out, it will be available to download <a target="_blank" href="https://nfsmods.xyz/mod?id=5133">here</a> and <a target="_blank" href="https://github.com/Brawltendo/NFS-Rivals-Framerate-Unlocker/releases">here</a>, and the source code is available on <a target="_blank" href="https://github.com/Brawltendo/NFS-Rivals-Framerate-Unlocker">my GitHub</a> for anyone looking to extend it, contribute, fix bugs, or use any of the reversed classes for other mods. Now to figure out why Rivals' AI pathfinding is behind every single game crash...</p>
]]></content:encoded></item></channel></rss>