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) : DxVsyncSource::DxVsyncSource(Pacer* pacer) :
m_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() DxVsyncSource::~DxVsyncSource()
{ {
if (m_Thread != nullptr) { if (m_WaitForVblankEventParams.hAdapter != 0) {
SDL_AtomicSet(&m_Stopping, 1); D3DKMT_CLOSEADAPTER closeAdapterParams = {};
SDL_WaitThread(m_Thread, nullptr); closeAdapterParams.hAdapter = m_WaitForVblankEventParams.hAdapter;
m_D3DKMTCloseAdapter(&closeAdapterParams);
} }
if (m_Gdi32Handle != nullptr) { 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"); m_Gdi32Handle = LoadLibraryA("gdi32.dll");
if (m_Gdi32Handle == nullptr) { if (m_Gdi32Handle == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
@ -59,114 +58,89 @@ bool DxVsyncSource::initialize(SDL_Window* window, int displayFps)
return false; 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); m_Window = info.info.win.window;
if (m_Thread == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Unable to create DX V-sync thread: %s",
SDL_GetError());
return false;
}
return true; 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) void DxVsyncSource::waitForVsync()
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_TIME_CRITICAL); {
#else NTSTATUS status;
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
#endif
D3DKMT_OPENADAPTERFROMHDC openAdapterParams = {}; // If the monitor has changed from last time, open the new adapter
HMONITOR lastMonitor = nullptr; HMONITOR currentMonitor = MonitorFromWindow(m_Window, MONITOR_DEFAULTTONEAREST);
DEVMODEA monitorMode; if (currentMonitor != m_LastMonitor) {
monitorMode.dmSize = sizeof(monitorMode); MONITORINFOEXA monitorInfo = {};
monitorInfo.cbSize = sizeof(monitorInfo);
while (SDL_AtomicGet(&me->m_Stopping) == 0) { if (!GetMonitorInfoA(currentMonitor, &monitorInfo)) {
D3DKMT_WAITFORVERTICALBLANKEVENT waitForVblankEventParams; SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
NTSTATUS status; "GetMonitorInfo() failed: %d",
GetLastError());
// If the monitor has changed from last time, open the new adapter return;
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;
} }
waitForVblankEventParams.hAdapter = openAdapterParams.hAdapter; DEVMODEA monitorMode;
waitForVblankEventParams.hDevice = 0; monitorMode.dmSize = sizeof(monitorMode);
waitForVblankEventParams.VidPnSourceId = openAdapterParams.VidPnSourceId; 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) { if (status != STATUS_SUCCESS) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"D3DKMTWaitForVerticalBlankEvent() failed: %x", "D3DKMTOpenAdapterFromHdc() failed: %x",
status); status);
SDL_Delay(10); return;
continue;
} }
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) { status = m_D3DKMTWaitForVerticalBlankEvent(&m_WaitForVblankEventParams);
D3DKMT_CLOSEADAPTER closeAdapterParams = {}; if (status != STATUS_SUCCESS) {
closeAdapterParams.hAdapter = openAdapterParams.hAdapter; SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
me->m_D3DKMTCloseAdapter(&closeAdapterParams); "D3DKMTWaitForVerticalBlankEvent() failed: %x",
status);
return;
} }
return 0;
} }

View file

@ -35,17 +35,18 @@ public:
virtual ~DxVsyncSource(); 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: private:
static int vsyncThread(void* context);
Pacer* m_Pacer; Pacer* m_Pacer;
SDL_Thread* m_Thread;
SDL_atomic_t m_Stopping;
HMODULE m_Gdi32Handle; HMODULE m_Gdi32Handle;
HWND m_Window; HWND m_Window;
int m_DisplayFps; HMONITOR m_LastMonitor;
D3DKMT_WAITFORVERTICALBLANKEVENT m_WaitForVblankEventParams;
PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc; PFND3DKMTOPENADAPTERFROMHDC m_D3DKMTOpenAdapterFromHdc;
PFND3DKMTCLOSEADAPTER m_D3DKMTCloseAdapter; PFND3DKMTCLOSEADAPTER m_D3DKMTCloseAdapter;

