1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include <QtGui/QVulkanInstance> |
30 | #include <QtGui/QVulkanFunctions> |
31 | #include <QtGui/QVulkanWindow> |
32 | |
33 | #include <QtTest/QtTest> |
34 | |
35 | #include <QSignalSpy> |
36 | |
37 | class tst_QVulkan : public QObject |
38 | { |
39 | Q_OBJECT |
40 | |
41 | private slots: |
42 | void vulkanInstance(); |
43 | void vulkanCheckSupported(); |
44 | void vulkanPlainWindow(); |
45 | void vulkanVersionRequest(); |
46 | void vulkanWindow(); |
47 | void vulkanWindowRenderer(); |
48 | void vulkanWindowGrab(); |
49 | }; |
50 | |
51 | void tst_QVulkan::vulkanInstance() |
52 | { |
53 | QVulkanInstance inst; |
54 | if (!inst.create()) |
55 | QSKIP("Vulkan init failed; skip" ); |
56 | |
57 | QVERIFY(inst.isValid()); |
58 | QVERIFY(inst.vkInstance() != VK_NULL_HANDLE); |
59 | QVERIFY(inst.functions()); |
60 | QVERIFY(!inst.flags().testFlag(QVulkanInstance::NoDebugOutputRedirect)); |
61 | |
62 | inst.destroy(); |
63 | |
64 | QVERIFY(!inst.isValid()); |
65 | QVERIFY(inst.handle() == nullptr); |
66 | |
67 | inst.setFlags(QVulkanInstance::NoDebugOutputRedirect); |
68 | // pass a bogus layer and extension |
69 | inst.setExtensions(QByteArrayList() << "abcdefg" << "notanextension" ); |
70 | inst.setLayers(QByteArrayList() << "notalayer" ); |
71 | QVERIFY(inst.create()); |
72 | |
73 | QVERIFY(inst.isValid()); |
74 | QVERIFY(inst.vkInstance() != VK_NULL_HANDLE); |
75 | QVERIFY(inst.handle() != nullptr); |
76 | QVERIFY(inst.functions()); |
77 | QVERIFY(inst.flags().testFlag(QVulkanInstance::NoDebugOutputRedirect)); |
78 | QVERIFY(!inst.extensions().contains("abcdefg" )); |
79 | QVERIFY(!inst.extensions().contains("notanextension" )); |
80 | QVERIFY(!inst.extensions().contains("notalayer" )); |
81 | // at least the surface extensions should be there however |
82 | QVERIFY(inst.extensions().contains("VK_KHR_surface" )); |
83 | |
84 | QVERIFY(inst.getInstanceProcAddr("vkGetDeviceQueue" )); |
85 | } |
86 | |
87 | void tst_QVulkan::vulkanCheckSupported() |
88 | { |
89 | // Test the early calls to supportedLayers/extensions that need the library |
90 | // and some basics, but do not initialize the instance. |
91 | QVulkanInstance inst; |
92 | QVERIFY(!inst.isValid()); |
93 | |
94 | QVulkanInfoVector<QVulkanLayer> vl = inst.supportedLayers(); |
95 | qDebug() << vl; |
96 | QVERIFY(!inst.isValid()); |
97 | |
98 | QVulkanInfoVector<QVulkanExtension> ve = inst.supportedExtensions(); |
99 | qDebug() << ve; |
100 | QVERIFY(!inst.isValid()); |
101 | |
102 | if (inst.create()) { // skip the rest when Vulkan is not supported at all |
103 | QVERIFY(!ve.isEmpty()); |
104 | QVERIFY(ve == inst.supportedExtensions()); |
105 | } |
106 | } |
107 | |
108 | void tst_QVulkan::vulkanPlainWindow() |
109 | { |
110 | QVulkanInstance inst; |
111 | if (!inst.create()) |
112 | QSKIP("Vulkan init failed; skip" ); |
113 | |
114 | QWindow w; |
115 | w.setSurfaceType(QSurface::VulkanSurface); |
116 | w.setVulkanInstance(&inst); |
117 | w.resize(w: 1024, h: 768); |
118 | w.show(); |
119 | QVERIFY(QTest::qWaitForWindowExposed(&w)); |
120 | |
121 | QCOMPARE(w.vulkanInstance(), &inst); |
122 | |
123 | VkSurfaceKHR surface = QVulkanInstance::surfaceForWindow(window: &w); |
124 | QVERIFY(surface != VK_NULL_HANDLE); |
125 | |
126 | // exercise supportsPresent (and QVulkanFunctions) a bit |
127 | QVulkanFunctions *f = inst.functions(); |
128 | VkPhysicalDevice physDev; |
129 | uint32_t count = 1; |
130 | VkResult err = f->vkEnumeratePhysicalDevices(inst.vkInstance(), &count, &physDev); |
131 | if (err != VK_SUCCESS) |
132 | QSKIP("No physical devices; skip" ); |
133 | |
134 | VkPhysicalDeviceProperties physDevProps; |
135 | f->vkGetPhysicalDeviceProperties(physDev, &physDevProps); |
136 | qDebug(msg: "Device name: %s Driver version: %d.%d.%d" , physDevProps.deviceName, |
137 | VK_VERSION_MAJOR(physDevProps.driverVersion), VK_VERSION_MINOR(physDevProps.driverVersion), |
138 | VK_VERSION_PATCH(physDevProps.driverVersion)); |
139 | |
140 | bool supports = inst.supportsPresent(physicalDevice: physDev, queueFamilyIndex: 0, window: &w); |
141 | qDebug(msg: "queue family 0 supports presenting to window = %d" , supports); |
142 | } |
143 | |
144 | void tst_QVulkan::vulkanVersionRequest() |
145 | { |
146 | QVulkanInstance inst; |
147 | if (!inst.create()) |
148 | QSKIP("Vulkan init failed; skip" ); |
149 | |
150 | // Now that we know Vulkan is functional, check the requested apiVersion is |
151 | // passed to vkCreateInstance as expected. |
152 | |
153 | inst.destroy(); |
154 | |
155 | inst.setApiVersion(QVersionNumber(10, 0, 0)); |
156 | |
157 | bool result = inst.create(); |
158 | |
159 | // Starting with Vulkan 1.1 the spec does not allow the implementation to |
160 | // fail the instance creation. So check for the 1.0 behavior only when |
161 | // create() failed, skip this verification with 1.1+ (where create() will |
162 | // succeed for any bogus api version). |
163 | if (!result) |
164 | QCOMPARE(inst.errorCode(), VK_ERROR_INCOMPATIBLE_DRIVER); |
165 | } |
166 | |
167 | static void waitForUnexposed(QWindow *w) |
168 | { |
169 | QElapsedTimer timer; |
170 | timer.start(); |
171 | while (w->isExposed()) { |
172 | int remaining = 5000 - int(timer.elapsed()); |
173 | if (remaining <= 0) |
174 | break; |
175 | QCoreApplication::processEvents(flags: QEventLoop::AllEvents, maxtime: remaining); |
176 | QCoreApplication::sendPostedEvents(receiver: nullptr, event_type: QEvent::DeferredDelete); |
177 | QTest::qSleep(ms: 10); |
178 | } |
179 | } |
180 | |
181 | void tst_QVulkan::vulkanWindow() |
182 | { |
183 | QVulkanInstance inst; |
184 | if (!inst.create()) |
185 | QSKIP("Vulkan init failed; skip" ); |
186 | |
187 | // First let's forget to set the instance. |
188 | QVulkanWindow w; |
189 | QVERIFY(!w.isValid()); |
190 | w.resize(w: 1024, h: 768); |
191 | w.show(); |
192 | QVERIFY(QTest::qWaitForWindowExposed(&w)); |
193 | QVERIFY(!w.isValid()); |
194 | |
195 | // Now set it. A simple hide - show should be enough to correct, this, no |
196 | // need for a full destroy - create. |
197 | w.hide(); |
198 | waitForUnexposed(w: &w); |
199 | w.setVulkanInstance(&inst); |
200 | QVector<VkPhysicalDeviceProperties> pdevs = w.availablePhysicalDevices(); |
201 | if (pdevs.isEmpty()) |
202 | QSKIP("No Vulkan physical devices; skip" ); |
203 | w.show(); |
204 | QVERIFY(QTest::qWaitForWindowExposed(&w)); |
205 | QVERIFY(w.isValid()); |
206 | QCOMPARE(w.vulkanInstance(), &inst); |
207 | QVulkanInfoVector<QVulkanExtension> exts = w.supportedDeviceExtensions(); |
208 | |
209 | // Now destroy and recreate. |
210 | w.destroy(); |
211 | waitForUnexposed(w: &w); |
212 | QVERIFY(!w.isValid()); |
213 | // check that flags can be set between a destroy() - show() |
214 | w.setFlags(QVulkanWindow::PersistentResources); |
215 | // supported lists can be queried before expose too |
216 | QVERIFY(w.supportedDeviceExtensions() == exts); |
217 | w.show(); |
218 | QVERIFY(QTest::qWaitForWindowExposed(&w)); |
219 | QVERIFY(w.isValid()); |
220 | QVERIFY(w.flags().testFlag(QVulkanWindow::PersistentResources)); |
221 | |
222 | QVERIFY(w.physicalDevice() != VK_NULL_HANDLE); |
223 | QVERIFY(w.physicalDeviceProperties() != nullptr); |
224 | QVERIFY(w.device() != VK_NULL_HANDLE); |
225 | QVERIFY(w.graphicsQueue() != VK_NULL_HANDLE); |
226 | QVERIFY(w.graphicsCommandPool() != VK_NULL_HANDLE); |
227 | QVERIFY(w.defaultRenderPass() != VK_NULL_HANDLE); |
228 | |
229 | QVERIFY(w.concurrentFrameCount() > 0); |
230 | QVERIFY(w.concurrentFrameCount() <= QVulkanWindow::MAX_CONCURRENT_FRAME_COUNT); |
231 | } |
232 | |
233 | class TestVulkanRenderer; |
234 | |
235 | class TestVulkanWindow : public QVulkanWindow |
236 | { |
237 | public: |
238 | QVulkanWindowRenderer *createRenderer() override; |
239 | |
240 | private: |
241 | TestVulkanRenderer *m_renderer = nullptr; |
242 | }; |
243 | |
244 | struct TestVulkan { |
245 | int preInitResCount = 0; |
246 | int initResCount = 0; |
247 | int initSwcResCount = 0; |
248 | int releaseResCount = 0; |
249 | int releaseSwcResCount = 0; |
250 | int startNextFrameCount = 0; |
251 | } testVulkan; |
252 | |
253 | class TestVulkanRenderer : public QVulkanWindowRenderer |
254 | { |
255 | public: |
256 | TestVulkanRenderer(QVulkanWindow *w) : m_window(w) { } |
257 | |
258 | void preInitResources() override; |
259 | void initResources() override; |
260 | void initSwapChainResources() override; |
261 | void releaseSwapChainResources() override; |
262 | void releaseResources() override; |
263 | |
264 | void startNextFrame() override; |
265 | |
266 | private: |
267 | QVulkanWindow *m_window; |
268 | QVulkanDeviceFunctions *m_devFuncs; |
269 | }; |
270 | |
271 | void TestVulkanRenderer::preInitResources() |
272 | { |
273 | if (testVulkan.initResCount) { |
274 | qWarning(msg: "initResources called before preInitResources?!" ); |
275 | testVulkan.preInitResCount = -1; |
276 | return; |
277 | } |
278 | |
279 | // Ensure the physical device and the surface are available at this stage. |
280 | VkPhysicalDevice physDev = m_window->physicalDevice(); |
281 | if (physDev == VK_NULL_HANDLE) { |
282 | qWarning(msg: "No physical device in preInitResources" ); |
283 | testVulkan.preInitResCount = -1; |
284 | return; |
285 | } |
286 | VkSurfaceKHR surface = m_window->vulkanInstance()->surfaceForWindow(window: m_window); |
287 | if (surface == VK_NULL_HANDLE) { |
288 | qWarning(msg: "No surface in preInitResources" ); |
289 | testVulkan.preInitResCount = -1; |
290 | return; |
291 | } |
292 | |
293 | ++testVulkan.preInitResCount; |
294 | } |
295 | |
296 | void TestVulkanRenderer::initResources() |
297 | { |
298 | m_devFuncs = m_window->vulkanInstance()->deviceFunctions(device: m_window->device()); |
299 | ++testVulkan.initResCount; |
300 | } |
301 | |
302 | void TestVulkanRenderer::initSwapChainResources() |
303 | { |
304 | ++testVulkan.initSwcResCount; |
305 | } |
306 | |
307 | void TestVulkanRenderer::releaseSwapChainResources() |
308 | { |
309 | ++testVulkan.releaseSwcResCount; |
310 | } |
311 | |
312 | void TestVulkanRenderer::releaseResources() |
313 | { |
314 | ++testVulkan.releaseResCount; |
315 | } |
316 | |
317 | void TestVulkanRenderer::startNextFrame() |
318 | { |
319 | ++testVulkan.startNextFrameCount; |
320 | |
321 | VkClearColorValue clearColor = { 0, 1, 0, 1 }; |
322 | VkClearDepthStencilValue clearDS = { .depth: 1, .stencil: 0 }; |
323 | VkClearValue clearValues[2]; |
324 | memset(s: clearValues, c: 0, n: sizeof(clearValues)); |
325 | clearValues[0].color = clearColor; |
326 | clearValues[1].depthStencil = clearDS; |
327 | |
328 | VkRenderPassBeginInfo rpBeginInfo; |
329 | memset(s: &rpBeginInfo, c: 0, n: sizeof(rpBeginInfo)); |
330 | rpBeginInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; |
331 | rpBeginInfo.renderPass = m_window->defaultRenderPass(); |
332 | rpBeginInfo.framebuffer = m_window->currentFramebuffer(); |
333 | const QSize sz = m_window->swapChainImageSize(); |
334 | rpBeginInfo.renderArea.extent.width = sz.width(); |
335 | rpBeginInfo.renderArea.extent.height = sz.height(); |
336 | rpBeginInfo.clearValueCount = 2; |
337 | rpBeginInfo.pClearValues = clearValues; |
338 | VkCommandBuffer cmdBuf = m_window->currentCommandBuffer(); |
339 | m_devFuncs->vkCmdBeginRenderPass(cmdBuf, &rpBeginInfo, VK_SUBPASS_CONTENTS_INLINE); |
340 | |
341 | m_devFuncs->vkCmdEndRenderPass(cmdBuf); |
342 | |
343 | m_window->frameReady(); |
344 | } |
345 | |
346 | QVulkanWindowRenderer *TestVulkanWindow::createRenderer() |
347 | { |
348 | Q_ASSERT(!m_renderer); |
349 | m_renderer = new TestVulkanRenderer(this); |
350 | return m_renderer; |
351 | } |
352 | |
353 | void tst_QVulkan::vulkanWindowRenderer() |
354 | { |
355 | QVulkanInstance inst; |
356 | if (!inst.create()) |
357 | QSKIP("Vulkan init failed; skip" ); |
358 | |
359 | testVulkan = TestVulkan(); |
360 | |
361 | TestVulkanWindow w; |
362 | w.setVulkanInstance(&inst); |
363 | w.resize(w: 1024, h: 768); |
364 | w.show(); |
365 | QVERIFY(QTest::qWaitForWindowExposed(&w)); |
366 | |
367 | if (w.availablePhysicalDevices().isEmpty()) |
368 | QSKIP("No Vulkan physical devices; skip" ); |
369 | |
370 | QVERIFY(testVulkan.preInitResCount == 1); |
371 | QVERIFY(testVulkan.initResCount == 1); |
372 | QVERIFY(testVulkan.initSwcResCount == 1); |
373 | // this has to be QTRY due to the async update in QVulkanWindowPrivate::ensureStarted() |
374 | QTRY_VERIFY(testVulkan.startNextFrameCount >= 1); |
375 | |
376 | QVERIFY(!w.swapChainImageSize().isEmpty()); |
377 | QVERIFY(w.colorFormat() != VK_FORMAT_UNDEFINED); |
378 | QVERIFY(w.depthStencilFormat() != VK_FORMAT_UNDEFINED); |
379 | |
380 | w.destroy(); |
381 | waitForUnexposed(w: &w); |
382 | QVERIFY(testVulkan.releaseSwcResCount == 1); |
383 | QVERIFY(testVulkan.releaseResCount == 1); |
384 | } |
385 | |
386 | void tst_QVulkan::vulkanWindowGrab() |
387 | { |
388 | QVulkanInstance inst; |
389 | inst.setLayers(QByteArrayList() << "VK_LAYER_LUNARG_standard_validation" ); |
390 | if (!inst.create()) |
391 | QSKIP("Vulkan init failed; skip" ); |
392 | |
393 | testVulkan = TestVulkan(); |
394 | |
395 | TestVulkanWindow w; |
396 | w.setVulkanInstance(&inst); |
397 | w.resize(w: 1024, h: 768); |
398 | w.show(); |
399 | QVERIFY(QTest::qWaitForWindowExposed(&w)); |
400 | |
401 | if (w.availablePhysicalDevices().isEmpty()) |
402 | QSKIP("No Vulkan physical devices; skip" ); |
403 | |
404 | if (!w.supportsGrab()) |
405 | QSKIP("No grab support; skip" ); |
406 | |
407 | QVERIFY(!w.swapChainImageSize().isEmpty()); |
408 | |
409 | QImage img1 = w.grab(); |
410 | QImage img2 = w.grab(); |
411 | QImage img3 = w.grab(); |
412 | |
413 | QVERIFY(!img1.isNull()); |
414 | QVERIFY(!img2.isNull()); |
415 | QVERIFY(!img3.isNull()); |
416 | |
417 | QCOMPARE(img1.size(), w.swapChainImageSize()); |
418 | QCOMPARE(img2.size(), w.swapChainImageSize()); |
419 | QCOMPARE(img3.size(), w.swapChainImageSize()); |
420 | |
421 | QRgb a = img1.pixel(x: 10, y: 20); |
422 | QRgb b = img2.pixel(x: 5, y: 5); |
423 | QRgb c = img3.pixel(x: 50, y: 30); |
424 | |
425 | QCOMPARE(a, b); |
426 | QCOMPARE(b, c); |
427 | QRgb refPixel = qRgb(r: 0, g: 255, b: 0); |
428 | |
429 | int redFuzz = qAbs(t: qRed(rgb: a) - qRed(rgb: refPixel)); |
430 | int greenFuzz = qAbs(t: qGreen(rgb: a) - qGreen(rgb: refPixel)); |
431 | int blueFuzz = qAbs(t: qBlue(rgb: a) - qBlue(rgb: refPixel)); |
432 | |
433 | QVERIFY(redFuzz <= 1); |
434 | QVERIFY(blueFuzz <= 1); |
435 | QVERIFY(greenFuzz <= 1); |
436 | |
437 | w.destroy(); |
438 | } |
439 | |
440 | QTEST_MAIN(tst_QVulkan) |
441 | |
442 | #include "tst_qvulkan.moc" |
443 | |