mirror of
https://github.com/Awakenchan/LanternEngine.git
synced 2026-11-12 18:08:45 -10:00
302 lines
9.4 KiB
C++
302 lines
9.4 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
#include "lantern_pacing.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <immintrin.h>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
|
|
#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
|
|
#endif
|
|
#else
|
|
#include <time.h>
|
|
#endif
|
|
|
|
namespace LanternPacing {
|
|
namespace {
|
|
using namespace std::chrono_literals;
|
|
|
|
struct Lateness {
|
|
static constexpr std::size_t Size = 32;
|
|
std::array<std::atomic<std::uint32_t>, Size> us{};
|
|
std::atomic<std::uint32_t> next{0};
|
|
|
|
Lateness() {
|
|
for (auto& v : us) {
|
|
v.store(300, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
void Add(std::chrono::nanoseconds late) {
|
|
const auto value = std::clamp<std::int64_t>(late.count() / 1000, 0, 4000);
|
|
us[next.fetch_add(1, std::memory_order_relaxed) % Size].store(
|
|
static_cast<std::uint32_t>(value), std::memory_order_relaxed);
|
|
}
|
|
std::chrono::nanoseconds Margin() const {
|
|
std::array<std::uint32_t, Size> copy;
|
|
for (std::size_t i = 0; i < Size; ++i) {
|
|
copy[i] = us[i].load(std::memory_order_relaxed);
|
|
}
|
|
constexpr std::size_t P90 = Size * 9 / 10;
|
|
std::nth_element(copy.begin(), copy.begin() + P90, copy.end());
|
|
return std::chrono::microseconds(copy[P90] + 50);
|
|
}
|
|
};
|
|
Lateness lateness;
|
|
|
|
#ifdef _WIN32
|
|
HANDLE ThreadTimer() {
|
|
thread_local const HANDLE timer = [] {
|
|
HANDLE t = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
|
|
TIMER_ALL_ACCESS);
|
|
return t ? t : CreateWaitableTimerExW(nullptr, nullptr, 0, TIMER_ALL_ACCESS);
|
|
}();
|
|
return timer;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool Enabled() {
|
|
static const bool on = [] {
|
|
const char* env = std::getenv("BB_PRECISE_PACING");
|
|
if (env && env[0]) {
|
|
return !(env[0] == '0' || env[0] == 'n' || env[0] == 'N' || env[0] == 'f' ||
|
|
env[0] == 'F');
|
|
}
|
|
#ifdef _WIN32
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}();
|
|
return on;
|
|
}
|
|
|
|
bool Sleep(std::chrono::nanoseconds duration, bool alertable) {
|
|
if (duration <= 0ns) {
|
|
std::this_thread::yield();
|
|
return true;
|
|
}
|
|
#ifdef _WIN32
|
|
if (const HANDLE timer = ThreadTimer()) {
|
|
LARGE_INTEGER due{.QuadPart = -std::max<LONGLONG>(1, duration.count() / 100)};
|
|
if (SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) {
|
|
return WaitForSingleObjectEx(timer, INFINITE, alertable ? TRUE : FALSE) ==
|
|
WAIT_OBJECT_0;
|
|
}
|
|
}
|
|
return SleepEx(static_cast<DWORD>(std::max<std::int64_t>(1, duration.count() / 1'000'000)),
|
|
alertable ? TRUE : FALSE) == 0;
|
|
#else
|
|
timespec ts{static_cast<time_t>(duration.count() / 1'000'000'000),
|
|
static_cast<long>(duration.count() % 1'000'000'000)};
|
|
return nanosleep(&ts, nullptr) == 0;
|
|
#endif
|
|
}
|
|
|
|
namespace {
|
|
struct VblankWatch {
|
|
std::mutex mutex;
|
|
std::wstring device;
|
|
unsigned generation = 0;
|
|
bool started = false;
|
|
std::atomic<std::int64_t> last_ns{0}, period_ns{0};
|
|
std::atomic<bool> steady{false};
|
|
};
|
|
VblankWatch& Watch() {
|
|
static VblankWatch watch;
|
|
return watch;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
using KmtHandle = UINT;
|
|
struct KmtOpenAdapterFromHdc {
|
|
HDC hdc;
|
|
KmtHandle adapter;
|
|
LUID luid;
|
|
UINT source;
|
|
};
|
|
struct KmtWaitForVblank {
|
|
KmtHandle adapter;
|
|
KmtHandle device;
|
|
UINT source;
|
|
};
|
|
struct KmtCloseAdapter {
|
|
KmtHandle adapter;
|
|
};
|
|
using KmtOpen = LONG(APIENTRY*)(KmtOpenAdapterFromHdc*);
|
|
using KmtWait = LONG(APIENTRY*)(const KmtWaitForVblank*);
|
|
using KmtClose = LONG(APIENTRY*)(const KmtCloseAdapter*);
|
|
|
|
void WatchVblanks() {
|
|
const HMODULE gdi = LoadLibraryW(L"gdi32.dll");
|
|
const auto open = gdi ? reinterpret_cast<KmtOpen>(GetProcAddress(gdi, "D3DKMTOpenAdapterFromHdc")) : nullptr;
|
|
const auto wait = gdi ? reinterpret_cast<KmtWait>(GetProcAddress(gdi, "D3DKMTWaitForVerticalBlankEvent")) : nullptr;
|
|
const auto close = gdi ? reinterpret_cast<KmtClose>(GetProcAddress(gdi, "D3DKMTCloseAdapter")) : nullptr;
|
|
if (!open || !wait || !close) {
|
|
std::printf("VideoOut: the display's vblanks cannot be followed (no D3DKMT)\n");
|
|
return;
|
|
}
|
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
|
auto& w = Watch();
|
|
std::wstring device;
|
|
unsigned generation = ~0u;
|
|
KmtWaitForVblank target{};
|
|
bool opened = false, variable = false, was_steady = false;
|
|
double period = 0, predicted = 0, base = 0, refreshes = 0;
|
|
int off_score = 0, failures = 0;
|
|
const auto restart = [&](double now) {
|
|
period = refreshes = 0;
|
|
predicted = base = now;
|
|
off_score = 0;
|
|
};
|
|
const auto close_adapter = [&] {
|
|
if (opened) {
|
|
const KmtCloseAdapter c{target.adapter};
|
|
close(&c);
|
|
opened = false;
|
|
}
|
|
};
|
|
for (;;) {
|
|
{
|
|
std::scoped_lock lock{w.mutex};
|
|
if (w.generation != generation) {
|
|
generation = w.generation;
|
|
device = w.device;
|
|
close_adapter();
|
|
variable = was_steady = false;
|
|
}
|
|
}
|
|
if (!opened) {
|
|
w.steady.store(false, std::memory_order_relaxed);
|
|
restart(0);
|
|
HDC dc = CreateDCW(nullptr, device.c_str(), nullptr, nullptr);
|
|
if (!dc) {
|
|
dc = CreateDCW(device.c_str(), nullptr, nullptr, nullptr);
|
|
}
|
|
KmtOpenAdapterFromHdc o{dc, 0, {}, 0};
|
|
opened = dc && open(&o) == 0;
|
|
if (dc) {
|
|
DeleteDC(dc);
|
|
}
|
|
if (!opened) {
|
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
|
continue;
|
|
}
|
|
target = {o.adapter, 0, o.source};
|
|
}
|
|
if (wait(&target) != 0) {
|
|
close_adapter();
|
|
if (++failures == 20) {
|
|
std::printf("VideoOut: the display's vblanks cannot be followed (the wait fails)\n");
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(failures < 20 ? 50 : 1000));
|
|
continue;
|
|
}
|
|
failures = 0;
|
|
const double now = double(Clock::now().time_since_epoch().count());
|
|
if (predicted == 0) {
|
|
restart(now);
|
|
continue;
|
|
}
|
|
const double interval = now - predicted;
|
|
if (period == 0) {
|
|
period = interval;
|
|
refreshes = 1;
|
|
predicted = now;
|
|
continue;
|
|
}
|
|
const double whole = std::round(interval / period);
|
|
if (whole < 1 || std::abs(interval - whole * period) > period * 0.1) {
|
|
if (whole >= 1) {
|
|
predicted += whole * period;
|
|
refreshes += whole;
|
|
} else {
|
|
predicted = now;
|
|
}
|
|
if ((off_score += 2) >= 16) {
|
|
if (was_steady && !variable) {
|
|
std::printf("VideoOut: the display's refresh varies (G-Sync, FreeSync or a mode "
|
|
"change): flips are not locked to its vblanks\n");
|
|
}
|
|
variable = variable || was_steady;
|
|
restart(now);
|
|
}
|
|
} else {
|
|
off_score = std::max(0, off_score - 1);
|
|
refreshes += whole;
|
|
period = (now - base) / refreshes;
|
|
predicted += whole * period;
|
|
predicted += (now - predicted) * 0.1;
|
|
}
|
|
const bool steady = period > 0 && refreshes >= 120 && off_score <= 2;
|
|
was_steady = was_steady || steady;
|
|
w.period_ns.store(std::int64_t(period), std::memory_order_relaxed);
|
|
w.last_ns.store(std::int64_t(predicted), std::memory_order_relaxed);
|
|
w.steady.store(steady && !variable, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
bool VblankPacing() {
|
|
static const bool on = [] {
|
|
const char* env = std::getenv("BB_VBLANK_PACING");
|
|
return Enabled() && !(env && env[0] == '0');
|
|
}();
|
|
return on;
|
|
}
|
|
|
|
void WatchVblank(const wchar_t* device) {
|
|
#ifdef _WIN32
|
|
if (!VblankPacing() || !device || !device[0]) {
|
|
return;
|
|
}
|
|
auto& w = Watch();
|
|
std::scoped_lock lock{w.mutex};
|
|
w.device = device;
|
|
++w.generation;
|
|
if (!w.started) {
|
|
w.started = true;
|
|
std::thread(WatchVblanks).detach();
|
|
}
|
|
#else
|
|
(void)device;
|
|
#endif
|
|
}
|
|
|
|
std::optional<VblankTime> Vblank() {
|
|
const auto& w = Watch();
|
|
if (!w.steady.load(std::memory_order_relaxed)) {
|
|
return std::nullopt;
|
|
}
|
|
return VblankTime{Clock::time_point(Clock::duration(w.last_ns.load(std::memory_order_relaxed))),
|
|
std::chrono::nanoseconds(w.period_ns.load(std::memory_order_relaxed))};
|
|
}
|
|
|
|
void WaitUntil(Clock::time_point deadline, bool exact, std::chrono::nanoseconds max_spin) {
|
|
const auto margin =
|
|
exact ? std::clamp<std::chrono::nanoseconds>(lateness.Margin(), 100us,
|
|
std::max<std::chrono::nanoseconds>(max_spin, 100us))
|
|
: 0ns;
|
|
const auto start = Clock::now();
|
|
if (const auto left = deadline - start; left > margin) {
|
|
const auto sleep = std::chrono::duration_cast<std::chrono::nanoseconds>(left - margin);
|
|
if (Sleep(sleep)) {
|
|
lateness.Add(Clock::now() - start - sleep);
|
|
}
|
|
}
|
|
while (Clock::now() < deadline) {
|
|
_mm_pause();
|
|
}
|
|
}
|
|
}
|