Refactor Pacer to handle both blocking and non-blocking VsyncSources

This commit is contained in:
Cameron Gutman 2022-11-12 15:08:50 -06:00
parent 8e3e19a7f7
commit 6ae6218043
6 changed files with 170 additions and 129 deletions

View file

@ -6,17 +6,18 @@
DxVsyncSource::DxVsyncSource(Pacer* pacer) :
m_Pacer(pacer),
m_Thread(nullptr),
m_Gdi32Handle(nullptr)
m_Gdi32Handle(nullptr),
m_LastMonitor(nullptr)
{
SDL_AtomicSet(&m_Stopping, 0);
SDL_zero(m_WaitForVblankEventParams);
}
DxVsyncSource::~DxVsyncSource()
{
if (m_Thread != nullptr) {
SDL_AtomicSet(&m_Stopping, 1);
SDL_WaitThread(m_Thread, nullptr);
if (m_WaitForVblankEventParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = m_WaitForVblankEventParams.hAdapter;
m_D3DKMTCloseAdapter(&closeAdapterParams);
}
if (m_Gdi32Handle != nullptr) {
@ -24,10 +25,8 @@ DxVsyncSource::~DxVsyncSource()
}
}
bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
bool DxVsyncSource::initialize(SDL_Window* window, int)
{
m_DisplayFps = displayFps;
m_Gdi32Handle = LoadLibraryA("gdi32.dll");
if (m_Gdi32Handle == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@ -59,114 +58,89 @@ bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
return false;
}
m_Window = info.info.win.window;
// Pacer should only create us on Win32
SDL_assert(info.subsystem == SDL_SYSWM_WINDOWS);
m_Thread = SDL_CreateThread(vsyncThread, "DXVsync", this);
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
SDL_GetError());
return false;
}
m_Window = info.info.win.window;
return true;
}
int DxVsyncSource::vsyncThread(void* context)
bool DxVsyncSource::isAsync()
{
DxVsyncSource* me = reinterpret_cast<DxVsyncSource*>(context);
// We wait in the context of the Pacer thread
return false;
}
#if SDL_VERSION_ATLEAST(2, 0, 9)
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
#else
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
#endif
void DxVsyncSource::waitForVsync()
{
NTSTATUS status;
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
HMONITOR lastMonitor = nullptr;
DEVMODEA monitorMode;
monitorMode.dmSize = sizeof(monitorMode);
while (SDL_AtomicGet(&me->m_Stopping) == 0) {
D3DKMT_WAITFORVERTICALBLANKEVENT waitForVblankEventParams;
NTSTATUS status;
// If the monitor has changed from last time, open the new adapter
HMONITOR currentMonitor = MonitorFromWindow(me->m_Window, MONITOR_DEFAULTTONEAREST);
if (currentMonitor != lastMonitor) {
MONITORINFOEXA monitorInfo = {};
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetMonitorInfo() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"EnumDisplaySettings() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Monitor changed: %s %d Hz",
monitorInfo.szDevice,
monitorMode.dmDisplayFrequency);
if (openAdapterParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
}
openAdapterParams.hDc = CreateDCA(nullptr, monitorInfo.szDevice, nullptr, nullptr);
if (!openAdapterParams.hDc) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateDC() failed: %d",
GetLastError());
SDL_Delay(10);
continue;
}
status = me->m_D3DKMTOpenAdapterFromHdc(&openAdapterParams);
DeleteDC(openAdapterParams.hDc);
if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTOpenAdapterFromHdc() failed: %x",
status);
SDL_Delay(10);
continue;
}
lastMonitor = currentMonitor;
// If the monitor has changed from last time, open the new adapter
HMONITOR currentMonitor = MonitorFromWindow(m_Window, MONITOR_DEFAULTTONEAREST);
if (currentMonitor != m_LastMonitor) {
MONITORINFOEXA monitorInfo = {};
monitorInfo.cbSize = sizeof(monitorInfo);
if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"GetMonitorInfo() failed: %d",
GetLastError());
return;
}
waitForVblankEventParams.hAdapter = openAdapterParams.hAdapter;
waitForVblankEventParams.hDevice = 0;
waitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId;
DEVMODEA monitorMode;
monitorMode.dmSize = sizeof(monitorMode);
if (!EnumDisplaySettingsA(monitorInfo.szDevice, ENUM_CURRENT_SETTINGS, &monitorMode)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"EnumDisplaySettings() failed: %d",
GetLastError());
return;
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Monitor changed: %s %d Hz",
monitorInfo.szDevice,
monitorMode.dmDisplayFrequency);
// Close the old adapter
if (m_WaitForVblankEventParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = m_WaitForVblankEventParams.hAdapter;
m_D3DKMTCloseAdapter(&closeAdapterParams);
}
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {};
openAdapterParams.hDc = CreateDCA(nullptr, monitorInfo.szDevice, nullptr, nullptr);
if (!openAdapterParams.hDc) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"CreateDC() failed: %d",
GetLastError());
return;
}
// Open the new adapter
status = m_D3DKMTOpenAdapterFromHdc(&openAdapterParams);
DeleteDC(openAdapterParams.hDc);
status = me->m_D3DKMTWaitForVerticalBlankEvent(&waitForVblankEventParams);
if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTWaitForVerticalBlankEvent() failed: %x",
"D3DKMTOpenAdapterFromHdc() failed: %x",
status);
SDL_Delay(10);
continue;
return;
}
me->m_Pacer->vsyncCallback(1000 / me->m_DisplayFps);
m_WaitForVblankEventParams.hAdapter = openAdapterParams.hAdapter;
m_WaitForVblankEventParams.hDevice = 0;
m_WaitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId;
m_LastMonitor = currentMonitor;
}
if (openAdapterParams.hAdapter != 0) {
D3DKMT_CLOSEADAPTER closeAdapterParams = {};
closeAdapterParams.hAdapter = openAdapterParams.hAdapter;
me->m_D3DKMTCloseAdapter(&closeAdapterParams);
status = m_D3DKMTWaitForVerticalBlankEvent(&m_WaitForVblankEventParams);
if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTWaitForVerticalBlankEvent() failed: %x",
status);
return;
}
return 0;
}

