mirror of
https://github.com/sal063/AC6_recomp
synced 2026-09-08 20:12:19 -04:00
Fix integer frame-delta bias at high fps (ac6_delta_precision)
The game builds its per-frame sim delta as floor(elapsed * 3000) + 1, capped at 100. The floor plus the +1 guard bias the delta high by (1 - frac) every frame, which is invisible at the native 30fps (the cap absorbs it) but becomes a systematic speed-up once the unlock frees the frame rate: about +2% at 60fps and +10% at 300fps. Add a hook on the delta computation that carries the fractional remainder across frames, so the summed integer deltas track real elapsed time exactly instead of rounding up every frame. Gated by ac6_delta_precision (default on) and only active while the unlock's divisor remap is.
This commit is contained in:
@@ -10585,6 +10585,27 @@ address = 0x821EFD38
|
||||
name = "ac6DeltaDivisorHook"
|
||||
registers = ["r29"]
|
||||
|
||||
# Fractional-remainder delta precision.
|
||||
#
|
||||
# The game's frame delta is an INTEGER: at 0x821EFD5C it computes
|
||||
# r11 = (elapsed_ticks * 100) / (freq / divisor), then r8 = min(r11 + 1, 100).
|
||||
# The truncation plus the +1 guard bias the delta HIGH by (1 - frac) every
|
||||
# frame: +2% at 60fps (51 vs 50.85 true), +10% at 300fps (11 vs ~10.2) — a
|
||||
# systematic game-speed inflation at unlocked framerates, and the error only
|
||||
# grows with fps because the integer gets smaller.
|
||||
#
|
||||
# This hook fires at 0x821EFD74, right after the guard/cap, where r8 = final
|
||||
# integer delta, r30 = elapsed timebase ticks, r10 = ticks-per-frame
|
||||
# (freq / divisor), r29 = divisor. While the unlock's divisor remap is active
|
||||
# (r29 == 30) it replaces r8 with floor(exact + remainder), carrying the
|
||||
# fractional remainder across frames so the SUM of deltas tracks real time
|
||||
# exactly (average bias -> 0 at any framerate). Inactive (30fps native,
|
||||
# cutscene clamp, menus) it leaves the vanilla value untouched.
|
||||
[[midasm_hook]]
|
||||
address = 0x821EFD74
|
||||
name = "ac6DeltaPrecisionHook"
|
||||
registers = ["r8", "r10", "r29", "r30"]
|
||||
|
||||
# Record frame timestamps at each D3DDevice_Swap call for timing overlay.
|
||||
# Placed before the device[21516] branch so it fires unconditionally —
|
||||
# when that field is non-zero the function skips VdSwap, which caused
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/graphics/graphics_system.h>
|
||||
@@ -22,6 +23,11 @@ REXCVAR_DEFINE_BOOL(ac6_dynamic_vblank, true, "AC6",
|
||||
"free-runs at the configured rate. Gameplay is detected via the "
|
||||
"world-compositor draw heartbeat; cutscenes via the cinematic "
|
||||
"hooks.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_delta_precision, true, "AC6",
|
||||
"With the FPS unlock active, carry the fractional remainder of the "
|
||||
"game's integer frame delta across frames so its floor(x)+1 "
|
||||
"truncation guard stops inflating game speed at high framerates "
|
||||
"(+2% at 60fps, +10% at 300fps)");
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
@@ -124,6 +130,49 @@ void ac6DeltaDivisorHook(PPCRegister& r29) {
|
||||
r29.u64 = 30;
|
||||
}
|
||||
|
||||
// Fires right after the game turns the measured frame time into its integer
|
||||
// delta: r8 = min(floor(elapsed*100 / (freq/divisor)) + 1, 100). Under the
|
||||
// unlock the floor plus the +1 guard bias the delta high by (1 - frac) every
|
||||
// frame - a systematic speed-up at unlocked framerates (+2% @60fps, +10% @300).
|
||||
// Carry the fractional remainder across frames so the SUM of integer deltas
|
||||
// tracks real time exactly. Only active while the divisor remap is (r29 == 30);
|
||||
// otherwise the vanilla value passes through and the remainder resets so no
|
||||
// stale correction leaks across mode changes.
|
||||
void ac6DeltaPrecisionHook(PPCRegister& r8, PPCRegister& r10, PPCRegister& r29, PPCRegister& r30) {
|
||||
static double s_remainder = 0.0;
|
||||
|
||||
// Cheapest gates first: the register compares are free, the cvar/clock work
|
||||
// only runs on the frames that can actually take the rewrite path.
|
||||
if (r29.u32 != 30 || r10.u32 == 0 || !AreTimingHooksActive()) {
|
||||
s_remainder = 0.0;
|
||||
return;
|
||||
}
|
||||
if (!REXCVAR_GET(ac6_delta_precision)) {
|
||||
s_remainder = 0.0;
|
||||
return;
|
||||
}
|
||||
const double max_delta = 100.0; // the game's stock 30fps delta clamp
|
||||
|
||||
// Exact delta this frame in the game's own scale (elapsed * 3000), using the
|
||||
// same ticks-per-frame divisor (r10 = freq / 30) the game divided by.
|
||||
double exact = double(r30.u32) * 100.0 / double(r10.u32);
|
||||
if (exact > max_delta) {
|
||||
exact = max_delta;
|
||||
}
|
||||
|
||||
// Carry the remainder (no +1) so the summed integer deltas track real time.
|
||||
const double base = s_remainder + exact;
|
||||
double delta = std::floor(base);
|
||||
if (delta < 1.0) {
|
||||
delta = 1.0; // the game guarantees progress every frame; the overshoot is
|
||||
// repaid through a negative remainder next frame
|
||||
} else if (delta > max_delta) {
|
||||
delta = max_delta;
|
||||
}
|
||||
s_remainder = base - delta;
|
||||
r8.u64 = uint64_t(delta);
|
||||
}
|
||||
|
||||
void ac6PresentTimingHook(PPCRegister& /*r31*/) {
|
||||
// ac6::d3d::OnFrameBoundary(); // MOVED TO GPU THREAD
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ bool WorldRenderActiveRecently();
|
||||
bool ac6FlipIntervalHook();
|
||||
bool ac6PresentIntervalHook(PPCRegister& r10);
|
||||
void ac6DeltaDivisorHook(PPCRegister& r29);
|
||||
void ac6DeltaPrecisionHook(PPCRegister& r8, PPCRegister& r10, PPCRegister& r29, PPCRegister& r30);
|
||||
void ac6PresentTimingHook(PPCRegister& r31);
|
||||
|
||||
// Fires once per frame from the demo-manager Exec while a cutscene is playing.
|
||||
|
||||
Reference in New Issue
Block a user