mirror of
https://github.com/sal063/AC6_recomp
synced 2026-07-12 15:42:23 -04:00
@@ -40,6 +40,7 @@ set(AC6RECOMP_SOURCES
|
||||
src/ac6_backend_fixes/ac6_backend_capture_bridge.cpp
|
||||
src/ac6_backend_fixes/ac6_backend_hooks.cpp
|
||||
src/ac6_backend_fixes/ac6_backend_pass_classifier.cpp
|
||||
src/ac6_backend_fixes/ac6_fps_physics_fix.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// Framerate-independent flight dynamics for the FPS unlock.
|
||||
//
|
||||
// The game's flight model mixes two timing styles:
|
||||
// - Kinematics are delta-scaled and framerate-correct: position integration,
|
||||
// the persistent-velocity wind relaxation, and the control-surface ramps
|
||||
// all multiply by the adaptive frame delta (see ac6DeltaDivisorHook).
|
||||
// - The longitudinal force/speed-command dynamics accumulate FIXED PER-FRAME
|
||||
// steps sized for the native 30fps cadence (constants assume a 33.3ms
|
||||
// frame, no delta anywhere in the chain).
|
||||
//
|
||||
// With ac6_unlock_fps the dynamics tick 2x+ as often, so throttle accel/decel,
|
||||
// gravity response, and high-G speed bleed all scale with framerate. Cruise
|
||||
// speed stays correct at any rate because it is the accumulator's equilibrium
|
||||
// (per-step gain and decay cancel), which is call-rate independent.
|
||||
//
|
||||
// The two per-frame-stepped accumulators, located by static analysis of the
|
||||
// flight-model class (ctor rex_sub_82328F48/82329160, embedded in the aircraft
|
||||
// at this+10672, base class 0x82303xxx):
|
||||
//
|
||||
// - rex_sub_823046A0 (core force step, generated recomp.22): [this+1320] is
|
||||
// the persistent longitudinal force/speed command. Thrust ([this+972] and
|
||||
// the rex_sub_82282BC8 term), overspeed drag (fractions of the reference
|
||||
// speeds [this+1264]/[this+1268]), min-speed correction ([this+1280]) and
|
||||
// the high-G/AoA bleed all add constant-sized steps to it each call.
|
||||
// Everything downstream (velocity blend, position += velocity * dt) is
|
||||
// delta-scaled.
|
||||
// - rex_sub_82329B40 (input shaping, generated recomp.23): [this+1456] is
|
||||
// the accel/brake trigger command in [-1, 1], ramped toward +/-1 and
|
||||
// decayed toward 0 by per-frame constants. (The stick lags at +360/+364,
|
||||
// hold timers +368/+372 and the +304/+312/+1364 terms in the same class
|
||||
// are already delta-scaled and are left untouched.)
|
||||
//
|
||||
// Fix: wrap both guest functions and rescale each accumulator's NET per-call
|
||||
// change by frame_ms / 33.333 (clamped to <= 1):
|
||||
//
|
||||
// x_new = x_pre + (x_post - x_pre) * ratio
|
||||
//
|
||||
// Scaling the per-step delta of a feedback accumulator by the step-frequency
|
||||
// ratio reproduces the same continuous-time dynamics the game has at 30fps,
|
||||
// without touching the delta-scaled math inside. At native cadence the ratio
|
||||
// is 1 and the wrapper is a bit-exact pass-through. The wrapper applies to
|
||||
// every aircraft that runs this flight model (player and AI), keeping the
|
||||
// whole sim consistent.
|
||||
//
|
||||
// Both symbols are weak in the generated code and registered by address in
|
||||
// ac6recomp_init, so these strong overrides capture direct calls and vtable
|
||||
// dispatch alike.
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/memory/utils.h>
|
||||
#include <rex/ppc.h>
|
||||
|
||||
#include "../render_hooks.h"
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_fps_physics_fix, true, "AC6",
|
||||
"Make flight-model accel/decel framerate-independent when the FPS unlock "
|
||||
"is active (rescales the game's fixed per-frame force steps by "
|
||||
"frame time / 33.3ms)");
|
||||
|
||||
namespace {
|
||||
|
||||
// Longitudinal force/speed-command accumulator in the flight-model object
|
||||
// (stepped per frame by rex_sub_823046A0 without delta scaling).
|
||||
constexpr uint32_t kForceCommandOffset = 1320;
|
||||
// Accel/brake trigger command in [-1, 1] (ramped/decayed per frame by
|
||||
// rex_sub_82329B40 without delta scaling).
|
||||
constexpr uint32_t kTriggerCommandOffset = 1456;
|
||||
|
||||
// The game's native simulation cadence the per-frame constants were tuned for.
|
||||
constexpr double kNativeFrameMs = 1000.0 / 30.0;
|
||||
|
||||
// Everything below runs only on the guest sim thread (the flight-model
|
||||
// wrappers are the sole callers), so plain statics are safe throughout.
|
||||
|
||||
// Per-frame-step scale for this frame: 1.0 at the native 30fps cadence,
|
||||
// shrinking as the frame rate rises (never above 1.0 - the game's own delta
|
||||
// clamp already holds sim speed at/below 30fps). Gated on the SAME signal as
|
||||
// the frame-delta hooks (ac6::TimingHooksActive) so the force step and the
|
||||
// kinematics delta always revert to vanilla together (cutscene clamp, menus,
|
||||
// unlock off). Returns exactly 1.0 for a pass-through. Inputs change at most
|
||||
// once per guest frame, so the result is cached per frame.
|
||||
double StepRatio() {
|
||||
static uint64_t s_cached_frame = ~uint64_t(0);
|
||||
static double s_cached_ratio = 1.0;
|
||||
const ac6::FrameStats stats = ac6::GetFrameStats();
|
||||
if (stats.frame_count == s_cached_frame) {
|
||||
return s_cached_ratio;
|
||||
}
|
||||
s_cached_frame = stats.frame_count;
|
||||
s_cached_ratio = 1.0;
|
||||
if (!REXCVAR_GET(ac6_fps_physics_fix) || !ac6::TimingHooksActive()) {
|
||||
return s_cached_ratio;
|
||||
}
|
||||
if (stats.frame_time_ms <= 0.0) {
|
||||
return s_cached_ratio; // no frame measured yet
|
||||
}
|
||||
double ratio = stats.frame_time_ms / kNativeFrameMs;
|
||||
if (ratio > 1.0) {
|
||||
ratio = 1.0; // never blend past the native 30fps step
|
||||
} else if (ratio < 0.02) {
|
||||
ratio = 0.02; // sanity floor against a bogus frame-time sample
|
||||
}
|
||||
s_cached_ratio = ratio;
|
||||
return s_cached_ratio;
|
||||
}
|
||||
|
||||
// Rescales the net change the wrapped call made to one guest float:
|
||||
// field = pre + (post - pre) * ratio. Logs the first activation per field so
|
||||
// each wrapper gets its own confirmation line in the log.
|
||||
void BlendFieldDelta(uint8_t* base, uint32_t ea, float pre, double ratio, const char* what,
|
||||
bool& logged) {
|
||||
const float post = rex::memory::load_and_swap<float>(base + ea);
|
||||
if (post == pre) {
|
||||
return;
|
||||
}
|
||||
rex::memory::store_and_swap<float>(base + ea, static_cast<float>(pre + (post - pre) * ratio));
|
||||
if (!logged) {
|
||||
logged = true;
|
||||
REXLOG_INFO("[AC6-PHYS-FIX] active: {} delta {:+.5f} scaled by {:.3f}", what, post - pre,
|
||||
ratio);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PPC_EXTERN_FUNC(__imp__rex_sub_823046A0); // flight-model force step
|
||||
PPC_EXTERN_FUNC(__imp__rex_sub_82329B40); // flight-model input shaping
|
||||
|
||||
// Flight-model core force step (0x823046A0): rescale the per-frame stepped
|
||||
// longitudinal force/speed command at [this+1320].
|
||||
PPC_FUNC_IMPL(rex_sub_823046A0) {
|
||||
PPC_FUNC_PROLOGUE();
|
||||
|
||||
const uint32_t self = ctx.r3.u32;
|
||||
const double ratio = StepRatio();
|
||||
// 1.0 is the exact pass-through value (native cadence, or the fix disabled).
|
||||
if (ratio == 1.0 || self == 0) {
|
||||
__imp__rex_sub_823046A0(ctx, base);
|
||||
return;
|
||||
}
|
||||
|
||||
const float pre = rex::memory::load_and_swap<float>(base + self + kForceCommandOffset);
|
||||
__imp__rex_sub_823046A0(ctx, base);
|
||||
static bool s_logged = false;
|
||||
BlendFieldDelta(base, self + kForceCommandOffset, pre, ratio, "force-command(+1320)", s_logged);
|
||||
}
|
||||
|
||||
// Flight-model input shaping (0x82329B40): rescale the per-frame ramped
|
||||
// accel/brake trigger command at [this+1456].
|
||||
PPC_FUNC_IMPL(rex_sub_82329B40) {
|
||||
PPC_FUNC_PROLOGUE();
|
||||
|
||||
const uint32_t self = ctx.r3.u32;
|
||||
const double ratio = StepRatio();
|
||||
if (ratio == 1.0 || self == 0) {
|
||||
__imp__rex_sub_82329B40(ctx, base);
|
||||
return;
|
||||
}
|
||||
|
||||
const float pre = rex::memory::load_and_swap<float>(base + self + kTriggerCommandOffset);
|
||||
__imp__rex_sub_82329B40(ctx, base);
|
||||
static bool s_logged = false;
|
||||
BlendFieldDelta(base, self + kTriggerCommandOffset, pre, ratio, "trigger-command(+1456)",
|
||||
s_logged);
|
||||
}
|
||||
@@ -19,11 +19,13 @@
|
||||
|
||||
REXCVAR_DECLARE(std::string, user_data_root);
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_texture_swaps_enabled, true, "AC6/TextureSwaps",
|
||||
"Enable AC6 texture dump and replacement support");
|
||||
REXCVAR_DEFINE_BOOL(ac6_texture_swaps_enabled, false, "AC6/TextureSwaps",
|
||||
"Enable AC6 texture dump and replacement support (default off: the "
|
||||
"replacement lookup currently checks the filesystem on every texture "
|
||||
"upload, which can cause frame stutters; enable for texture modding)");
|
||||
REXCVAR_DEFINE_BOOL(ac6_texture_swaps_dump_enabled, false, "AC6/TextureSwaps",
|
||||
"Dump host-ready textures to the user-data texture dump folder");
|
||||
REXCVAR_DEFINE_BOOL(ac6_texture_swaps_replace_enabled, true, "AC6/TextureSwaps",
|
||||
REXCVAR_DEFINE_BOOL(ac6_texture_swaps_replace_enabled, false, "AC6/TextureSwaps",
|
||||
"Load matching replacement DDS files from the user-data texture override folders");
|
||||
REXCVAR_DEFINE_STRING(ac6_texture_swaps_dump_dir, "texture_dumps", "AC6/TextureSwaps",
|
||||
"User-data subdirectory that stores dumped texture DDS files and metadata");
|
||||
|
||||
+10
-2
@@ -32,6 +32,8 @@ 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);
|
||||
REXCVAR_DECLARE(bool, d3d12_low_latency_swap_chain);
|
||||
REXCVAR_DECLARE(int32_t, d3d12_max_frame_latency);
|
||||
#endif
|
||||
REXCVAR_DECLARE(bool, vfetch_index_rounding_bias);
|
||||
REXCVAR_DECLARE(int32_t, video_mode_width);
|
||||
@@ -114,11 +116,17 @@ void ApplyAc6DefaultSettings() {
|
||||
REXCVAR_SET(guest_vblank_sync_to_refresh, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("host_present_from_non_ui_thread")) {
|
||||
REXCVAR_SET(host_present_from_non_ui_thread, false);
|
||||
REXCVAR_SET(host_present_from_non_ui_thread, true);
|
||||
}
|
||||
#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);
|
||||
REXCVAR_SET(d3d12_allow_variable_refresh_rate_and_tearing, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("d3d12_low_latency_swap_chain")) {
|
||||
REXCVAR_SET(d3d12_low_latency_swap_chain, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("d3d12_max_frame_latency")) {
|
||||
REXCVAR_SET(d3d12_max_frame_latency, 1);
|
||||
}
|
||||
#endif
|
||||
if (!rex::cvar::HasNonDefaultValue("vfetch_index_rounding_bias")) {
|
||||
|
||||
+128
-1
@@ -4,8 +4,11 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
#include <rex/graphics/graphics_system.h>
|
||||
#include <rex/logging.h>
|
||||
#include <rex/system/kernel_state.h>
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_unlock_fps, false, "AC6", "Unlock frame rate to 60fps");
|
||||
@@ -14,6 +17,17 @@ REXCVAR_DEFINE_BOOL(ac6_timing_hooks_enabled, true, "AC6",
|
||||
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");
|
||||
REXCVAR_DEFINE_BOOL(ac6_dynamic_vblank, true, "AC6",
|
||||
"With the FPS unlock active, pace frame-locked content (menus, "
|
||||
"cutscenes, pause) at the native 60Hz guest vblank while gameplay "
|
||||
"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;
|
||||
|
||||
@@ -34,21 +48,56 @@ std::atomic<int64_t> g_last_cinematic_tick_ms{INT64_MIN};
|
||||
// auto-releases shortly after it ends.
|
||||
constexpr int64_t kCinematicDecayMs = 100;
|
||||
|
||||
// Wall-clock ms of the last draw using the world/effects compositor pixel
|
||||
// shader (stamped by the GPU command processor via NotifyWorldCompositorDraw).
|
||||
// The compositor runs every frame the 3D world renders and never in the 2D
|
||||
// front-end, so its freshness distinguishes free-runnable gameplay from
|
||||
// frame-locked menus/hangar. (The delta-time hook was tried first as this
|
||||
// signal, but it lives in the frame layer and ticks in menus too.)
|
||||
// INT64_MIN = never drawn.
|
||||
std::atomic<int64_t> g_last_world_draw_ms{INT64_MIN};
|
||||
constexpr int64_t kWorldDrawDecayMs = 300;
|
||||
|
||||
int64_t NowMs() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
Clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
bool IsWorldRenderActive() {
|
||||
const int64_t last = g_last_world_draw_ms.load(std::memory_order_relaxed);
|
||||
if (last == INT64_MIN) {
|
||||
return false;
|
||||
}
|
||||
return (NowMs() - last) <= kWorldDrawDecayMs;
|
||||
}
|
||||
|
||||
bool AreTimingHooksActive() {
|
||||
// The world-render gate only applies under dynamic vblank pacing: it exists
|
||||
// to keep menus/hangar at native cadence, and it fails closed (a game
|
||||
// build/render path whose compositor shader hashes differently would never
|
||||
// stamp it). ac6_dynamic_vblank=false restores the plain always-on unlock.
|
||||
return REXCVAR_GET(ac6_timing_hooks_enabled) && REXCVAR_GET(ac6_unlock_fps) &&
|
||||
!ac6::IsCinematicActive();
|
||||
!ac6::IsCinematicActive() &&
|
||||
(!REXCVAR_GET(ac6_dynamic_vblank) || IsWorldRenderActive());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ac6 {
|
||||
|
||||
// True while the FPS-unlock timing hooks are remapping the game's cadence
|
||||
// (unlock cvars on and no cutscene clamp). The physics force-step rescale keys
|
||||
// off this so it stays in lockstep with the frame-delta hooks: whenever the
|
||||
// delta reverts to vanilla, the force step must too.
|
||||
bool TimingHooksActive() {
|
||||
return AreTimingHooksActive();
|
||||
}
|
||||
|
||||
bool WorldRenderActiveRecently() {
|
||||
return IsWorldRenderActive();
|
||||
}
|
||||
|
||||
bool IsCinematicActive() {
|
||||
if (!REXCVAR_GET(ac6_cutscene_clamp)) {
|
||||
return false;
|
||||
@@ -81,9 +130,70 @@ 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
|
||||
|
||||
// Dynamic vblank pacing: free-run only while the 3D world is rendering; pace
|
||||
// frame-locked content (menus, hangar, cutscenes) at the native 60Hz. Only
|
||||
// engages when the FPS unlock is on, so default configurations keep the plain
|
||||
// cvar-driven vblank behavior.
|
||||
const bool unlock = REXCVAR_GET(ac6_timing_hooks_enabled) && REXCVAR_GET(ac6_unlock_fps);
|
||||
const bool dynamic_pacing = REXCVAR_GET(ac6_dynamic_vblank) && unlock;
|
||||
// Single source of truth for "the unlock is remapping the cadence right now" -
|
||||
// the same signal that gates the interval/delta hooks and the physics rescale.
|
||||
const bool free_running = dynamic_pacing && AreTimingHooksActive();
|
||||
|
||||
// Guest-vblank Hz override for this frame. 0 = no override (free-run at the
|
||||
// vsync/tearing rate); dynamic pacing forces frame-locked content to 60Hz.
|
||||
double override_hz = 0.0;
|
||||
if (dynamic_pacing && !free_running) {
|
||||
override_hz = 60.0;
|
||||
}
|
||||
rex::graphics::GraphicsSystem::SetGuestVblankHzOverride(override_hz);
|
||||
|
||||
const auto now = Clock::now();
|
||||
if (g_frame_start.time_since_epoch().count() != 0) {
|
||||
const double frame_time_ms =
|
||||
@@ -94,6 +204,19 @@ void ac6PresentTimingHook(PPCRegister& /*r31*/) {
|
||||
g_frame_count.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
g_frame_start = now;
|
||||
|
||||
// Log the first handful of pacing transitions.
|
||||
static double last_log_key = -0.5;
|
||||
static uint32_t transition_logs = 0;
|
||||
if (unlock && override_hz != last_log_key && transition_logs < 32) {
|
||||
++transition_logs;
|
||||
if (override_hz == 0.0) {
|
||||
REXLOG_INFO("[AC6-VBLANK] pacing -> free-run (uncapped)");
|
||||
} else {
|
||||
REXLOG_INFO("[AC6-VBLANK] pacing -> {:.0f}Hz guest vblank", override_hz);
|
||||
}
|
||||
}
|
||||
last_log_key = override_hz;
|
||||
}
|
||||
|
||||
void ac6CinematicTickHook(PPCRegister& /*r3*/) {
|
||||
@@ -113,4 +236,8 @@ FrameStats GetFrameStats() {
|
||||
g_frame_count.load(std::memory_order_relaxed)};
|
||||
}
|
||||
|
||||
void NotifyWorldCompositorDraw() {
|
||||
g_last_world_draw_ms.store(NowMs(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace ac6
|
||||
|
||||
@@ -19,16 +19,34 @@ struct FrameStats {
|
||||
|
||||
FrameStats GetFrameStats();
|
||||
|
||||
// True while the FPS-unlock timing hooks are remapping the game's cadence
|
||||
// (unlock cvars on and no cutscene clamp). The physics force-step rescale keys
|
||||
// off this so it stays in lockstep with the frame-delta hooks.
|
||||
bool TimingHooksActive();
|
||||
|
||||
// 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();
|
||||
|
||||
// Called by the GPU command processor whenever a draw uses the world/effects
|
||||
// compositor pixel shader (guest ucode 17e5e4ac3e713245). The compositor runs
|
||||
// once per frame whenever the 3D world is being rendered and never in the 2D
|
||||
// front-end (menus, hangar), so its freshness is the "gameplay world active"
|
||||
// signal for the dynamic FPS pacing.
|
||||
void NotifyWorldCompositorDraw();
|
||||
|
||||
// True while a world-compositor draw happened within the last decay window
|
||||
// (i.e. the 3D world is being rendered - gameplay or in-engine cutscene, not
|
||||
// the 2D front-end). Same signal the dynamic FPS pacing uses.
|
||||
bool WorldRenderActiveRecently();
|
||||
|
||||
} // namespace ac6
|
||||
|
||||
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.
|
||||
|
||||
@@ -102,6 +102,13 @@ class GraphicsSystem : public system::IGraphicsSystem {
|
||||
return last_vblank_interrupt_guest_tick_.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
// Per-game override of the guest vblank rate, in Hz. 0 = no override (use
|
||||
// the vsync/guest_vblank_sync_to_refresh cvar logic). Lets game-specific
|
||||
// timing hooks pace frame-locked content (menus, cinematics) at the native
|
||||
// rate while gameplay free-runs. Process-wide, not per-instance.
|
||||
static void SetGuestVblankHzOverride(double hz);
|
||||
static double GetGuestVblankHzOverride();
|
||||
|
||||
bool Save(::rex::stream::ByteStream* stream);
|
||||
bool Restore(::rex::stream::ByteStream* stream);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
|
||||
#include "../../../../../src/ac6_backend_fixes/ac6_backend_hooks.h"
|
||||
#include "../../../../../src/ac6_native_graphics.h"
|
||||
#include "../../../../../src/render_hooks.h"
|
||||
|
||||
REXCVAR_DEFINE_BOOL(d3d12_bindless, true, "GPU/D3D12", "Use bindless resources where available")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
@@ -2660,6 +2661,13 @@ bool D3D12CommandProcessor::IssueDraw(xenos::PrimitiveType primitive_type, uint3
|
||||
bool memexport_used_pixel = pixel_shader && (pixel_shader->memexport_eM_written() != 0);
|
||||
bool memexport_used = memexport_used_vertex || memexport_used_pixel;
|
||||
|
||||
// AC6: the world/effects compositor draws once per frame whenever the 3D
|
||||
// world renders (never in the 2D front-end) - stamp it as the "gameplay
|
||||
// world active" signal for the dynamic FPS pacing.
|
||||
if (pixel_shader && pixel_shader->ucode_data_hash() == UINT64_C(0x17e5e4ac3e713245)) {
|
||||
ac6::NotifyWorldCompositorDraw();
|
||||
}
|
||||
|
||||
if (!BeginSubmission(true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+25
-1
@@ -200,7 +200,8 @@ X_STATUS GraphicsSystem::SetupGuestGpu(runtime::FunctionDispatcher* function_dis
|
||||
vsync_worker_running_ = true;
|
||||
vsync_worker_thread_ = system::object_ref<system::XHostThread>(
|
||||
new system::XHostThread(kernel_state_, 128 * 1024, 0, [this, vsync_interval_ticks,
|
||||
no_vsync_interval_ticks]() {
|
||||
no_vsync_interval_ticks,
|
||||
guest_tick_frequency]() {
|
||||
uint64_t last_frame_time = chrono::Clock::QueryGuestTickCount();
|
||||
while (vsync_worker_running_) {
|
||||
uint64_t current_time = chrono::Clock::QueryGuestTickCount();
|
||||
@@ -208,6 +209,17 @@ X_STATUS GraphicsSystem::SetupGuestGpu(runtime::FunctionDispatcher* function_dis
|
||||
? vsync_interval_ticks
|
||||
: (REXCVAR_GET(vsync) ? vsync_interval_ticks
|
||||
: no_vsync_interval_ticks);
|
||||
double vblank_hz_override = GetGuestVblankHzOverride();
|
||||
if (vblank_hz_override > 0.0) {
|
||||
interval_ticks = std::max(
|
||||
uint64_t(1), uint64_t(double(guest_tick_frequency) / vblank_hz_override));
|
||||
}
|
||||
// Re-anchor when far behind so a shrinking interval (an override
|
||||
// switching from a paced rate to a much faster one) or a long stall
|
||||
// does not burst a backlog of MarkVblank calls in one wake.
|
||||
if (current_time - last_frame_time >= interval_ticks * 4) {
|
||||
last_frame_time = current_time - interval_ticks;
|
||||
}
|
||||
while (current_time - last_frame_time >= interval_ticks) {
|
||||
MarkVblank();
|
||||
last_frame_time += interval_ticks;
|
||||
@@ -399,6 +411,18 @@ void GraphicsSystem::DispatchInterruptCallback(uint32_t source, uint32_t cpu) {
|
||||
rex::countof(args));
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::atomic<double> g_guest_vblank_hz_override{0.0};
|
||||
} // namespace
|
||||
|
||||
void GraphicsSystem::SetGuestVblankHzOverride(double hz) {
|
||||
g_guest_vblank_hz_override.store(hz, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
double GraphicsSystem::GetGuestVblankHzOverride() {
|
||||
return g_guest_vblank_hz_override.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void GraphicsSystem::MarkVblank() {
|
||||
// TODO: Enable profiling once ported
|
||||
// SCOPE_profile_cpu_f("gpu");
|
||||
|
||||
Reference in New Issue
Block a user