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