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