View file

@ -35,17 +35,18 @@ public:
virtual ~DxVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
virtual bool initialize(SDL_Window* window, int) override;
virtual bool isAsync() override;
virtual void waitForVsync() override;
private:
static int vsyncThread(void* context);
Pacer* m_Pacer;
SDL_Thread* m_Thread;
SDL_atomic_t m_Stopping;
HMODULE m_Gdi32Handle;
HWND m_Window;
int m_DisplayFps;
HMONITOR m_LastMonitor;
D3DKMT_WAITFORVERTICALBLANKEVENT m_WaitForVblankEventParams;
PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc;
PFND3DKMTCLOSEADAPTER m_D3DKMTCloseAdapter;

View file

@ -30,6 +30,7 @@
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
m_RenderThread(nullptr),
m_VsyncThread(nullptr),
m_Stopping(false),
m_VsyncSource(nullptr),
m_VsyncRenderer(renderer),
@ -42,12 +43,19 @@ Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
Pacer::~Pacer()
{
m_Stopping = true;
// Stop the V-sync thread
if (m_VsyncThread != nullptr) {
m_VsyncSignalled.wakeAll();
SDL_WaitThread(m_VsyncThread, nullptr);
}
// Stop V-sync callbacks
delete m_VsyncSource;
m_VsyncSource = nullptr;
// Stop the render thread
m_Stopping = true;
if (m_RenderThread != nullptr) {
m_RenderQueueNotEmpty.wakeAll();
SDL_WaitThread(m_RenderThread, nullptr);
@ -89,6 +97,39 @@ void Pacer::renderOnMainThread()
}
}
int Pacer::vsyncThread(void *context)
{
Pacer* me = reinterpret_cast<Pacer*>(context);
#if SDL_VERSION_ATLEAST(2, 0, 9)
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL);
#else
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
#endif
bool async = me->m_VsyncSource->isAsync();
while (!me->m_Stopping) {
if (async) {
// Wait for the VSync source to invoke signalVsync() or 100ms to elapse
me->m_FrameQueueLock.lock();
me->m_VsyncSignalled.wait(&me->m_FrameQueueLock, 100);
me->m_FrameQueueLock.unlock();
}
else {
// Let the VSync source wait in the context of our thread
me->m_VsyncSource->waitForVsync();
}
if (me->m_Stopping) {
break;
}
me->handleVsync(1000 / me->m_DisplayFps);
}
return 0;
}
int Pacer::renderThread(void* context)
{
Pacer* me = reinterpret_cast<Pacer*>(context);
@ -153,7 +194,7 @@ void Pacer::enqueueFrameForRenderingAndUnlock(AVFrame *frame)
// Called in an arbitrary thread by the IVsyncSource on V-sync
// or an event synchronized with V-sync
void Pacer::vsyncCallback(int timeUntilNextVsyncMillis)
void Pacer::handleVsync(int timeUntilNextVsyncMillis)
{
// Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0);
@ -217,7 +258,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
if (enablePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION,
"Frame pacing active: target %d Hz with %d FPS stream",
"Frame pacing: target %d Hz with %d FPS stream",
m_DisplayFps, m_MaxVideoFps);
SDL_SysWMinfo info;
@ -255,7 +296,10 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
SDL_assert(m_VsyncSource != nullptr || !(m_RendererAttributes & RENDERER_ATTRIBUTE_FORCE_PACING));
if (m_VsyncSource != nullptr && !m_VsyncSource->initialize(window, m_DisplayFps)) {
return false;
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Vsync source failed to initialize. Frame pacing will not be available!");
delete m_VsyncSource;
m_VsyncSource = nullptr;
}
}
else {
@ -264,6 +308,10 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
m_DisplayFps, m_MaxVideoFps);
}
if (m_VsyncSource != nullptr) {
m_VsyncThread = SDL_CreateThread(Pacer::vsyncThread, "PacerVsync", this);
}
if (m_VsyncRenderer->isRenderThreadSupported()) {
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "PacerRender", this);
}
@ -271,6 +319,11 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
return true;
}
void Pacer::signalVsync()
{
m_VsyncSignalled.wakeOne();
}
void Pacer::renderFrame(AVFrame* frame)
{
// Count time spent in Pacer's queues

View file

@ -11,6 +11,15 @@ class IVsyncSource {
public:
virtual ~IVsyncSource() {}
virtual bool initialize(SDL_Window* window, int displayFps) = 0;
// Asynchronous sources produce callbacks on their own, while synchronous
// sources require calls to waitForVsync().
virtual bool isAsync() = 0;
virtual void waitForVsync() {
// Synchronous sources must implement waitForVsync()!
SDL_assert(false);
}
};
class Pacer
@ -24,13 +33,17 @@ public:
bool initialize(SDL_Window* window, int maxVideoFps, bool enablePacing);
void vsyncCallback(int timeUntilNextVsyncMillis);
void signalVsync();
void renderOnMainThread();
private:
static int vsyncThread(void* context);
static int renderThread(void* context);
void handleVsync(int timeUntilNextVsyncMillis);
void enqueueFrameForRenderingAndUnlock(AVFrame* frame);
void renderFrame(AVFrame* frame);
@ -44,7 +57,9 @@ private:
QMutex m_FrameQueueLock;
QWaitCondition m_RenderQueueNotEmpty;
QWaitCondition m_PacingQueueNotEmpty;
QWaitCondition m_VsyncSignalled;
SDL_Thread* m_RenderThread;
SDL_Thread* m_VsyncThread;
bool m_Stopping;
IVsyncSource* m_VsyncSource;

View file

@ -10,8 +10,7 @@ WaylandVsyncSource::WaylandVsyncSource(Pacer* pacer)
: m_Pacer(pacer),
m_Display(nullptr),
m_Surface(nullptr),
m_Callback(nullptr),
m_LastFrameTime(0)
m_Callback(nullptr)
{
}
@ -24,7 +23,7 @@ WaylandVsyncSource::~WaylandVsyncSource()
}
}
bool WaylandVsyncSource::initialize(SDL_Window* window, int displayFps)
bool WaylandVsyncSource::initialize(SDL_Window* window, int)
{
SDL_SysWMinfo info;
@ -40,7 +39,6 @@ bool WaylandVsyncSource::initialize(SDL_Window* window, int displayFps)
// Pacer shoud not create us for non-Wayland windows
SDL_assert(info.subsystem == SDL_SYSWM_WAYLAND);
m_DisplayFps = displayFps;
m_Display = info.info.wl.display;
m_Surface = info.info.wl.surface;
@ -52,7 +50,13 @@ bool WaylandVsyncSource::initialize(SDL_Window* window, int displayFps)
return true;
}
void WaylandVsyncSource::frameDone(void* data, struct wl_callback* oldCb, uint32_t time)
bool WaylandVsyncSource::isAsync()
{
// Wayland frame callbacks are asynchronous
return true;
}
void WaylandVsyncSource::frameDone(void* data, struct wl_callback* oldCb, uint32_t)
{
auto me = (WaylandVsyncSource*)data;
@ -60,18 +64,12 @@ void WaylandVsyncSource::frameDone(void* data, struct wl_callback* oldCb, uint32
SDL_assert(oldCb == me->m_Callback);
wl_callback_destroy(oldCb);
// Register for another callback before invoking Pacer to ensure we don't miss
// a frame callback if Pacer takes too long.
// Wake the Pacer Vsync thread
me->m_Pacer->signalVsync();
// Register for another callback
me->m_Callback = wl_surface_frame(me->m_Surface);
wl_callback_add_listener(me->m_Callback, &s_FrameListener, data);
wl_surface_commit(me->m_Surface);
wl_display_flush(me->m_Display);
if (me->m_LastFrameTime != 0) {
// Assuming that the time until the next V-Sync will usually be equal to the
// time from last callback to this one but cap it at 2x the expected frame period.
me->m_Pacer->vsyncCallback(SDL_min(time - me->m_LastFrameTime, 2000 / me->m_DisplayFps));
}
me->m_LastFrameTime = time;
}

View file

@ -12,7 +12,9 @@ public:
virtual ~WaylandVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps);
virtual bool initialize(SDL_Window* window, int displayFps) override;
virtual bool isAsync() override;
private:
static void frameDone(void* data, struct wl_callback* oldCb, uint32_t time);
@ -20,10 +22,8 @@ private:
static const struct wl_callback_listener s_FrameListener;
Pacer* m_Pacer;
int m_DisplayFps;
wl_display* m_Display;
wl_surface* m_Surface;
wl_callback* m_Callback;
uint32_t m_LastFrameTime;
};