diff --git a/ac6recomp_config.toml b/ac6recomp_config.toml index f1757327..bf6f83ba 100644 --- a/ac6recomp_config.toml +++ b/ac6recomp_config.toml @@ -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 diff --git a/src/render_hooks.cpp b/src/render_hooks.cpp index c3eb2b2e..cc3a5728 100644 --- a/src/render_hooks.cpp +++ b/src/render_hooks.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -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 diff --git a/src/render_hooks.h b/src/render_hooks.h index 401201cd..4b57a83b 100644 --- a/src/render_hooks.h +++ b/src/render_hooks.h @@ -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.