View file

@ -30,6 +30,7 @@
Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) : Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
m_RenderThread(nullptr), m_RenderThread(nullptr),
m_VsyncThread(nullptr),
m_Stopping(false), m_Stopping(false),
m_VsyncSource(nullptr), m_VsyncSource(nullptr),
m_VsyncRenderer(renderer), m_VsyncRenderer(renderer),
@ -42,12 +43,19 @@ Pacer::Pacer(IFFmpegRenderer* renderer, PVIDEO_STATS videoStats) :
Pacer::~Pacer() 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 // Stop V-sync callbacks
delete m_VsyncSource; delete m_VsyncSource;
m_VsyncSource = nullptr; m_VsyncSource = nullptr;
// Stop the render thread // Stop the render thread
m_Stopping = true;
if (m_RenderThread != nullptr) { if (m_RenderThread != nullptr) {
m_RenderQueueNotEmpty.wakeAll(); m_RenderQueueNotEmpty.wakeAll();
SDL_WaitThread(m_RenderThread, nullptr); 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) int Pacer::renderThread(void* context)
{ {
Pacer* me = reinterpret_cast<Pacer*>(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 // Called in an arbitrary thread by the IVsyncSource on V-sync
// or an event synchronized with 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 // Make sure initialize() has been called
SDL_assert(m_MaxVideoFps != 0); SDL_assert(m_MaxVideoFps != 0);
@ -217,7 +258,7 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
if (enablePacing) { if (enablePacing) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, 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); m_DisplayFps, m_MaxVideoFps);
SDL_SysWMinfo info; 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)); SDL_assert(m_VsyncSource != nullptr || !(m_RendererAttributes & RENDERER_ATTRIBUTE_FORCE_PACING));
if (m_VsyncSource != nullptr && !m_VsyncSource->initialize(window, m_DisplayFps)) { 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 { else {
@ -264,6 +308,10 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
m_DisplayFps, m_MaxVideoFps); m_DisplayFps, m_MaxVideoFps);
} }
if (m_VsyncSource != nullptr) {
m_VsyncThread = SDL_CreateThread(Pacer::vsyncThread, "PacerVsync", this);
}
if (m_VsyncRenderer->isRenderThreadSupported()) { if (m_VsyncRenderer->isRenderThreadSupported()) {
m_RenderThread = SDL_CreateThread(Pacer::renderThread, "PacerRender", this); m_RenderThread = SDL_CreateThread(Pacer::renderThread, "PacerRender", this);
} }
@ -271,6 +319,11 @@ bool Pacer::initialize(SDL_Window* window, int maxVideoFps, bool enablePacing)
return true; return true;
} }
void Pacer::signalVsync()
{
m_VsyncSignalled.wakeOne();
}
void Pacer::renderFrame(AVFrame* frame) void Pacer::renderFrame(AVFrame* frame)
{ {
// Count time spent in Pacer's queues // Count time spent in Pacer's queues

View file

@ -11,6 +11,15 @@ class IVsyncSource {
public: public:
virtual ~IVsyncSource() {} virtual ~IVsyncSource() {}
virtual bool initialize(SDL_Window* window, int displayFps) = 0; 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 class Pacer
@ -24,13 +33,17 @@ public:
bool initialize(SDL_Window* window, int maxVideoFps, bool enablePacing); bool initialize(SDL_Window* window, int maxVideoFps, bool enablePacing);
void vsyncCallback(int timeUntilNextVsyncMillis); void signalVsync();
void renderOnMainThread(); void renderOnMainThread();
private: private:
static int vsyncThread(void* context);
static int renderThread(void* context); static int renderThread(void* context);
void handleVsync(int timeUntilNextVsyncMillis);
void enqueueFrameForRenderingAndUnlock(AVFrame* frame); void enqueueFrameForRenderingAndUnlock(AVFrame* frame);
void renderFrame(AVFrame* frame); void renderFrame(AVFrame* frame);
@ -44,7 +57,9 @@ private:
QMutex m_FrameQueueLock; QMutex m_FrameQueueLock;
QWaitCondition m_RenderQueueNotEmpty; QWaitCondition m_RenderQueueNotEmpty;
QWaitCondition m_PacingQueueNotEmpty; QWaitCondition m_PacingQueueNotEmpty;
QWaitCondition m_VsyncSignalled;
SDL_Thread* m_RenderThread; SDL_Thread* m_RenderThread;
SDL_Thread* m_VsyncThread;
bool m_Stopping; bool m_Stopping;
IVsyncSource* m_VsyncSource; IVsyncSource* m_VsyncSource;

View file

@ -10,8 +10,7 @@ WaylandVsyncSource::WaylandVsyncSource(Pacer* pacer)
: m_Pacer(pacer), : m_Pacer(pacer),
m_Display(nullptr), m_Display(nullptr),
m_Surface(nullptr), m_Surface(nullptr),
m_Callback(nullptr), m_Callback(nullptr)
m_LastFrameTime(0)
{ {
} }
@ -24,7 +23,7 @@ WaylandVsyncSource::~WaylandVsyncSource()
} }
} }
bool WaylandVsyncSource::initialize(SDL_Window* window, int displayFps) bool WaylandVsyncSource::initialize(SDL_Window* window, int)
{ {
SDL_SysWMinfo info; SDL_SysWMinfo info;
@ -40,7 +39,6 @@ bool WaylandVsyncSource::initialize(SDL_Window* window, int displayFps)
// Pacer shoud not create us for non-Wayland windows // Pacer shoud not create us for non-Wayland windows
SDL_assert(info.subsystem == SDL_SYSWM_WAYLAND); SDL_assert(info.subsystem == SDL_SYSWM_WAYLAND);
m_DisplayFps = displayFps;
m_Display = info.info.wl.display; m_Display = info.info.wl.display;
m_Surface = info.info.wl.surface; m_Surface = info.info.wl.surface;
@ -52,7 +50,13 @@ bool WaylandVsyncSource::initialize(SDL_Window* window, int displayFps)
return true; 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; auto me = (WaylandVsyncSource*)data;
@ -60,18 +64,12 @@ void WaylandVsyncSource::frameDone(void* data, struct wl_callback* oldCb, uint32
SDL_assert(oldCb == me->m_Callback); SDL_assert(oldCb == me->m_Callback);
wl_callback_destroy(oldCb); wl_callback_destroy(oldCb);
// Register for another callback before invoking Pacer to ensure we don't miss // Wake the Pacer Vsync thread
// a frame callback if Pacer takes too long. me->m_Pacer->signalVsync();
// Register for another callback
me->m_Callback = wl_surface_frame(me->m_Surface); me->m_Callback = wl_surface_frame(me->m_Surface);
wl_callback_add_listener(me->m_Callback, &s_FrameListener, data); wl_callback_add_listener(me->m_Callback, &s_FrameListener, data);
wl_surface_commit(me->m_Surface); wl_surface_commit(me->m_Surface);
wl_display_flush(me->m_Display); 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 ~WaylandVsyncSource();
virtual bool initialize(SDL_Window* window, int displayFps); virtual bool initialize(SDL_Window* window, int displayFps) override;
virtual bool isAsync() override;
private: private:
static void frameDone(void* data, struct wl_callback* oldCb, uint32_t time); 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; static const struct wl_callback_listener s_FrameListener;
Pacer* m_Pacer; Pacer* m_Pacer;
int m_DisplayFps;
wl_display* m_Display; wl_display* m_Display;
wl_surface* m_Surface; wl_surface* m_Surface;
wl_callback* m_Callback; wl_callback* m_Callback;
uint32_t m_LastFrameTime;
}; };