mirror of
https://github.com/sal063/AC6_recomp
synced 2026-09-10 20:39:43 -04:00
Add 60fps cutscene clamp for in-engine cinematics
Suspend the FPS unlock while a demo-manager Exec (DD sub_82184460 / EM sub_821856F8) ticks, so the frame-locked IngameCinematics Sequencer plays at native ~30fps instead of double speed. Adds ac6_cutscene_clamp CVar (default on).
This commit is contained in:
+45
-1
@@ -11,6 +11,9 @@
|
||||
REXCVAR_DEFINE_BOOL(ac6_unlock_fps, false, "AC6", "Unlock frame rate to 60fps");
|
||||
REXCVAR_DEFINE_BOOL(ac6_timing_hooks_enabled, true, "AC6",
|
||||
"Enable AC6 timing hooks that alter the game's presentation cadence");
|
||||
REXCVAR_DEFINE_BOOL(ac6_cutscene_clamp, true, "AC6",
|
||||
"Suspend the 60fps unlock during in-engine cutscenes so they "
|
||||
"play at native ~30fps instead of double speed");
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
@@ -21,12 +24,44 @@ std::atomic<double> g_fps{0.0};
|
||||
std::atomic<uint64_t> g_frame_count{0};
|
||||
Clock::time_point g_frame_start{};
|
||||
|
||||
// Wall-clock (steady) ms of the last in-engine cutscene tick. The cutscene
|
||||
// hook (ac6CinematicTickHook) runs on the game thread; the timing hooks read
|
||||
// this on the present/GPU path, hence the atomic. INT64_MIN = never ticked.
|
||||
std::atomic<int64_t> g_last_cinematic_tick_ms{INT64_MIN};
|
||||
|
||||
// The demo-manager Exec runs every game frame while a cutscene plays (~16-33ms
|
||||
// apart). A few-frame decay keeps the clamp asserted across the cutscene and
|
||||
// auto-releases shortly after it ends.
|
||||
constexpr int64_t kCinematicDecayMs = 100;
|
||||
|
||||
int64_t NowMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
Clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
bool AreTimingHooksActive() {
|
||||
return REXCVAR_GET(ac6_timing_hooks_enabled) && REXCVAR_GET(ac6_unlock_fps);
|
||||
return REXCVAR_GET(ac6_timing_hooks_enabled) && REXCVAR_GET(ac6_unlock_fps) &&
|
||||
!ac6::IsCinematicActive();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ac6 {
|
||||
|
||||
bool IsCinematicActive() {
|
||||
if (!REXCVAR_GET(ac6_cutscene_clamp)) {
|
||||
return false;
|
||||
}
|
||||
const int64_t last = g_last_cinematic_tick_ms.load(std::memory_order_relaxed);
|
||||
if (last == INT64_MIN) {
|
||||
return false;
|
||||
}
|
||||
return (NowMs() - last) <= kCinematicDecayMs;
|
||||
}
|
||||
|
||||
} // namespace ac6
|
||||
|
||||
bool ac6FlipIntervalHook() {
|
||||
return AreTimingHooksActive();
|
||||
}
|
||||
@@ -61,6 +96,15 @@ void ac6PresentTimingHook(PPCRegister& /*r31*/) {
|
||||
g_frame_start = now;
|
||||
}
|
||||
|
||||
void ac6CinematicTickHook(PPCRegister& /*r3*/) {
|
||||
// A demo-manager Exec (DD: sub_82184460 / EM: sub_821856F8) ran this frame ->
|
||||
// an in-engine cutscene is playing. Stamp the time so AreTimingHooksActive()
|
||||
// suspends the 60fps unlock until the cutscene ends (the stamp goes stale a
|
||||
// few frames after the last tick), letting the frame-locked cinematic
|
||||
// Sequencer play at native ~30fps instead of double speed.
|
||||
g_last_cinematic_tick_ms.store(NowMs(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
namespace ac6 {
|
||||
|
||||
FrameStats GetFrameStats() {
|
||||
|
||||
Reference in New Issue
Block a user