Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qdxgivsyncservice.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5#include <QThread>
6#include <QWaitCondition>
7#include <QElapsedTimer>
8#include <QCoreApplication>
9#include <QLoggingCategory>
10#include <QScreen>
11#include <QVarLengthArray>
12#include <QtCore/private/qsystemerror_p.h>
13
15
16Q_STATIC_LOGGING_CATEGORY(lcQpaScreenUpdates, "qt.qpa.screen.updates", QtCriticalMsg);
17
19{
20public:
21 // the HMONITOR is unique (i.e. identifies the output), the IDXGIOutput (the pointer/object itself) is not
23 QDxgiVSyncThread(IDXGIOutput *output, float vsyncIntervalMsReportedForScreen, Callback callback);
24 void stop(); // to be called from a thread that's not this thread
25 void run() override;
26
27private:
28 IDXGIOutput *output;
29 float vsyncIntervalMsReportedForScreen;
30 Callback callback;
31 HMONITOR monitor;
32 QAtomicInt quit;
33 QMutex mutex;
34 QWaitCondition cond;
35};
36
37QDxgiVSyncThread::QDxgiVSyncThread(IDXGIOutput *output, float vsyncIntervalMsReportedForScreen, Callback callback)
38 : output(output),
39 vsyncIntervalMsReportedForScreen(vsyncIntervalMsReportedForScreen),
41{
42 DXGI_OUTPUT_DESC desc;
43 output->GetDesc(&desc);
44 monitor = desc.Monitor;
45}
46
48{
49 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncThread" << this << "for output" << output << "monitor" << monitor << "entered run()";
50 QElapsedTimer timestamp;
51 QElapsedTimer elapsed;
52 timestamp.start();
53 while (!quit.loadAcquire()) {
54 elapsed.start();
55 HRESULT hr = output->WaitForVBlank();
56 if (FAILED(hr) || elapsed.nsecsElapsed() <= 1000000) {
57 // 1 ms minimum; if less than that was spent in WaitForVBlank
58 // (reportedly can happen e.g. when a screen gets powered on/off?),
59 // or it reported an error, do a sleep; spinning unthrottled is
60 // never acceptable
61 QThread::msleep((unsigned long) vsyncIntervalMsReportedForScreen);
62 } else {
63 callback(output, monitor, timestamp.nsecsElapsed());
64 }
65 }
66 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncThread" << this << "is stopping";
67 mutex.lock();
68 cond.wakeOne();
69 mutex.unlock();
70 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncThread" << this << "run() out";
71}
72
74{
75 mutex.lock();
76 qCDebug(lcQpaScreenUpdates) << "Requesting QDxgiVSyncThread stop from thread" << QThread::currentThread() << "on" << this;
77 if (isRunning() && !quit.loadAcquire()) {
78 quit.storeRelease(1);
79 cond.wait(&mutex);
80 }
81 wait();
82 mutex.unlock();
83}
84
85QDxgiVSyncService *QDxgiVSyncService::instance()
86{
87 static QDxgiVSyncService service;
88 return &service;
89}
90
91QDxgiVSyncService::QDxgiVSyncService()
92{
93 qCDebug(lcQpaScreenUpdates) << "New QDxgiVSyncService" << this;
94
95 disableService = qEnvironmentVariableIntValue("QT_D3D_NO_VBLANK_THREAD");
96 if (disableService) {
97 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService disabled by environment";
98 return;
99 }
100}
101
102QDxgiVSyncService::~QDxgiVSyncService()
103{
104 qCDebug(lcQpaScreenUpdates) << "~QDxgiVSyncService" << this;
105
106 // Deadlock is almost guaranteed if we try to clean up here, when the global static is being destructed.
107 // Must have been done earlier.
108 if (dxgiFactory)
109 qWarning("QDxgiVSyncService not destroyed in time");
110}
111
112void QDxgiVSyncService::global_destroy()
113{
114 QDxgiVSyncService *inst = QDxgiVSyncService::instance();
115 inst->cleanupRegistered = false;
116 inst->destroy();
117}
118
119void QDxgiVSyncService::destroy()
120{
121 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService::destroy()";
122
123 if (disableService)
124 return;
125
126 for (auto it = windows.begin(), end = windows.end(); it != end; ++it)
127 cleanupWindowData(&*it);
128 windows.clear();
129
130 teardownDxgi();
131}
132
133void QDxgiVSyncService::teardownDxgi()
134{
135 for (auto it = adapters.begin(), end = adapters.end(); it != end; ++it)
136 cleanupAdapterData(&*it);
137 adapters.clear();
138
139 if (dxgiFactory) {
140 dxgiFactory->Release();
141 dxgiFactory = nullptr;
142 }
143
144 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService DXGI teardown complete";
145}
146
147void QDxgiVSyncService::beginFrame(LUID)
148{
149 QMutexLocker lock(&mutex);
150 if (disableService)
151 return;
152
153 // Handle "the possible need to re-create the factory and re-enumerate
154 // adapters". At the time of writing the QRhi D3D11 and D3D12 backends do
155 // not handle this at all (and rendering does not actually break or stop
156 // just because the factory says !IsCurrent), whereas here it makes more
157 // sense to act since we may want to get rid of threads that are no longer
158 // needed. Keep the adapter IDs and the registered windows, drop everything
159 // else, then start from scratch.
160
161 if (dxgiFactory && !dxgiFactory->IsCurrent()) {
162 qWarning("QDxgiVSyncService: DXGI Factory is no longer Current");
163 QVarLengthArray<LUID, 8> luids;
164 for (auto it = adapters.begin(), end = adapters.end(); it != end; ++it)
165 luids.append(it->luid);
166 for (auto it = windows.begin(), end = windows.end(); it != end; ++it)
167 cleanupWindowData(&*it);
168 lock.unlock();
169 teardownDxgi();
170 for (LUID luid : luids)
171 refAdapter(luid);
172 lock.relock();
173 for (auto it = windows.begin(), end = windows.end(); it != end; ++it)
174 updateWindowData(it.key(), &*it);
175 }
176}
177
178void QDxgiVSyncService::refAdapter(LUID luid)
179{
180 QMutexLocker lock(&mutex);
181 if (disableService)
182 return;
183
184 if (!dxgiFactory) {
185 HRESULT hr = CreateDXGIFactory2(0, __uuidof(IDXGIFactory2), reinterpret_cast<void **>(&dxgiFactory));
186 if (FAILED(hr)) {
187 disableService = true;
188 qWarning("QDxgiVSyncService: CreateDXGIFactory2 failed: %s", qPrintable(QSystemError::windowsComString(hr)));
189 return;
190 }
191 if (!cleanupRegistered) {
192 qAddPostRoutine(QDxgiVSyncService::global_destroy);
193 cleanupRegistered = true;
194 }
195 }
196
197 for (AdapterData &a : adapters) {
198 if (a.luid.LowPart == luid.LowPart && a.luid.HighPart == luid.HighPart) {
199 a.ref += 1;
200 return;
201 }
202 }
203
204 AdapterData a;
205 a.ref = 1;
206 a.luid = luid;
207 a.adapter = nullptr;
208
209 IDXGIAdapter1 *ad;
210 for (int adapterIndex = 0; dxgiFactory->EnumAdapters1(UINT(adapterIndex), &ad) != DXGI_ERROR_NOT_FOUND; ++adapterIndex) {
211 DXGI_ADAPTER_DESC1 desc;
212 ad->GetDesc1(&desc);
213 if (desc.AdapterLuid.LowPart == luid.LowPart && desc.AdapterLuid.HighPart == luid.HighPart) {
214 a.adapter = ad;
215 break;
216 }
217 ad->Release();
218 }
219
220 if (!a.adapter) {
221 qWarning("VSyncService: Failed to find adapter (via EnumAdapters1), skipping");
222 return;
223 }
224
225 adapters.append(a);
226
227 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService refAdapter for not yet seen adapter" << luid.LowPart << luid.HighPart;
228
229 // windows may have been registered before any adapters
230 for (auto it = windows.begin(), end = windows.end(); it != end; ++it)
231 updateWindowData(it.key(), &*it);
232}
233
234void QDxgiVSyncService::derefAdapter(LUID luid)
235{
236 QVarLengthArray<AdapterData, 4> cleanupList;
237
238 {
239 QMutexLocker lock(&mutex);
240 if (disableService)
241 return;
242
243 for (qsizetype i = 0; i < adapters.count(); ++i) {
244 AdapterData &a(adapters[i]);
245 if (a.luid.LowPart == luid.LowPart && a.luid.HighPart == luid.HighPart) {
246 if (!--a.ref) {
247 cleanupList.append(a);
248 adapters.removeAt(i);
249 }
250 break;
251 }
252 }
253 }
254
255 // the lock must *not* be held when triggering cleanup
256 for (AdapterData &a : cleanupList)
257 cleanupAdapterData(&a);
258}
259
260void QDxgiVSyncService::cleanupAdapterData(AdapterData *a)
261{
262 for (auto it = a->notifiers.begin(), end = a->notifiers.end(); it != end; ++it) {
263 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService::cleanupAdapterData(): about to call stop()";
264 it->thread->stop();
265 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService::cleanupAdapterData(): stop() called";
266 delete it->thread;
267 it->output->Release();
268 }
269 a->notifiers.clear();
270
271 a->adapter->Release();
272 a->adapter = nullptr;
273}
274
275void QDxgiVSyncService::cleanupWindowData(WindowData *w)
276{
277 if (w->output) {
278 w->output->Release();
279 w->output = nullptr;
280 }
281}
282
283static IDXGIOutput *outputForWindow(QWindow *w, IDXGIAdapter *adapter)
284{
285 // Generic canonical solution as per
286 // https://learn.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-getcontainingoutput
287 // and
288 // https://learn.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range
289
290 QRect wr = w->geometry();
291 wr = QRect(wr.topLeft() * w->devicePixelRatio(), wr.size() * w->devicePixelRatio());
292 const QPoint center = wr.center();
293 IDXGIOutput *currentOutput = nullptr;
294 IDXGIOutput *output = nullptr;
295 for (UINT i = 0; adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND; ++i) {
296 DXGI_OUTPUT_DESC desc;
297 output->GetDesc(&desc);
298 const RECT r = desc.DesktopCoordinates;
299 const QRect dr(QPoint(r.left, r.top), QPoint(r.right - 1, r.bottom - 1));
300 if (dr.contains(center)) {
301 currentOutput = output;
302 break;
303 } else {
304 output->Release();
305 }
306 }
307 return currentOutput; // has a ref on it, will need Release by caller
308}
309
310void QDxgiVSyncService::updateWindowData(QWindow *window, WindowData *wd)
311{
312 for (auto it = adapters.begin(), end = adapters.end(); it != end; ++it) {
313 IDXGIOutput *output = outputForWindow(window, it->adapter);
314 if (!output)
315 continue;
316
317 // Two windows on the same screen may well return two different
318 // IDXGIOutput pointers due to enumerating outputs every time; always
319 // compare the HMONITOR, not the pointer itself.
320
321 DXGI_OUTPUT_DESC desc;
322 output->GetDesc(&desc);
323
324 if (wd->output && wd->output != output) {
325 if (desc.Monitor == wd->monitor) {
326 output->Release();
327 return;
328 }
329 wd->output->Release();
330 }
331
332 wd->output = output;
333 wd->monitor = desc.Monitor;
334
335 QScreen *screen = window->screen();
336 const qreal refresh = screen ? screen->refreshRate() : 60;
337 wd->reportedRefreshIntervalMs = refresh > 0 ? 1000.0f / float(refresh) : 1000.f / 60.0f;
338
339 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: Output for window" << window
340 << "on the actively used adapters is now" << output
341 << "HMONITOR" << wd->monitor
342 << "refresh" << wd->reportedRefreshIntervalMs;
343
344 if (!it->notifiers.contains(wd->monitor)) {
345 output->AddRef();
346 QDxgiVSyncThread *t = new QDxgiVSyncThread(output, wd->reportedRefreshIntervalMs,
347 [this](IDXGIOutput *, HMONITOR monitor, qint64 timestampNs) {
348 CallbackWindowList w;
349 QMutexLocker lock(&mutex);
350 for (auto it = windows.cbegin(), end = windows.cend(); it != end; ++it) {
351 if (it->output && it->monitor == monitor)
352 w.append(it.key());
353 }
354 if (!w.isEmpty()) {
355#if 0
356 qDebug() << "vsync thread" << QThread::currentThread() << monitor << "window list" << w << timestampNs;
357#endif
358 for (const Callback &cb : std::as_const(callbacks)) {
359 if (cb)
360 cb(w, timestampNs);
361 }
362 }
363 });
364 t->start(QThread::TimeCriticalPriority);
365 it->notifiers.insert(wd->monitor, { wd->output, t });
366 }
367 return;
368 }
369
370 // If we get here, there is no IDXGIOutput and supportsWindow() will return false for this window.
371 // This is perfectly normal when using an adapter such as WARP.
372}
373
374void QDxgiVSyncService::registerWindow(QWindow *window)
375{
376 QMutexLocker lock(&mutex);
377 if (disableService || windows.contains(window))
378 return;
379
380 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: adding window" << window;
381
382 WindowData wd;
383 wd.output = nullptr;
384 updateWindowData(window, &wd);
385 windows.insert(window, wd);
386
387 QObject::connect(window, &QWindow::screenChanged, window, [this, window](QScreen *screen) {
388 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: screen changed for window:" << window << screen;
389 QMutexLocker lock(&mutex);
390 auto it = windows.find(window);
391 if (it != windows.end())
392 updateWindowData(window, &*it);
393 }, Qt::QueuedConnection); // intentionally using Queued
394 // It has been observed that with DirectConnection _sometimes_ we do not
395 // find any IDXGIOutput for the window when moving it to a different screen.
396 // Add a delay by going through the event loop.
397}
398
399void QDxgiVSyncService::unregisterWindow(QWindow *window)
400{
401 QMutexLocker lock(&mutex);
402 auto it = windows.find(window);
403 if (it == windows.end())
404 return;
405
406 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: removing window" << window;
407
408 cleanupWindowData(&*it);
409
410 windows.remove(window);
411}
412
413bool QDxgiVSyncService::supportsWindow(QWindow *window)
414{
415 QMutexLocker lock(&mutex);
416 auto it = windows.constFind(window);
417 return it != windows.cend() ? (it->output != nullptr) : false;
418}
419
420qsizetype QDxgiVSyncService::registerCallback(Callback cb)
421{
422 QMutexLocker lock(&mutex);
423 for (qsizetype i = 0; i < callbacks.count(); ++i) {
424 if (!callbacks[i]) {
425 callbacks[i] = cb;
426 return i + 1;
427 }
428 }
429 callbacks.append(cb);
430 return callbacks.count();
431}
432
433void QDxgiVSyncService::unregisterCallback(qsizetype id)
434{
435 QMutexLocker lock(&mutex);
436 const qsizetype index = id - 1;
437 if (index >= 0 && index < callbacks.count())
438 callbacks[index] = nullptr;
439}
440
441QT_END_NAMESPACE
QDxgiVSyncThread(IDXGIOutput *output, float vsyncIntervalMsReportedForScreen, Callback callback)
void run() override
Combined button and popup list for selecting options.
static IDXGIOutput * outputForWindow(QWindow *w, IDXGIAdapter *adapter)