mirror of
https://github.com/sal063/AC6_recomp
synced 2026-09-03 02:23:48 -04:00
Added modern present pacing + dt-snap: smooth fps unlock
Guest swap thread blocks on real GPU delivery (NotifyGuestPresent feeds the delivery counter) + an absolute-deadline limiter at ac6_fps_target, not the vblank grid (no sub-harmonic staircase). dt-snap locks the sim delta to the exact target at steady rate (kills the shake). Guest vblank forced to free-run during gameplay so pacing needs zero config. ac6_unlock_fps is the single master switch, ON by default (folded in timing-hooks/vblank-auto/delta-precision/dt-snap-tolerance AND the flight-model framerate-independence fix).
This commit is contained in:
@@ -109,6 +109,23 @@ class GraphicsSystem : public system::IGraphicsSystem {
|
||||
static void SetGuestVblankHzOverride(double hz);
|
||||
static double GetGuestVblankHzOverride();
|
||||
|
||||
// Modern present pacing: while target_hz > 0, PaceGuestPresent (called by
|
||||
// VdSwap on the guest thread that submits swaps) blocks that thread until
|
||||
// every previously issued swap has been delivered to the presenter (GPU
|
||||
// backpressure, frame latency 1), then rate-limits it to target_hz with an
|
||||
// absolute-deadline limiter. The game then runs at min(target, real GPU
|
||||
// throughput) with uniform frame times - the modern game loop - instead of
|
||||
// quantizing to vblank-grid sub-harmonics (60/30/20) when the GPU cannot
|
||||
// hold the target. Combine with a free-running guest vblank so the vblank
|
||||
// wait never gates. target_hz 0 = off. Process-wide, not per-instance.
|
||||
static void SetGuestPresentPacing(double target_hz);
|
||||
// Called by VdSwap on the swapping guest thread before it submits the next
|
||||
// swap; blocks per SetGuestPresentPacing. No-op while pacing is off.
|
||||
static void PaceGuestPresent();
|
||||
// Called by the command processor whenever a guest frame is actually
|
||||
// delivered to the presenter (guest output refreshed for a swap).
|
||||
static void NotifyGuestPresent();
|
||||
|
||||
bool Save(::rex::stream::ByteStream* stream);
|
||||
bool Restore(::rex::stream::ByteStream* stream);
|
||||
|
||||
|
||||
@@ -2582,6 +2582,12 @@ bool D3D12CommandProcessor::IssueSwapInternal(uint32_t frontbuffer_ptr,
|
||||
return true;
|
||||
});
|
||||
|
||||
if (refreshed) {
|
||||
// A guest frame was actually delivered to the presenter - feed the
|
||||
// delivery-paced present pacing (see GraphicsSystem::NotifyGuestPresent).
|
||||
GraphicsSystem::NotifyGuestPresent();
|
||||
}
|
||||
|
||||
// End the frame even if did not present for any reason (the image refresher
|
||||
// was not called), to prevent leaking per-frame resources.
|
||||
REXGPU_DEBUG("IssueSwap: post-RefreshGuestOutput EndSubmission");
|
||||
|
||||
@@ -13,10 +13,13 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include <rex/cvar.h>
|
||||
@@ -113,6 +116,23 @@ __declspec(dllexport) uint32_t AmdPowerXpressRequestHighPerformance = 1;
|
||||
} // extern "C"
|
||||
#endif // REX_PLATFORM_WIN32
|
||||
|
||||
namespace {
|
||||
// Modern present pacing state (see SetGuestPresentPacing / PaceGuestPresent).
|
||||
// The counters pair the guest's swap submissions (VdSwap) with the frames the
|
||||
// command processor actually delivered to the presenter (NotifyGuestPresent).
|
||||
// PaceGuestPresent blocks the swapping guest thread on that pairing plus an
|
||||
// absolute-deadline frame limiter - GPU backpressure and a rate ceiling, the
|
||||
// modern game loop - instead of pacing the guest off the vblank grid.
|
||||
std::atomic<double> g_present_pacing_target_hz{0.0};
|
||||
std::atomic<uint64_t> g_guest_present_count{0};
|
||||
std::atomic<uint64_t> g_guest_swaps_issued{0};
|
||||
std::mutex g_present_pacing_mutex;
|
||||
std::condition_variable g_present_pacing_cv;
|
||||
// Guarded by g_present_pacing_mutex.
|
||||
std::chrono::steady_clock::time_point g_present_pacing_deadline{};
|
||||
uint64_t g_present_delivery_at_last_timeout = UINT64_MAX;
|
||||
} // namespace
|
||||
|
||||
GraphicsSystem::GraphicsSystem() : vsync_worker_running_(false) {}
|
||||
|
||||
GraphicsSystem::~GraphicsSystem() = default;
|
||||
@@ -423,6 +443,91 @@ double GraphicsSystem::GetGuestVblankHzOverride() {
|
||||
return g_guest_vblank_hz_override.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void GraphicsSystem::SetGuestPresentPacing(double target_hz) {
|
||||
g_present_pacing_target_hz.store(target_hz, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void GraphicsSystem::NotifyGuestPresent() {
|
||||
g_guest_present_count.fetch_add(1, std::memory_order_relaxed);
|
||||
// Empty critical section closes the race with PaceGuestPresent's predicate
|
||||
// check-then-wait, so the notify below cannot fall between them and be lost.
|
||||
{ std::lock_guard<std::mutex> lock(g_present_pacing_mutex); }
|
||||
g_present_pacing_cv.notify_all();
|
||||
}
|
||||
|
||||
void GraphicsSystem::PaceGuestPresent() {
|
||||
const double target_hz = g_present_pacing_target_hz.load(std::memory_order_relaxed);
|
||||
if (target_hz <= 0.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Backpressure: before letting the game submit the next swap, wait until
|
||||
// every previously issued swap has been delivered to the presenter (frame
|
||||
// latency 1). This is what paces the game to the real GPU rate when it
|
||||
// cannot hold target_hz - uniformly, with no vblank grid to alias against.
|
||||
const uint64_t issued = g_guest_swaps_issued.load(std::memory_order_relaxed);
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(g_present_pacing_mutex);
|
||||
const auto pred = [&] {
|
||||
return g_guest_present_count.load(std::memory_order_relaxed) >= issued;
|
||||
};
|
||||
const uint64_t delivered_now = g_guest_present_count.load(std::memory_order_relaxed);
|
||||
// While delivery is known-stalled (swaps are not reaching the presenter -
|
||||
// device loss, experimental swap paths), skip the wait entirely; the
|
||||
// limiter below still runs, degrading this to a plain frame-rate cap.
|
||||
const bool delivery_stalled = g_present_delivery_at_last_timeout != UINT64_MAX &&
|
||||
delivered_now == g_present_delivery_at_last_timeout;
|
||||
if (!delivery_stalled && !pred()) {
|
||||
if (g_present_pacing_cv.wait_for(lock, std::chrono::milliseconds(100), pred)) {
|
||||
g_present_delivery_at_last_timeout = UINT64_MAX;
|
||||
} else {
|
||||
// Timed out: forgive the whole backlog so one undelivered swap cannot
|
||||
// park every future frame on this timeout.
|
||||
g_present_delivery_at_last_timeout =
|
||||
g_guest_present_count.load(std::memory_order_relaxed);
|
||||
g_guest_swaps_issued.store(g_present_delivery_at_last_timeout,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
} else if (!delivery_stalled) {
|
||||
g_present_delivery_at_last_timeout = UINT64_MAX;
|
||||
}
|
||||
}
|
||||
g_guest_swaps_issued.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
// 2) Ceiling: absolute-deadline limiter at target_hz. Deadlines advance by
|
||||
// exactly one interval while the game keeps up (drift-free target rate) and
|
||||
// re-anchor to now when it does not, so a slow stretch accrues no catch-up
|
||||
// debt that would burst frames afterwards.
|
||||
const auto interval = std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
||||
std::chrono::duration<double>(1.0 / target_hz));
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
std::chrono::steady_clock::time_point wake;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_present_pacing_mutex);
|
||||
if (g_present_pacing_deadline < now) {
|
||||
g_present_pacing_deadline = now;
|
||||
}
|
||||
wake = g_present_pacing_deadline;
|
||||
g_present_pacing_deadline = wake + interval;
|
||||
}
|
||||
// Coarse sleep to ~2ms short of the deadline, then spin the remainder for
|
||||
// precision (OS sleep granularity is ~1ms and can overshoot).
|
||||
while (true) {
|
||||
now = std::chrono::steady_clock::now();
|
||||
if (now >= wake) {
|
||||
break;
|
||||
}
|
||||
const auto remaining = wake - now;
|
||||
if (remaining > std::chrono::milliseconds(2)) {
|
||||
rex::thread::Sleep(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(remaining) -
|
||||
std::chrono::milliseconds(2));
|
||||
} else {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GraphicsSystem::MarkVblank() {
|
||||
// TODO: Enable profiling once ported
|
||||
// SCOPE_profile_cpu_f("gpu");
|
||||
|
||||
@@ -430,6 +430,11 @@ void VdSwap_entry(ppc_pvoid_t buffer_ptr, // ptr into primary ringbuffer
|
||||
assert(width);
|
||||
assert(height);
|
||||
|
||||
// Modern present pacing (no-op unless enabled): block this guest thread on
|
||||
// GPU delivery of the previous swap plus the target-rate limiter before the
|
||||
// game may submit the next one. See GraphicsSystem::PaceGuestPresent.
|
||||
rex::graphics::GraphicsSystem::PaceGuestPresent();
|
||||
|
||||
namespace xenos = rex::graphics::xenos;
|
||||
|
||||
xenos::xe_gpu_texture_fetch_t gpu_fetch;
|
||||
|
||||
Reference in New Issue
Block a user