mirror of
https://github.com/sal063/AC6_recomp
synced 2026-06-25 02:02:10 -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:
@@ -25,6 +25,7 @@ REXCVAR_DEFINE_BOOL(ac6_force_safe_draw_resolution_scale, true, "AC6/NativeGraph
|
||||
"Force AC6 hybrid backend fixes mode to use 1x draw resolution scaling until the scaled path is fixed");
|
||||
REXCVAR_DEFINE_BOOL(ac6_force_safe_direct_host_resolve, true, "AC6/NativeGraphics",
|
||||
"Force AC6 hybrid backend fixes mode to keep direct_host_resolve disabled until the AC6 crash is fixed");
|
||||
REXCVAR_DECLARE(std::string, ac6_graphics_backend);
|
||||
|
||||
namespace ac6::graphics {
|
||||
namespace {
|
||||
@@ -69,6 +70,9 @@ void SyncRuntimeFlags() {
|
||||
g_runtime_status.authoritative_renderer_active =
|
||||
g_runtime_status.enabled &&
|
||||
g_runtime_status.mode != GraphicsRuntimeMode::kDisabled;
|
||||
if (g_runtime_status.authoritative_renderer_name.empty()) {
|
||||
g_runtime_status.authoritative_renderer_name = REXCVAR_GET(ac6_graphics_backend);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -140,6 +144,7 @@ void OnFrameBoundary(rex::memory::Memory* memory) {
|
||||
auto* kernel_state = ts->context()->kernel_state;
|
||||
if (auto* concrete_graphics =
|
||||
dynamic_cast<rex::graphics::GraphicsSystem*>(kernel_state->graphics_system())) {
|
||||
g_runtime_status.authoritative_renderer_name = concrete_graphics->name();
|
||||
concrete_graphics->GetLastSwapSubmission(&swap_submission, &swap_sequence);
|
||||
guest_vblank_interval_ticks = concrete_graphics->guest_vblank_interval_ticks();
|
||||
last_guest_vblank_tick = concrete_graphics->last_vblank_interrupt_guest_tick();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <rex/memory.h>
|
||||
@@ -23,6 +24,7 @@ struct NativeGraphicsRuntimeStatus {
|
||||
GraphicsRuntimeMode mode = GraphicsRuntimeMode::kHybridBackendFixes;
|
||||
bool capture_enabled = false;
|
||||
bool authoritative_renderer_active = false;
|
||||
std::string authoritative_renderer_name;
|
||||
uint32_t draw_resolution_scale_x = 1;
|
||||
uint32_t draw_resolution_scale_y = 1;
|
||||
bool direct_host_resolve = true;
|
||||
|
||||
@@ -38,8 +38,11 @@ void NativeGraphicsStatusDialog::OnDraw(ImGuiIO& io) {
|
||||
ImGui::Text("mode: %.*s", static_cast<int>(ToString(status.mode).size()),
|
||||
ToString(status.mode).data());
|
||||
ImGui::Text("authoritative renderer: %s",
|
||||
status.authoritative_renderer_active ? "RexGlue/Xenia D3D12 backend"
|
||||
: "disabled");
|
||||
status.authoritative_renderer_active
|
||||
? (status.authoritative_renderer_name.empty()
|
||||
? "RexGlue/Xenia backend"
|
||||
: status.authoritative_renderer_name.c_str())
|
||||
: "disabled");
|
||||
ImGui::Text("capture active: %s", status.capture_enabled ? "yes" : "no");
|
||||
ImGui::Text("draw resolution scale: %ux%u", status.draw_resolution_scale_x,
|
||||
status.draw_resolution_scale_y);
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <rex/ui/d3d12/d3d12_api.h>
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <d3d12.h>
|
||||
#include <dxgiformat.h>
|
||||
|
||||
namespace ac6::textures {
|
||||
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/rex_app.h>
|
||||
#if REX_HAS_D3D12
|
||||
#include <rex/graphics/d3d12/graphics_system.h>
|
||||
#endif
|
||||
#if REX_HAS_VULKAN
|
||||
#include <rex/graphics/vulkan/graphics_system.h>
|
||||
#endif
|
||||
|
||||
#include "ac6_native_graphics.h"
|
||||
#include "ac6_native_graphics_overlay.h"
|
||||
#include "generated/ac6recomp_config.h"
|
||||
|
||||
REXCVAR_DECLARE(std::string, ac6_graphics_backend);
|
||||
|
||||
class Ac6recompApp : public rex::ReXApp {
|
||||
public:
|
||||
using rex::ReXApp::ReXApp;
|
||||
@@ -27,6 +38,26 @@ class Ac6recompApp : public rex::ReXApp {
|
||||
void OnPreSetup(rex::RuntimeConfig& config) override {
|
||||
REXLOG_INFO("Ac6recompApp::OnPreSetup");
|
||||
rex::ReXApp::OnPreSetup(config);
|
||||
|
||||
const std::string requested_backend = REXCVAR_GET(ac6_graphics_backend);
|
||||
#if REX_HAS_VULKAN
|
||||
if (requested_backend == "vulkan" || requested_backend == "auto") {
|
||||
config.graphics =
|
||||
REX_GRAPHICS_BACKEND(rex::graphics::vulkan::VulkanGraphicsSystem);
|
||||
REXLOG_INFO("Ac6recompApp: selected Vulkan graphics backend");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#if REX_HAS_D3D12
|
||||
if (requested_backend == "d3d12" || requested_backend == "auto") {
|
||||
config.graphics =
|
||||
REX_GRAPHICS_BACKEND(rex::graphics::d3d12::D3D12GraphicsSystem);
|
||||
REXLOG_INFO("Ac6recompApp: selected D3D12 graphics backend");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
REXLOG_WARN("Ac6recompApp: requested graphics backend '{}' is not available in this build",
|
||||
requested_backend);
|
||||
}
|
||||
|
||||
void OnPostSetup() override {
|
||||
|
||||
@@ -27,7 +27,9 @@ REXCVAR_DECLARE(bool, ac6_texture_swaps_dump_enabled);
|
||||
REXCVAR_DECLARE(bool, vsync);
|
||||
REXCVAR_DECLARE(bool, guest_vblank_sync_to_refresh);
|
||||
REXCVAR_DECLARE(bool, host_present_from_non_ui_thread);
|
||||
#if REX_HAS_D3D12
|
||||
REXCVAR_DECLARE(bool, d3d12_allow_variable_refresh_rate_and_tearing);
|
||||
#endif
|
||||
REXCVAR_DECLARE(bool, vfetch_index_rounding_bias);
|
||||
REXCVAR_DECLARE(int32_t, video_mode_width);
|
||||
REXCVAR_DECLARE(int32_t, video_mode_height);
|
||||
@@ -35,6 +37,20 @@ REXCVAR_DECLARE(std::string, resolution);
|
||||
REXCVAR_DECLARE(int32_t, window_width);
|
||||
REXCVAR_DECLARE(int32_t, window_height);
|
||||
|
||||
#if REX_HAS_VULKAN
|
||||
#define AC6_DEFAULT_GRAPHICS_BACKEND "vulkan"
|
||||
#elif REX_HAS_D3D12
|
||||
#define AC6_DEFAULT_GRAPHICS_BACKEND "d3d12"
|
||||
#else
|
||||
#define AC6_DEFAULT_GRAPHICS_BACKEND "auto"
|
||||
#endif
|
||||
|
||||
REXCVAR_DEFINE_STRING(ac6_graphics_backend, AC6_DEFAULT_GRAPHICS_BACKEND,
|
||||
"AC6/NativeGraphics",
|
||||
"Host graphics backend: vulkan, d3d12, or auto")
|
||||
.allowed({"vulkan", "d3d12", "auto"})
|
||||
.lifecycle(rex::cvar::Lifecycle::kInitOnly);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_performance_mode, true, "AC6/Performance",
|
||||
"Disable all diagnostics, logging, and development overlays for maximum runtime performance");
|
||||
|
||||
@@ -82,9 +98,11 @@ void ApplyAc6DefaultSettings() {
|
||||
if (!rex::cvar::HasNonDefaultValue("host_present_from_non_ui_thread")) {
|
||||
REXCVAR_SET(host_present_from_non_ui_thread, false);
|
||||
}
|
||||
#if REX_HAS_D3D12
|
||||
if (!rex::cvar::HasNonDefaultValue("d3d12_allow_variable_refresh_rate_and_tearing")) {
|
||||
REXCVAR_SET(d3d12_allow_variable_refresh_rate_and_tearing, false);
|
||||
}
|
||||
#endif
|
||||
if (!rex::cvar::HasNonDefaultValue("vfetch_index_rounding_bias")) {
|
||||
REXCVAR_SET(vfetch_index_rounding_bias, true);
|
||||
}
|
||||
|
||||
+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() {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
REXCVAR_DECLARE(bool, ac6_unlock_fps);
|
||||
REXCVAR_DECLARE(bool, ac6_timing_hooks_enabled);
|
||||
REXCVAR_DECLARE(bool, ac6_cutscene_clamp);
|
||||
|
||||
namespace ac6 {
|
||||
|
||||
@@ -18,9 +19,18 @@ struct FrameStats {
|
||||
|
||||
FrameStats GetFrameStats();
|
||||
|
||||
// True while an in-engine cutscene (NU::FW::IngameCinematics, driven by
|
||||
// CAce6DemoManager::Exec) has ticked within the last decay window. Used by the
|
||||
// timing hooks to suspend the 60fps unlock so cutscenes play at native cadence.
|
||||
bool IsCinematicActive();
|
||||
|
||||
} // namespace ac6
|
||||
|
||||
bool ac6FlipIntervalHook();
|
||||
bool ac6PresentIntervalHook(PPCRegister& r10);
|
||||
void ac6DeltaDivisorHook(PPCRegister& r29);
|
||||
void ac6PresentTimingHook(PPCRegister& r31);
|
||||
|
||||
// Fires once per frame from the demo-manager Exec while a cutscene is playing.
|
||||
// r3 = the demo-manager `this`; unused (presence-of-call is the signal).
|
||||
void ac6CinematicTickHook(PPCRegister& r3);
|
||||
|
||||
Reference in New Issue
Block a user