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 QRect wr = w->geometry();
296 wr = QRect(wr.topLeft() * w->devicePixelRatio(), wr.size() * w->devicePixelRatio());
297 const QPoint center = wr.center();
298 IDXGIOutput *currentOutput = nullptr;
299 IDXGIOutput *output = nullptr;
300 for (UINT i = 0; adapter->EnumOutputs(i, &output) != DXGI_ERROR_NOT_FOUND; ++i) {
301 DXGI_OUTPUT_DESC desc;
302 output->GetDesc(&desc);
303 const RECT r = desc.DesktopCoordinates;
304 const QRect dr(QPoint(r.left, r.top), QPoint(r.right - 1, r.bottom - 1));
305 if (dr.contains(center)) {
306 currentOutput = output;
307 break;
308 } else {
309 output->Release();
310 }
311 }
312 return currentOutput; // has a ref on it, will need Release by caller
313}
314
315void QDxgiVSyncService::updateWindowData(QWindow *window, WindowData *wd)
316{
317 for (auto it = adapters.begin(), end = adapters.end(); it != end; ++it) {
318 IDXGIOutput *output = outputForWindow(window, it->adapter);
319 if (!output)
320 continue;
321
322 // Two windows on the same screen may well return two different
323 // IDXGIOutput pointers due to enumerating outputs every time; always
324 // compare the HMONITOR, not the pointer itself.
325
326 DXGI_OUTPUT_DESC desc;
327 output->GetDesc(&desc);
328
329 if (wd->output && wd->output != output) {
330 if (desc.Monitor == wd->monitor) {
331 output->Release();
332 return;
333 }
334 wd->output->Release();
335 }
336
337 wd->output = output;
338 wd->monitor = desc.Monitor;
339
340 QScreen *screen = window->screen();
341 const qreal refresh = screen ? screen->refreshRate() : 60;
342 wd->reportedRefreshIntervalMs = refresh > 0 ? 1000.0f / float(refresh) : 1000.f / 60.0f;
343
344 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: Output for window" << window
345 << "on the actively used adapters is now" << output
346 << "HMONITOR" << wd->monitor
347 << "refresh" << wd->reportedRefreshIntervalMs;
348
349 if (!it->notifiers.contains(wd->monitor)) {
350 output->AddRef();
351 QDxgiVSyncThread *t = new QDxgiVSyncThread(output, wd->reportedRefreshIntervalMs,
352 [this](IDXGIOutput *, HMONITOR monitor, qint64 timestampNs) {
353 CallbackWindowList w;
354 QMutexLocker lock(&mutex);
355 for (auto it = windows.cbegin(), end = windows.cend(); it != end; ++it) {
356 if (it->output && it->monitor == monitor)
357 w.append(it.key());
358 }
359 if (!w.isEmpty()) {
360#if 0
361 qDebug() << "vsync thread" << QThread::currentThread() << monitor << "window list" << w << timestampNs;
362#endif
363 for (const Callback &cb : std::as_const(callbacks)) {
364 if (cb)
365 cb(w, timestampNs);
366 }
367 }
368 });
369 t->start(QThread::TimeCriticalPriority);
370 it->notifiers.insert(wd->monitor, { wd->output, t });
371 }
372 return;
373 }
374
375 // If we get here, there is no IDXGIOutput and supportsWindow() will return false for this window.
376 // This is perfectly normal when using an adapter such as WARP.
377}
378
379void QDxgiVSyncService::registerWindow(QWindow *window)
380{
381 QMutexLocker lock(&mutex);
382 if (disableService || windows.contains(window))
383 return;
384
385 if (window->format().swapInterval() == 0) {
386 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: window" << window
387 << "has swapInterval 0, skipping this window";
388 return;
389 }
390
391 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: adding window" << window;
392
393 WindowData wd;
394 wd.output = nullptr;
395 updateWindowData(window, &wd);
396 windows.insert(window, wd);
397
398 QObject::connect(window, &QWindow::screenChanged, window, [this, window](QScreen *screen) {
399 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: screen changed for window:" << window << screen;
400 QMutexLocker lock(&mutex);
401 auto it = windows.find(window);
402 if (it != windows.end())
403 updateWindowData(window, &*it);
404 }, Qt::QueuedConnection); // intentionally using Queued
405 // It has been observed that with DirectConnection _sometimes_ we do not
406 // find any IDXGIOutput for the window when moving it to a different screen.
407 // Add a delay by going through the event loop.
408}
409
410void QDxgiVSyncService::unregisterWindow(QWindow *window)
411{
412 QMutexLocker lock(&mutex);
413 auto it = windows.find(window);
414 if (it == windows.end())
415 return;
416
417 qCDebug(lcQpaScreenUpdates) << "QDxgiVSyncService: removing window" << window;
418
419 cleanupWindowData(&*it);
420
421 windows.remove(window);
422}
423
424bool QDxgiVSyncService::supportsWindow(QWindow *window)
425{
426 QMutexLocker lock(&mutex);
427 auto it = windows.constFind(window);
428 return it != windows.cend() ? (it->output != nullptr) : false;
429}
430
431qsizetype QDxgiVSyncService::registerCallback(Callback cb)
432{
433 QMutexLocker lock(&mutex);
434 for (qsizetype i = 0; i < callbacks.count(); ++i) {
435 if (!callbacks[i]) {
436 callbacks[i] = cb;
437 return i + 1;
438 }
439 }
440 callbacks.append(cb);
441 return callbacks.count();
442}
443
444void QDxgiVSyncService::unregisterCallback(qsizetype id)
445{
446 QMutexLocker lock(&mutex);
447 const qsizetype index = id - 1;
448 if (index >= 0 && index < callbacks.count())
449 callbacks[index] = nullptr;
450}
451
452QT_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)