1
2 /*
3 * Copyright 2015 Google Inc.
4 *
5 * Use of this source code is governed by a BSD-style license that can be
6 * found in the LICENSE file.
7 */
8
9 #include "tools/sk_app/VulkanWindowContext.h"
10
11 #include "include/core/SkSurface.h"
12 #include "include/gpu/GrBackendSemaphore.h"
13 #include "include/gpu/GrBackendSurface.h"
14 #include "include/gpu/GrContext.h"
15 #include "src/core/SkAutoMalloc.h"
16
17 #include "include/gpu/vk/GrVkExtensions.h"
18 #include "include/gpu/vk/GrVkTypes.h"
19 #include "src/gpu/vk/GrVkImage.h"
20 #include "src/gpu/vk/GrVkUtil.h"
21
22 #ifdef VK_USE_PLATFORM_WIN32_KHR
23 // windows wants to define this as CreateSemaphoreA or CreateSemaphoreW
24 #undef CreateSemaphore
25 #endif
26
27 #define GET_PROC(F) f ## F = (PFN_vk ## F) fGetInstanceProcAddr(fInstance, "vk" #F)
28 #define GET_DEV_PROC(F) f ## F = (PFN_vk ## F) fGetDeviceProcAddr(fDevice, "vk" #F)
29
30 namespace sk_app {
31
VulkanWindowContext(const DisplayParams & params,CreateVkSurfaceFn createVkSurface,CanPresentFn canPresent,PFN_vkGetInstanceProcAddr instProc,PFN_vkGetDeviceProcAddr devProc)32 VulkanWindowContext::VulkanWindowContext(const DisplayParams& params,
33 CreateVkSurfaceFn createVkSurface,
34 CanPresentFn canPresent,
35 PFN_vkGetInstanceProcAddr instProc,
36 PFN_vkGetDeviceProcAddr devProc)
37 : WindowContext(params)
38 , fCreateVkSurfaceFn(createVkSurface)
39 , fCanPresentFn(canPresent)
40 , fSurface(VK_NULL_HANDLE)
41 , fSwapchain(VK_NULL_HANDLE)
42 , fImages(nullptr)
43 , fImageLayouts(nullptr)
44 , fSurfaces(nullptr)
45 , fBackbuffers(nullptr) {
46 fGetInstanceProcAddr = instProc;
47 fGetDeviceProcAddr = devProc;
48 this->initializeContext();
49 }
50
initializeContext()51 void VulkanWindowContext::initializeContext() {
52 // any config code here (particularly for msaa)?
53
54 PFN_vkGetInstanceProcAddr getInstanceProc = fGetInstanceProcAddr;
55 PFN_vkGetDeviceProcAddr getDeviceProc = fGetDeviceProcAddr;
56 auto getProc = [getInstanceProc, getDeviceProc](const char* proc_name,
57 VkInstance instance, VkDevice device) {
58 if (device != VK_NULL_HANDLE) {
59 return getDeviceProc(device, proc_name);
60 }
61 return getInstanceProc(instance, proc_name);
62 };
63 GrVkBackendContext backendContext;
64 GrVkExtensions extensions;
65 VkPhysicalDeviceFeatures2 features;
66 if (!sk_gpu_test::CreateVkBackendContext(getProc, &backendContext, &extensions, &features,
67 &fDebugCallback, &fPresentQueueIndex, fCanPresentFn)) {
68 sk_gpu_test::FreeVulkanFeaturesStructs(&features);
69 return;
70 }
71
72 if (!extensions.hasExtension(VK_KHR_SURFACE_EXTENSION_NAME, 25) ||
73 !extensions.hasExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME, 68)) {
74 sk_gpu_test::FreeVulkanFeaturesStructs(&features);
75 return;
76 }
77
78 fInstance = backendContext.fInstance;
79 fPhysicalDevice = backendContext.fPhysicalDevice;
80 fDevice = backendContext.fDevice;
81 fGraphicsQueueIndex = backendContext.fGraphicsQueueIndex;
82 fGraphicsQueue = backendContext.fQueue;
83
84 PFN_vkGetPhysicalDeviceProperties localGetPhysicalDeviceProperties =
85 reinterpret_cast<PFN_vkGetPhysicalDeviceProperties>(
86 backendContext.fGetProc("vkGetPhysicalDeviceProperties",
87 backendContext.fInstance,
88 VK_NULL_HANDLE));
89 if (!localGetPhysicalDeviceProperties) {
90 sk_gpu_test::FreeVulkanFeaturesStructs(&features);
91 return;
92 }
93 VkPhysicalDeviceProperties physDeviceProperties;
94 localGetPhysicalDeviceProperties(backendContext.fPhysicalDevice, &physDeviceProperties);
95 uint32_t physDevVersion = physDeviceProperties.apiVersion;
96
97 fInterface.reset(new GrVkInterface(backendContext.fGetProc, fInstance, fDevice,
98 backendContext.fInstanceVersion, physDevVersion,
99 &extensions));
100
101 GET_PROC(DestroyInstance);
102 if (fDebugCallback != VK_NULL_HANDLE) {
103 GET_PROC(DestroyDebugReportCallbackEXT);
104 }
105 GET_PROC(DestroySurfaceKHR);
106 GET_PROC(GetPhysicalDeviceSurfaceSupportKHR);
107 GET_PROC(GetPhysicalDeviceSurfaceCapabilitiesKHR);
108 GET_PROC(GetPhysicalDeviceSurfaceFormatsKHR);
109 GET_PROC(GetPhysicalDeviceSurfacePresentModesKHR);
110 GET_DEV_PROC(DeviceWaitIdle);
111 GET_DEV_PROC(QueueWaitIdle);
112 GET_DEV_PROC(DestroyDevice);
113 GET_DEV_PROC(CreateSwapchainKHR);
114 GET_DEV_PROC(DestroySwapchainKHR);
115 GET_DEV_PROC(GetSwapchainImagesKHR);
116 GET_DEV_PROC(AcquireNextImageKHR);
117 GET_DEV_PROC(QueuePresentKHR);
118 GET_DEV_PROC(GetDeviceQueue);
119
120 fContext = GrContext::MakeVulkan(backendContext, fDisplayParams.fGrContextOptions);
121
122 fSurface = fCreateVkSurfaceFn(fInstance);
123 if (VK_NULL_HANDLE == fSurface) {
124 this->destroyContext();
125 sk_gpu_test::FreeVulkanFeaturesStructs(&features);
126 return;
127 }
128
129 VkBool32 supported;
130 VkResult res = fGetPhysicalDeviceSurfaceSupportKHR(fPhysicalDevice, fPresentQueueIndex,
131 fSurface, &supported);
132 if (VK_SUCCESS != res) {
133 this->destroyContext();
134 sk_gpu_test::FreeVulkanFeaturesStructs(&features);
135 return;
136 }
137
138 if (!this->createSwapchain(-1, -1, fDisplayParams)) {
139 this->destroyContext();
140 sk_gpu_test::FreeVulkanFeaturesStructs(&features);
141 return;
142 }
143
144 // create presentQueue
145 fGetDeviceQueue(fDevice, fPresentQueueIndex, 0, &fPresentQueue);
146 sk_gpu_test::FreeVulkanFeaturesStructs(&features);
147 }
148
createSwapchain(int width,int height,const DisplayParams & params)149 bool VulkanWindowContext::createSwapchain(int width, int height,
150 const DisplayParams& params) {
151 // check for capabilities
152 VkSurfaceCapabilitiesKHR caps;
153 VkResult res = fGetPhysicalDeviceSurfaceCapabilitiesKHR(fPhysicalDevice, fSurface, &caps);
154 if (VK_SUCCESS != res) {
155 return false;
156 }
157
158 uint32_t surfaceFormatCount;
159 res = fGetPhysicalDeviceSurfaceFormatsKHR(fPhysicalDevice, fSurface, &surfaceFormatCount,
160 nullptr);
161 if (VK_SUCCESS != res) {
162 return false;
163 }
164
165 SkAutoMalloc surfaceFormatAlloc(surfaceFormatCount * sizeof(VkSurfaceFormatKHR));
166 VkSurfaceFormatKHR* surfaceFormats = (VkSurfaceFormatKHR*)surfaceFormatAlloc.get();
167 res = fGetPhysicalDeviceSurfaceFormatsKHR(fPhysicalDevice, fSurface, &surfaceFormatCount,
168 surfaceFormats);
169 if (VK_SUCCESS != res) {
170 return false;
171 }
172
173 uint32_t presentModeCount;
174 res = fGetPhysicalDeviceSurfacePresentModesKHR(fPhysicalDevice, fSurface, &presentModeCount,
175 nullptr);
176 if (VK_SUCCESS != res) {
177 return false;
178 }
179
180 SkAutoMalloc presentModeAlloc(presentModeCount * sizeof(VkPresentModeKHR));
181 VkPresentModeKHR* presentModes = (VkPresentModeKHR*)presentModeAlloc.get();
182 res = fGetPhysicalDeviceSurfacePresentModesKHR(fPhysicalDevice, fSurface, &presentModeCount,
183 presentModes);
184 if (VK_SUCCESS != res) {
185 return false;
186 }
187
188 VkExtent2D extent = caps.currentExtent;
189 // use the hints
190 if (extent.width == (uint32_t)-1) {
191 extent.width = width;
192 extent.height = height;
193 }
194
195 // clamp width; to protect us from broken hints
196 if (extent.width < caps.minImageExtent.width) {
197 extent.width = caps.minImageExtent.width;
198 } else if (extent.width > caps.maxImageExtent.width) {
199 extent.width = caps.maxImageExtent.width;
200 }
201 // clamp height
202 if (extent.height < caps.minImageExtent.height) {
203 extent.height = caps.minImageExtent.height;
204 } else if (extent.height > caps.maxImageExtent.height) {
205 extent.height = caps.maxImageExtent.height;
206 }
207
208 fWidth = (int)extent.width;
209 fHeight = (int)extent.height;
210
211 uint32_t imageCount = caps.minImageCount + 2;
212 if (caps.maxImageCount > 0 && imageCount > caps.maxImageCount) {
213 // Application must settle for fewer images than desired:
214 imageCount = caps.maxImageCount;
215 }
216
217 VkImageUsageFlags usageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
218 VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
219 VK_IMAGE_USAGE_TRANSFER_DST_BIT;
220 SkASSERT((caps.supportedUsageFlags & usageFlags) == usageFlags);
221 SkASSERT(caps.supportedTransforms & caps.currentTransform);
222 SkASSERT(caps.supportedCompositeAlpha & (VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR |
223 VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR));
224 VkCompositeAlphaFlagBitsKHR composite_alpha =
225 (caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR) ?
226 VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR :
227 VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
228
229 // Pick our surface format.
230 VkFormat surfaceFormat = VK_FORMAT_UNDEFINED;
231 VkColorSpaceKHR colorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
232 for (uint32_t i = 0; i < surfaceFormatCount; ++i) {
233 VkFormat localFormat = surfaceFormats[i].format;
234 if (GrVkFormatIsSupported(localFormat)) {
235 surfaceFormat = localFormat;
236 colorSpace = surfaceFormats[i].colorSpace;
237 break;
238 }
239 }
240 fDisplayParams = params;
241 fSampleCount = params.fMSAASampleCount;
242 fStencilBits = 8;
243
244 if (VK_FORMAT_UNDEFINED == surfaceFormat) {
245 return false;
246 }
247
248 SkColorType colorType;
249 switch (surfaceFormat) {
250 case VK_FORMAT_R8G8B8A8_UNORM: // fall through
251 case VK_FORMAT_R8G8B8A8_SRGB:
252 colorType = kRGBA_8888_SkColorType;
253 break;
254 case VK_FORMAT_B8G8R8A8_UNORM: // fall through
255 colorType = kBGRA_8888_SkColorType;
256 break;
257 default:
258 return false;
259 }
260
261 // If mailbox mode is available, use it, as it is the lowest-latency non-
262 // tearing mode. If not, fall back to FIFO which is always available.
263 VkPresentModeKHR mode = VK_PRESENT_MODE_FIFO_KHR;
264 bool hasImmediate = false;
265 for (uint32_t i = 0; i < presentModeCount; ++i) {
266 // use mailbox
267 if (VK_PRESENT_MODE_MAILBOX_KHR == presentModes[i]) {
268 mode = VK_PRESENT_MODE_MAILBOX_KHR;
269 }
270 if (VK_PRESENT_MODE_IMMEDIATE_KHR == presentModes[i]) {
271 hasImmediate = true;
272 }
273 }
274 if (params.fDisableVsync && hasImmediate) {
275 mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
276 }
277
278 VkSwapchainCreateInfoKHR swapchainCreateInfo;
279 memset(&swapchainCreateInfo, 0, sizeof(VkSwapchainCreateInfoKHR));
280 swapchainCreateInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
281 swapchainCreateInfo.surface = fSurface;
282 swapchainCreateInfo.minImageCount = imageCount;
283 swapchainCreateInfo.imageFormat = surfaceFormat;
284 swapchainCreateInfo.imageColorSpace = colorSpace;
285 swapchainCreateInfo.imageExtent = extent;
286 swapchainCreateInfo.imageArrayLayers = 1;
287 swapchainCreateInfo.imageUsage = usageFlags;
288
289 uint32_t queueFamilies[] = { fGraphicsQueueIndex, fPresentQueueIndex };
290 if (fGraphicsQueueIndex != fPresentQueueIndex) {
291 swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
292 swapchainCreateInfo.queueFamilyIndexCount = 2;
293 swapchainCreateInfo.pQueueFamilyIndices = queueFamilies;
294 } else {
295 swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
296 swapchainCreateInfo.queueFamilyIndexCount = 0;
297 swapchainCreateInfo.pQueueFamilyIndices = nullptr;
298 }
299
300 swapchainCreateInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
301 swapchainCreateInfo.compositeAlpha = composite_alpha;
302 swapchainCreateInfo.presentMode = mode;
303 swapchainCreateInfo.clipped = true;
304 swapchainCreateInfo.oldSwapchain = fSwapchain;
305
306 res = fCreateSwapchainKHR(fDevice, &swapchainCreateInfo, nullptr, &fSwapchain);
307 if (VK_SUCCESS != res) {
308 return false;
309 }
310
311 // destroy the old swapchain
312 if (swapchainCreateInfo.oldSwapchain != VK_NULL_HANDLE) {
313 fDeviceWaitIdle(fDevice);
314
315 this->destroyBuffers();
316
317 fDestroySwapchainKHR(fDevice, swapchainCreateInfo.oldSwapchain, nullptr);
318 }
319
320 this->createBuffers(swapchainCreateInfo.imageFormat, colorType);
321
322 return true;
323 }
324
createBuffers(VkFormat format,SkColorType colorType)325 void VulkanWindowContext::createBuffers(VkFormat format, SkColorType colorType) {
326 fGetSwapchainImagesKHR(fDevice, fSwapchain, &fImageCount, nullptr);
327 SkASSERT(fImageCount);
328 fImages = new VkImage[fImageCount];
329 fGetSwapchainImagesKHR(fDevice, fSwapchain, &fImageCount, fImages);
330
331 // set up initial image layouts and create surfaces
332 fImageLayouts = new VkImageLayout[fImageCount];
333 fSurfaces = new sk_sp<SkSurface>[fImageCount];
334 for (uint32_t i = 0; i < fImageCount; ++i) {
335 fImageLayouts[i] = VK_IMAGE_LAYOUT_UNDEFINED;
336
337 GrVkImageInfo info;
338 info.fImage = fImages[i];
339 info.fAlloc = GrVkAlloc();
340 info.fImageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
341 info.fImageTiling = VK_IMAGE_TILING_OPTIMAL;
342 info.fFormat = format;
343 info.fLevelCount = 1;
344 info.fCurrentQueueFamily = fPresentQueueIndex;
345
346 if (fSampleCount == 1) {
347 GrBackendRenderTarget backendRT(fWidth, fHeight, fSampleCount, info);
348
349 fSurfaces[i] = SkSurface::MakeFromBackendRenderTarget(
350 fContext.get(), backendRT, kTopLeft_GrSurfaceOrigin, colorType,
351 fDisplayParams.fColorSpace, &fDisplayParams.fSurfaceProps);
352 } else {
353 GrBackendTexture backendTexture(fWidth, fHeight, info);
354
355 fSurfaces[i] = SkSurface::MakeFromBackendTexture(
356 fContext.get(), backendTexture, kTopLeft_GrSurfaceOrigin, fSampleCount,
357 colorType, fDisplayParams.fColorSpace, &fDisplayParams.fSurfaceProps);
358
359 }
360 }
361
362 // set up the backbuffers
363 VkSemaphoreCreateInfo semaphoreInfo;
364 memset(&semaphoreInfo, 0, sizeof(VkSemaphoreCreateInfo));
365 semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
366 semaphoreInfo.pNext = nullptr;
367 semaphoreInfo.flags = 0;
368
369 // we create one additional backbuffer structure here, because we want to
370 // give the command buffers they contain a chance to finish before we cycle back
371 fBackbuffers = new BackbufferInfo[fImageCount + 1];
372 for (uint32_t i = 0; i < fImageCount + 1; ++i) {
373 fBackbuffers[i].fImageIndex = -1;
374 GR_VK_CALL_ERRCHECK(fInterface,
375 CreateSemaphore(fDevice, &semaphoreInfo,
376 nullptr, &fBackbuffers[i].fRenderSemaphore));
377 }
378 fCurrentBackbufferIndex = fImageCount;
379 }
380
destroyBuffers()381 void VulkanWindowContext::destroyBuffers() {
382
383 if (fBackbuffers) {
384 for (uint32_t i = 0; i < fImageCount + 1; ++i) {
385 fBackbuffers[i].fImageIndex = -1;
386 GR_VK_CALL(fInterface,
387 DestroySemaphore(fDevice,
388 fBackbuffers[i].fRenderSemaphore,
389 nullptr));
390 }
391 }
392
393 delete[] fBackbuffers;
394 fBackbuffers = nullptr;
395
396 // Does this actually free the surfaces?
397 delete[] fSurfaces;
398 fSurfaces = nullptr;
399 delete[] fImageLayouts;
400 fImageLayouts = nullptr;
401 delete[] fImages;
402 fImages = nullptr;
403 }
404
~VulkanWindowContext()405 VulkanWindowContext::~VulkanWindowContext() {
406 this->destroyContext();
407 }
408
destroyContext()409 void VulkanWindowContext::destroyContext() {
410 if (this->isValid()) {
411 fQueueWaitIdle(fPresentQueue);
412 fDeviceWaitIdle(fDevice);
413
414 this->destroyBuffers();
415
416 if (VK_NULL_HANDLE != fSwapchain) {
417 fDestroySwapchainKHR(fDevice, fSwapchain, nullptr);
418 fSwapchain = VK_NULL_HANDLE;
419 }
420
421 if (VK_NULL_HANDLE != fSurface) {
422 fDestroySurfaceKHR(fInstance, fSurface, nullptr);
423 fSurface = VK_NULL_HANDLE;
424 }
425 }
426
427 fContext.reset();
428 fInterface.reset();
429
430 if (VK_NULL_HANDLE != fDevice) {
431 fDestroyDevice(fDevice, nullptr);
432 fDevice = VK_NULL_HANDLE;
433 }
434
435 #ifdef SK_ENABLE_VK_LAYERS
436 if (fDebugCallback != VK_NULL_HANDLE) {
437 fDestroyDebugReportCallbackEXT(fInstance, fDebugCallback, nullptr);
438 }
439 #endif
440
441 fPhysicalDevice = VK_NULL_HANDLE;
442
443 if (VK_NULL_HANDLE != fInstance) {
444 fDestroyInstance(fInstance, nullptr);
445 fInstance = VK_NULL_HANDLE;
446 }
447 }
448
getAvailableBackbuffer()449 VulkanWindowContext::BackbufferInfo* VulkanWindowContext::getAvailableBackbuffer() {
450 SkASSERT(fBackbuffers);
451
452 ++fCurrentBackbufferIndex;
453 if (fCurrentBackbufferIndex > fImageCount) {
454 fCurrentBackbufferIndex = 0;
455 }
456
457 BackbufferInfo* backbuffer = fBackbuffers + fCurrentBackbufferIndex;
458 return backbuffer;
459 }
460
getBackbufferSurface()461 sk_sp<SkSurface> VulkanWindowContext::getBackbufferSurface() {
462 BackbufferInfo* backbuffer = this->getAvailableBackbuffer();
463 SkASSERT(backbuffer);
464
465 // semaphores should be in unsignaled state
466 VkSemaphoreCreateInfo semaphoreInfo;
467 memset(&semaphoreInfo, 0, sizeof(VkSemaphoreCreateInfo));
468 semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
469 semaphoreInfo.pNext = nullptr;
470 semaphoreInfo.flags = 0;
471 VkSemaphore semaphore;
472 GR_VK_CALL_ERRCHECK(fInterface, CreateSemaphore(fDevice, &semaphoreInfo,
473 nullptr, &semaphore));
474
475 // acquire the image
476 VkResult res = fAcquireNextImageKHR(fDevice, fSwapchain, UINT64_MAX,
477 semaphore, VK_NULL_HANDLE,
478 &backbuffer->fImageIndex);
479 if (VK_ERROR_SURFACE_LOST_KHR == res) {
480 // need to figure out how to create a new vkSurface without the platformData*
481 // maybe use attach somehow? but need a Window
482 GR_VK_CALL(fInterface, DestroySemaphore(fDevice, semaphore, nullptr));
483 return nullptr;
484 }
485 if (VK_ERROR_OUT_OF_DATE_KHR == res) {
486 // tear swapchain down and try again
487 if (!this->createSwapchain(-1, -1, fDisplayParams)) {
488 GR_VK_CALL(fInterface, DestroySemaphore(fDevice, semaphore, nullptr));
489 return nullptr;
490 }
491 backbuffer = this->getAvailableBackbuffer();
492
493 // acquire the image
494 res = fAcquireNextImageKHR(fDevice, fSwapchain, UINT64_MAX,
495 semaphore, VK_NULL_HANDLE,
496 &backbuffer->fImageIndex);
497
498 if (VK_SUCCESS != res) {
499 GR_VK_CALL(fInterface, DestroySemaphore(fDevice, semaphore, nullptr));
500 return nullptr;
501 }
502 }
503
504 SkSurface* surface = fSurfaces[backbuffer->fImageIndex].get();
505
506 GrBackendSemaphore beSemaphore;
507 beSemaphore.initVulkan(semaphore);
508
509 surface->wait(1, &beSemaphore);
510
511 return sk_ref_sp(surface);
512 }
513
swapBuffers()514 void VulkanWindowContext::swapBuffers() {
515
516 BackbufferInfo* backbuffer = fBackbuffers + fCurrentBackbufferIndex;
517 SkSurface* surface = fSurfaces[backbuffer->fImageIndex].get();
518
519 GrBackendSemaphore beSemaphore;
520 beSemaphore.initVulkan(backbuffer->fRenderSemaphore);
521
522 GrFlushInfo info;
523 info.fNumSemaphores = 1;
524 info.fSignalSemaphores = &beSemaphore;
525 surface->flush(SkSurface::BackendSurfaceAccess::kPresent, info);
526
527 // Submit present operation to present queue
528 const VkPresentInfoKHR presentInfo =
529 {
530 VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, // sType
531 NULL, // pNext
532 1, // waitSemaphoreCount
533 &backbuffer->fRenderSemaphore, // pWaitSemaphores
534 1, // swapchainCount
535 &fSwapchain, // pSwapchains
536 &backbuffer->fImageIndex, // pImageIndices
537 NULL // pResults
538 };
539
540 fQueuePresentKHR(fPresentQueue, &presentInfo);
541 }
542
543 } //namespace sk_app
544