1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 #ifdef ANDROID 18 #define SWAPPYVK_USE_WRAPPER 19 #endif 20 #include <swappyVk/SwappyVk.h> 21 22 #include <map> 23 #include <condition_variable> 24 #include <cstring> 25 #include <unistd.h> 26 27 #include <dlfcn.h> 28 #include <cstdlib> 29 30 #include <inttypes.h> 31 32 #ifdef ANDROID 33 #include <mutex> 34 #include <pthread.h> 35 #include <list> 36 #include <android/looper.h> 37 #include <android/log.h> 38 #include "Trace.h" 39 #include "ChoreographerShim.h" 40 41 #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, "SwappyVk", __VA_ARGS__) 42 #define ALOGW(...) __android_log_print(ANDROID_LOG_WARN, "SwappyVk", __VA_ARGS__) 43 #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, "SwappyVk", __VA_ARGS__) 44 #define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "SwappyVk", __VA_ARGS__) 45 #define ALOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, "SwappyVk", __VA_ARGS__) 46 #else 47 #define ATRACE_CALL() ((void)0) 48 #define ALOGE(...) ((void)0) 49 #define ALOGW(...) ((void)0) 50 #define ALOGD(...) ((void)0) 51 #define ALOGV(...) ((void)0) 52 #endif 53 54 55 constexpr uint32_t kThousand = 1000; 56 constexpr uint32_t kMillion = 1000000; 57 constexpr uint32_t kBillion = 1000000000; 58 constexpr uint32_t k16_6msec = 16666666; 59 60 constexpr uint32_t kTooCloseToVsyncBoundary = 3000000; 61 constexpr uint32_t kTooFarAwayFromVsyncBoundary = 7000000; 62 constexpr uint32_t kNudgeWithinVsyncBoundaries = 2000000; 63 64 // Note: The API functions is at the botton of the file. Those functions call methods of the 65 // singleton SwappyVk class. Those methods call virtual methods of the abstract SwappyVkBase 66 // class, which is actually implemented by one of the derived/concrete classes: 67 // 68 // - SwappyVkGoogleDisplayTiming 69 // - SwappyVkVulkanFallback 70 // - SwappyVkAndroidFallback 71 72 // Forward declarations: 73 class SwappyVk; 74 75 // AChoreographer is supported from API 24. To allow compilation for minSDK < 24 76 // and still use AChoreographer for SDK >= 24 we need runtime support to call 77 // AChoreographer APIs. 78 79 using PFN_AChoreographer_getInstance = AChoreographer* (*)(); 80 81 using PFN_AChoreographer_postFrameCallback = void (*)(AChoreographer* choreographer, 82 AChoreographer_frameCallback callback, 83 void* data); 84 85 using PFN_AChoreographer_postFrameCallbackDelayed = void (*)(AChoreographer* choreographer, 86 AChoreographer_frameCallback callback, 87 void* data, 88 long delayMillis); 89 90 /*************************************************************************************************** 91 * 92 * Per-Device abstract base class. 93 * 94 ***************************************************************************************************/ 95 96 /** 97 * Abstract base class that calls the Vulkan API. 98 * 99 * It is expected that one concrete class will be instantiated per VkDevice, and that all 100 * VkSwapchainKHR's for a given VkDevice will share the same instance. 101 * 102 * Base class members are used by the derived classes to unify the behavior across implementaitons: 103 * @mThread - Thread used for getting Choreographer events. 104 * @mTreadRunning - Used to signal the tread to exit 105 * @mNextPresentID - unique ID for frame presentation. 106 * @mNextDesiredPresentTime - Holds the time in nanoseconds for the next frame to be presented. 107 * @mNextPresentIDToCheck - Used to determine whether presentation time needs to be adjusted. 108 * @mFrameID - Keeps track of how many Choreographer callbacks received. 109 * @mLastframeTimeNanos - Holds the last frame time reported by Choreographer. 110 * @mSumRefreshTime - Used together with @mSamples to calculate refresh rate based on Choreographer. 111 */ 112 class SwappyVkBase 113 { 114 public: 115 SwappyVkBase(VkPhysicalDevice physicalDevice, 116 VkDevice device, 117 uint64_t refreshDur, 118 uint32_t interval, 119 SwappyVk &swappyVk, 120 void *libVulkan) : 121 mPhysicalDevice(physicalDevice), mDevice(device), mRefreshDur(refreshDur), 122 mInterval(interval), mSwappyVk(swappyVk), mLibVulkan(libVulkan), 123 mInitialized(false) 124 { 125 #ifdef ANDROID 126 InitVulkan(); 127 #endif 128 mpfnGetDeviceProcAddr = 129 reinterpret_cast<PFN_vkGetDeviceProcAddr>( 130 dlsym(mLibVulkan, "vkGetDeviceProcAddr")); 131 mpfnQueuePresentKHR = 132 reinterpret_cast<PFN_vkQueuePresentKHR>( 133 mpfnGetDeviceProcAddr(mDevice, "vkQueuePresentKHR")); 134 135 mLibAndroid = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); 136 if (mLibAndroid == nullptr) { 137 ALOGE("FATAL: cannot open libandroid.so: %s", strerror(errno)); 138 abort(); 139 } 140 141 mAChoreographer_getInstance = 142 reinterpret_cast<PFN_AChoreographer_getInstance >( 143 dlsym(mLibAndroid, "AChoreographer_getInstance")); 144 145 mAChoreographer_postFrameCallback = 146 reinterpret_cast<PFN_AChoreographer_postFrameCallback >( 147 dlsym(mLibAndroid, "AChoreographer_postFrameCallback")); 148 149 mAChoreographer_postFrameCallbackDelayed = 150 reinterpret_cast<PFN_AChoreographer_postFrameCallbackDelayed >( 151 dlsym(mLibAndroid, "AChoreographer_postFrameCallbackDelayed")); 152 if (!mAChoreographer_getInstance || 153 !mAChoreographer_postFrameCallback || 154 !mAChoreographer_postFrameCallbackDelayed) { 155 ALOGE("FATAL: cannot get AChoreographer symbols"); 156 abort(); 157 } 158 } 159 virtual ~SwappyVkBase() { 160 if(mLibAndroid) 161 dlclose(mLibAndroid); 162 } 163 virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain, 164 uint64_t* pRefreshDuration) = 0; 165 void doSetSwapInterval(VkSwapchainKHR swapchain, 166 uint32_t interval) 167 { 168 mInterval = interval; 169 } 170 virtual VkResult doQueuePresent(VkQueue queue, 171 uint32_t queueFamilyIndex, 172 const VkPresentInfoKHR* pPresentInfo) = 0; 173 protected: 174 VkPhysicalDevice mPhysicalDevice; 175 VkDevice mDevice; 176 uint64_t mRefreshDur; 177 uint32_t mInterval; 178 SwappyVk &mSwappyVk; 179 void *mLibVulkan; 180 bool mInitialized; 181 pthread_t mThread = 0; 182 ALooper *mLooper = nullptr; 183 bool mTreadRunning = false; 184 AChoreographer *mChoreographer = nullptr; 185 std::mutex mWaitingMutex; 186 std::condition_variable mWaitingCondition; 187 uint32_t mNextPresentID = 0; 188 uint64_t mNextDesiredPresentTime = 0; 189 uint32_t mNextPresentIDToCheck = 2; 190 191 PFN_vkGetDeviceProcAddr mpfnGetDeviceProcAddr = nullptr; 192 PFN_vkQueuePresentKHR mpfnQueuePresentKHR = nullptr; 193 PFN_vkGetRefreshCycleDurationGOOGLE mpfnGetRefreshCycleDurationGOOGLE = nullptr; 194 PFN_vkGetPastPresentationTimingGOOGLE mpfnGetPastPresentationTimingGOOGLE = nullptr; 195 196 void *mLibAndroid = nullptr; 197 PFN_AChoreographer_getInstance mAChoreographer_getInstance = nullptr; 198 PFN_AChoreographer_postFrameCallback mAChoreographer_postFrameCallback = nullptr; 199 PFN_AChoreographer_postFrameCallbackDelayed mAChoreographer_postFrameCallbackDelayed = nullptr; 200 201 long mFrameID = 0; 202 long mTargetFrameID = 0; 203 uint64_t mLastframeTimeNanos = 0; 204 long mSumRefreshTime = 0; 205 long mSamples = 0; 206 long mCallbacksBeforeIdle = 0; 207 208 static constexpr int MAX_SAMPLES = 5; 209 static constexpr int MAX_CALLBACKS_BEFORE_IDLE = 10; 210 211 void initGoogExtention() 212 { 213 mpfnGetRefreshCycleDurationGOOGLE = 214 reinterpret_cast<PFN_vkGetRefreshCycleDurationGOOGLE>( 215 mpfnGetDeviceProcAddr(mDevice, "vkGetRefreshCycleDurationGOOGLE")); 216 mpfnGetPastPresentationTimingGOOGLE = 217 reinterpret_cast<PFN_vkGetPastPresentationTimingGOOGLE>( 218 mpfnGetDeviceProcAddr(mDevice, "vkGetPastPresentationTimingGOOGLE")); 219 } 220 221 void startChoreographerThread(); 222 void stopChoreographerThread(); 223 static void *looperThreadWrapper(void *data); 224 void *looperThread(); 225 static void frameCallback(long frameTimeNanos, void *data); 226 void onDisplayRefresh(long frameTimeNanos); 227 void calcRefreshRate(uint64_t currentTime); 228 void postChoreographerCallback(); 229 }; 230 231 void SwappyVkBase::startChoreographerThread() { 232 std::unique_lock<std::mutex> lock(mWaitingMutex); 233 // create a new ALooper thread to get Choreographer events 234 mTreadRunning = true; 235 pthread_create(&mThread, NULL, looperThreadWrapper, this); 236 mWaitingCondition.wait(lock, [&]() { return mChoreographer != nullptr; }); 237 } 238 239 void SwappyVkBase::stopChoreographerThread() { 240 if (mLooper) { 241 ALooper_acquire(mLooper); 242 mTreadRunning = false; 243 ALooper_wake(mLooper); 244 ALooper_release(mLooper); 245 pthread_join(mThread, NULL); 246 } 247 } 248 249 void *SwappyVkBase::looperThreadWrapper(void *data) { 250 SwappyVkBase *me = reinterpret_cast<SwappyVkBase *>(data); 251 return me->looperThread(); 252 } 253 254 void *SwappyVkBase::looperThread() { 255 int outFd, outEvents; 256 void *outData; 257 258 mLooper = ALooper_prepare(0); 259 if (!mLooper) { 260 ALOGE("ALooper_prepare failed"); 261 return NULL; 262 } 263 264 mChoreographer = mAChoreographer_getInstance(); 265 if (!mChoreographer) { 266 ALOGE("AChoreographer_getInstance failed"); 267 return NULL; 268 } 269 mWaitingCondition.notify_all(); 270 271 while (mTreadRunning) { 272 ALooper_pollAll(-1, &outFd, &outEvents, &outData); 273 } 274 275 return NULL; 276 } 277 278 void SwappyVkBase::frameCallback(long frameTimeNanos, void *data) { 279 SwappyVkBase *me = reinterpret_cast<SwappyVkBase *>(data); 280 me->onDisplayRefresh(frameTimeNanos); 281 } 282 283 void SwappyVkBase::onDisplayRefresh(long frameTimeNanos) { 284 std::lock_guard<std::mutex> lock(mWaitingMutex); 285 struct timespec currTime; 286 clock_gettime(CLOCK_MONOTONIC, &currTime); 287 uint64_t currentTime = 288 ((uint64_t) currTime.tv_sec * kBillion) + (uint64_t) currTime.tv_nsec; 289 290 calcRefreshRate(currentTime); 291 mLastframeTimeNanos = currentTime; 292 mFrameID++; 293 mWaitingCondition.notify_all(); 294 295 // queue the next frame callback 296 if (mCallbacksBeforeIdle > 0) { 297 mCallbacksBeforeIdle--; 298 mAChoreographer_postFrameCallbackDelayed(mChoreographer, frameCallback, this, 1); 299 } 300 } 301 302 void SwappyVkBase::postChoreographerCallback() { 303 if (mCallbacksBeforeIdle == 0) { 304 mAChoreographer_postFrameCallbackDelayed(mChoreographer, frameCallback, this, 1); 305 } 306 mCallbacksBeforeIdle = MAX_CALLBACKS_BEFORE_IDLE; 307 } 308 309 void SwappyVkBase::calcRefreshRate(uint64_t currentTime) { 310 long refresh_nano = currentTime - mLastframeTimeNanos; 311 312 if (mRefreshDur != 0 || mLastframeTimeNanos == 0) { 313 return; 314 } 315 316 mSumRefreshTime += refresh_nano; 317 mSamples++; 318 319 if (mSamples == MAX_SAMPLES) { 320 mRefreshDur = mSumRefreshTime / mSamples; 321 } 322 } 323 324 325 /*************************************************************************************************** 326 * 327 * Per-Device concrete/derived class for using VK_GOOGLE_display_timing. 328 * 329 * This class uses the VK_GOOGLE_display_timing in order to present frames at a muliple (the "swap 330 * interval") of a fixed refresh-cycle duration (i.e. the time between successive vsync's). 331 * 332 * In order to reduce complexity, some simplifying assumptions are made: 333 * 334 * - We assume a fixed refresh-rate (FRR) display that's between 60 Hz and 120 Hz. 335 * 336 * - While Vulkan allows applications to create and use multiple VkSwapchainKHR's per VkDevice, and 337 * to re-create VkSwapchainKHR's, we assume that the application uses a single VkSwapchainKHR, 338 * and never re-creates it. 339 * 340 * - The values reported back by the VK_GOOGLE_display_timing extension (which comes from 341 * lower-level Android interfaces) are not precise, and that values can drift over time. For 342 * example, the refresh-cycle duration for a 60 Hz display should be 16,666,666 nsec; but the 343 * value reported back by the extension won't be precisely this. Also, the differences betweeen 344 * the times of two successive frames won't be an exact multiple of 16,666,666 nsec. This can 345 * make it difficult to precisely predict when a future vsync will be (it can appear to drift 346 * overtime). Therefore, we try to give a desiredPresentTime for each image that is between 3 347 * and 7 msec before vsync. We look at the actualPresentTime for previously-presented images, 348 * and nudge the future desiredPresentTime back within those 3-7 msec boundaries. 349 * 350 * - There can be a few frames of latency between when an image is presented and when the 351 * actualPresentTime is available for that image. Therefore, we initially just pick times based 352 * upon CLOCK_MONOTONIC (which is the time domain for VK_GOOGLE_display_timing). After we get 353 * past-present times, we nudge the desiredPresentTime, we wait for a few presents before looking 354 * again to see whether we need to nudge again. 355 * 356 * - If, for some reason, an application can't keep up with its chosen swap interval (e.g. it's 357 * designed for 30FPS on a premium device and is now running on a slow device; or it's running on 358 * a 120Hz display), this algorithm may not be able to make up for this (i.e. smooth rendering at 359 * a targetted frame rate may not be possible with an application that can't render fast enough). 360 * 361 ***************************************************************************************************/ 362 363 /** 364 * Concrete/derived class that sits on top of VK_GOOGLE_display_timing 365 */ 366 class SwappyVkGoogleDisplayTiming : public SwappyVkBase 367 { 368 public: 369 SwappyVkGoogleDisplayTiming(VkPhysicalDevice physicalDevice, 370 VkDevice device, 371 SwappyVk &swappyVk, 372 void *libVulkan) : 373 SwappyVkBase(physicalDevice, device, k16_6msec, 1, swappyVk, libVulkan) 374 { 375 initGoogExtention(); 376 } 377 virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain, 378 uint64_t* pRefreshDuration) override 379 { 380 VkRefreshCycleDurationGOOGLE refreshCycleDuration; 381 VkResult res = mpfnGetRefreshCycleDurationGOOGLE(mDevice, swapchain, &refreshCycleDuration); 382 if (res != VK_SUCCESS) { 383 // This should never occur, but in case it does, return 16,666,666ns: 384 mRefreshDur = k16_6msec; 385 } else { 386 mRefreshDur = refreshCycleDuration.refreshDuration; 387 } 388 389 // TEMP CODE: LOG REFRESH DURATION AND RATE: 390 double refreshRate = mRefreshDur; 391 refreshRate = 1.0 / (refreshRate / 1000000000.0); 392 393 ALOGD("Returning refresh duration of %" PRIu64 " nsec (approx %f Hz)", mRefreshDur, refreshRate); 394 395 *pRefreshDuration = mRefreshDur; 396 return true; 397 } 398 virtual VkResult doQueuePresent(VkQueue queue, 399 uint32_t queueFamilyIndex, 400 const VkPresentInfoKHR* pPresentInfo) override; 401 402 private: 403 void calculateNextDesiredPresentTime(VkSwapchainKHR swapchain); 404 void checkPastPresentTiming(VkSwapchainKHR swapchain); 405 }; 406 407 VkResult SwappyVkGoogleDisplayTiming::doQueuePresent(VkQueue queue, 408 uint32_t queueFamilyIndex, 409 const VkPresentInfoKHR* pPresentInfo) 410 { 411 VkResult ret = VK_SUCCESS; 412 413 calculateNextDesiredPresentTime(pPresentInfo->pSwapchains[0]); 414 415 // Setup the new structures to pass: 416 VkPresentTimeGOOGLE *pPresentTimes = 417 reinterpret_cast<VkPresentTimeGOOGLE*>(malloc(sizeof(VkPresentTimeGOOGLE) * 418 pPresentInfo->swapchainCount)); 419 for (uint32_t i = 0 ; i < pPresentInfo->swapchainCount ; i++) { 420 pPresentTimes[i].presentID = mNextPresentID; 421 pPresentTimes[i].desiredPresentTime = mNextDesiredPresentTime; 422 } 423 mNextPresentID++; 424 425 VkPresentTimesInfoGOOGLE presentTimesInfo = {VK_STRUCTURE_TYPE_PRESENT_TIMES_INFO_GOOGLE, 426 pPresentInfo->pNext, pPresentInfo->swapchainCount, 427 pPresentTimes}; 428 VkPresentInfoKHR replacementPresentInfo = {pPresentInfo->sType, &presentTimesInfo, 429 pPresentInfo->waitSemaphoreCount, 430 pPresentInfo->pWaitSemaphores, 431 pPresentInfo->swapchainCount, 432 pPresentInfo->pSwapchains, 433 pPresentInfo->pImageIndices, pPresentInfo->pResults}; 434 ret = mpfnQueuePresentKHR(queue, &replacementPresentInfo); 435 free(pPresentTimes); 436 return ret; 437 } 438 439 void SwappyVkGoogleDisplayTiming::calculateNextDesiredPresentTime(VkSwapchainKHR swapchain) 440 { 441 struct timespec currTime; 442 clock_gettime(CLOCK_MONOTONIC, &currTime); 443 uint64_t currentTime = 444 ((uint64_t) currTime.tv_sec * kBillion) + (uint64_t) currTime.tv_nsec; 445 446 447 // Determine the desiredPresentTime: 448 if (!mNextDesiredPresentTime) { 449 mNextDesiredPresentTime = currentTime + mRefreshDur; 450 } else { 451 // Look at the timing of past presents, and potentially adjust mNextDesiredPresentTime: 452 checkPastPresentTiming(swapchain); 453 mNextDesiredPresentTime += mRefreshDur * mInterval; 454 455 // Make sure the calculated time is not before the current time to present 456 if (mNextDesiredPresentTime < currentTime) { 457 mNextDesiredPresentTime = currentTime + mRefreshDur; 458 } 459 } 460 } 461 462 void SwappyVkGoogleDisplayTiming::checkPastPresentTiming(VkSwapchainKHR swapchain) 463 { 464 VkResult ret = VK_SUCCESS; 465 466 if (mNextPresentID <= mNextPresentIDToCheck) { 467 return; 468 } 469 // Check the timing of past presents to see if we need to adjust mNextDesiredPresentTime: 470 uint32_t pastPresentationTimingCount = 0; 471 VkResult err = mpfnGetPastPresentationTimingGOOGLE(mDevice, swapchain, 472 &pastPresentationTimingCount, NULL); 473 if (!pastPresentationTimingCount) { 474 return; 475 } 476 // TODO: don't allocate memory for the timestamps every time. 477 VkPastPresentationTimingGOOGLE *past = 478 reinterpret_cast<VkPastPresentationTimingGOOGLE*>( 479 malloc(sizeof(VkPastPresentationTimingGOOGLE) * 480 pastPresentationTimingCount)); 481 err = mpfnGetPastPresentationTimingGOOGLE(mDevice, swapchain, 482 &pastPresentationTimingCount, past); 483 for (uint32_t i = 0; i < pastPresentationTimingCount; i++) { 484 // Note: On Android, actualPresentTime can actually be before desiredPresentTime 485 // (which shouldn't be possible. Therefore, this must be a signed integer. 486 int64_t amountEarlyBy = 487 (int64_t) past[i].actualPresentTime - (int64_t)past[i].desiredPresentTime; 488 if (amountEarlyBy < kTooCloseToVsyncBoundary) { 489 // We're getting too close to vsync. Nudge the next present back 490 // towards/in the boundaries, and check back after a few more presents: 491 mNextDesiredPresentTime -= kNudgeWithinVsyncBoundaries; 492 mNextPresentIDToCheck = mNextPresentID + 7; 493 break; 494 } 495 if (amountEarlyBy > kTooFarAwayFromVsyncBoundary) { 496 // We're getting too far away from vsync. Nudge the next present back 497 // towards/in the boundaries, and check back after a few more presents: 498 mNextDesiredPresentTime += kNudgeWithinVsyncBoundaries; 499 mNextPresentIDToCheck = mNextPresentID + 7; 500 break; 501 } 502 } 503 free(past); 504 } 505 506 /** 507 * Concrete/derived class that sits on top of VK_GOOGLE_display_timing 508 */ 509 class SwappyVkGoogleDisplayTimingAndroid : public SwappyVkGoogleDisplayTiming 510 { 511 public: 512 SwappyVkGoogleDisplayTimingAndroid(VkPhysicalDevice physicalDevice, 513 VkDevice device, 514 SwappyVk &swappyVk, 515 void *libVulkan) : 516 SwappyVkGoogleDisplayTiming(physicalDevice, device, swappyVk,libVulkan) { 517 startChoreographerThread(); 518 } 519 520 ~SwappyVkGoogleDisplayTimingAndroid() { 521 stopChoreographerThread(); 522 destroyVkSyncObjects(); 523 } 524 525 virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain, 526 uint64_t* pRefreshDuration) override { 527 bool res = SwappyVkGoogleDisplayTiming::doGetRefreshCycleDuration(swapchain, pRefreshDuration); 528 return res; 529 } 530 531 532 533 virtual VkResult doQueuePresent(VkQueue queue, 534 uint32_t queueFamilyIndex, 535 const VkPresentInfoKHR *pPresentInfo) override; 536 537 private: 538 VkResult initializeVkSyncObjects(VkQueue queue, uint32_t queueFamilyIndex); 539 void destroyVkSyncObjects(); 540 541 void waitForFenceChoreographer(VkQueue queue); 542 543 struct VkSync { 544 VkFence fence; 545 VkSemaphore semaphore; 546 VkCommandBuffer command; 547 VkEvent event; 548 }; 549 550 std::map<VkQueue, std::list<VkSync>> mFreeSync; 551 std::map<VkQueue, std::list<VkSync>> mPendingSync; 552 std::map<VkQueue, VkCommandPool> mCommandPool; 553 554 static constexpr int MAX_PENDING_FENCES = 1; 555 }; 556 557 VkResult SwappyVkGoogleDisplayTimingAndroid::initializeVkSyncObjects(VkQueue queue, 558 uint32_t queueFamilyIndex) 559 { 560 if (mCommandPool.find(queue) != mCommandPool.end()) { 561 return VK_SUCCESS; 562 } 563 564 VkSync sync; 565 566 const VkCommandPoolCreateInfo cmd_pool_info = { 567 .sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, 568 .pNext = NULL, 569 .queueFamilyIndex = queueFamilyIndex, 570 .flags = 0, 571 }; 572 573 VkResult res = vkCreateCommandPool(mDevice, &cmd_pool_info, NULL, &mCommandPool[queue]); 574 if (res) { 575 ALOGE("vkCreateCommandPool failed %d", res); 576 return res; 577 } 578 const VkCommandBufferAllocateInfo present_cmd_info = { 579 .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, 580 .pNext = NULL, 581 .commandPool = mCommandPool[queue], 582 .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, 583 .commandBufferCount = 1, 584 }; 585 586 for(int i = 0; i < MAX_PENDING_FENCES; i++) { 587 VkFenceCreateInfo fence_ci = 588 {.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, .pNext = NULL, .flags = 0}; 589 590 res = vkCreateFence(mDevice, &fence_ci, NULL, &sync.fence); 591 if (res) { 592 ALOGE("failed to create fence: %d", res); 593 return res; 594 } 595 596 VkSemaphoreCreateInfo semaphore_ci = 597 {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = NULL, .flags = 0}; 598 599 res = vkCreateSemaphore(mDevice, &semaphore_ci, NULL, &sync.semaphore); 600 if (res) { 601 ALOGE("failed to create semaphore: %d", res); 602 return res; 603 } 604 605 606 res = vkAllocateCommandBuffers(mDevice, &present_cmd_info, &sync.command); 607 if (res) { 608 ALOGE("vkAllocateCommandBuffers failed %d", res); 609 return res; 610 } 611 612 const VkCommandBufferBeginInfo cmd_buf_info = { 613 .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, 614 .pNext = NULL, 615 .flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT, 616 .pInheritanceInfo = NULL, 617 }; 618 619 res = vkBeginCommandBuffer(sync.command, &cmd_buf_info); 620 if (res) { 621 ALOGE("vkAllocateCommandBuffers failed %d", res); 622 return res; 623 } 624 625 VkEventCreateInfo event_info = { 626 .sType = VK_STRUCTURE_TYPE_EVENT_CREATE_INFO, 627 .pNext = NULL, 628 .flags = 0, 629 }; 630 631 res = vkCreateEvent(mDevice, &event_info, NULL, &sync.event); 632 if (res) { 633 ALOGE("vkCreateEvent failed %d", res); 634 return res; 635 } 636 637 vkCmdSetEvent(sync.command, sync.event, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); 638 639 res = vkEndCommandBuffer(sync.command); 640 if (res) { 641 ALOGE("vkCreateEvent failed %d", res); 642 return res; 643 } 644 645 mFreeSync[queue].push_back(sync); 646 } 647 648 return VK_SUCCESS; 649 } 650 651 void SwappyVkGoogleDisplayTimingAndroid::destroyVkSyncObjects() { 652 for (auto it = mPendingSync.begin(); it != mPendingSync.end(); it++) { 653 while (mPendingSync[it->first].size() > 0) { 654 VkSync sync = mPendingSync[it->first].front(); 655 mPendingSync[it->first].pop_front(); 656 vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE, UINT64_MAX); 657 vkResetFences(mDevice, 1, &sync.fence); 658 mFreeSync[it->first].push_back(sync); 659 } 660 661 while (mFreeSync[it->first].size() > 0) { 662 VkSync sync = mFreeSync[it->first].front(); 663 mFreeSync[it->first].pop_front(); 664 vkFreeCommandBuffers(mDevice, mCommandPool[it->first], 1, &sync.command); 665 vkDestroyEvent(mDevice, sync.event, NULL); 666 vkDestroySemaphore(mDevice, sync.semaphore, NULL); 667 vkDestroyFence(mDevice, sync.fence, NULL); 668 } 669 670 vkDestroyCommandPool(mDevice, mCommandPool[it->first], NULL); 671 } 672 } 673 674 void SwappyVkGoogleDisplayTimingAndroid::waitForFenceChoreographer(VkQueue queue) 675 { 676 std::unique_lock<std::mutex> lock(mWaitingMutex); 677 VkSync sync = mPendingSync[queue].front(); 678 mPendingSync[queue].pop_front(); 679 mWaitingCondition.wait(lock, [&]() { 680 if (vkWaitForFences(mDevice, 1, &sync.fence, VK_TRUE, 0) == VK_TIMEOUT) { 681 postChoreographerCallback(); 682 683 // adjust the target frame here as we are waiting additional frame for the fence 684 mTargetFrameID++; 685 return false; 686 } 687 return true; 688 }); 689 690 vkResetFences(mDevice, 1, &sync.fence); 691 mFreeSync[queue].push_back(sync); 692 } 693 694 VkResult SwappyVkGoogleDisplayTimingAndroid::doQueuePresent(VkQueue queue, 695 uint32_t queueFamilyIndex, 696 const VkPresentInfoKHR* pPresentInfo) 697 { 698 VkResult ret = initializeVkSyncObjects(queue, queueFamilyIndex); 699 if (ret) { 700 return ret; 701 } 702 703 { 704 std::unique_lock<std::mutex> lock(mWaitingMutex); 705 mWaitingCondition.wait(lock, [&]() { 706 if (mFrameID < mTargetFrameID) { 707 postChoreographerCallback(); 708 return false; 709 } 710 return true; 711 }); 712 } 713 714 if (mPendingSync[queue].size() >= MAX_PENDING_FENCES) { 715 waitForFenceChoreographer(queue); 716 } 717 718 // Adjust the presentation time based on the current frameID we are at. 719 if(mFrameID < mTargetFrameID) { 720 ALOGE("Bad frame ID %ld < target %ld", mFrameID, mTargetFrameID); 721 mTargetFrameID = mFrameID; 722 } 723 mNextDesiredPresentTime += (mFrameID - mTargetFrameID) * mRefreshDur; 724 725 // Setup the new structures to pass: 726 VkPresentTimeGOOGLE pPresentTimes[pPresentInfo->swapchainCount]; 727 for (uint32_t i = 0 ; i < pPresentInfo->swapchainCount ; i++) { 728 pPresentTimes[i].presentID = mNextPresentID; 729 pPresentTimes[i].desiredPresentTime = mNextDesiredPresentTime; 730 } 731 mNextPresentID++; 732 733 VkSync sync = mFreeSync[queue].front(); 734 mFreeSync[queue].pop_front(); 735 mPendingSync[queue].push_back(sync); 736 737 VkPipelineStageFlags pipe_stage_flags; 738 VkSubmitInfo submit_info; 739 submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; 740 submit_info.pNext = NULL; 741 submit_info.pWaitDstStageMask = &pipe_stage_flags; 742 pipe_stage_flags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; 743 submit_info.waitSemaphoreCount = pPresentInfo->waitSemaphoreCount; 744 submit_info.pWaitSemaphores = pPresentInfo->pWaitSemaphores; 745 submit_info.commandBufferCount = 1; 746 submit_info.pCommandBuffers = &sync.command; 747 submit_info.signalSemaphoreCount = 1; 748 submit_info.pSignalSemaphores = &sync.semaphore; 749 ret = vkQueueSubmit(queue, 1, &submit_info, sync.fence); 750 if (ret) { 751 ALOGE("Failed to vkQueueSubmit %d", ret); 752 return ret; 753 } 754 755 VkPresentTimesInfoGOOGLE presentTimesInfo = {VK_STRUCTURE_TYPE_PRESENT_TIMES_INFO_GOOGLE, 756 pPresentInfo->pNext, pPresentInfo->swapchainCount, 757 pPresentTimes}; 758 VkPresentInfoKHR replacementPresentInfo = {pPresentInfo->sType, &presentTimesInfo, 759 1, 760 &sync.semaphore, 761 pPresentInfo->swapchainCount, 762 pPresentInfo->pSwapchains, 763 pPresentInfo->pImageIndices, pPresentInfo->pResults}; 764 ret = mpfnQueuePresentKHR(queue, &replacementPresentInfo); 765 766 // next present time is going to be 2 intervals from now, leaving 1 interval for cpu work 767 // and 1 interval for gpu work 768 mNextDesiredPresentTime = mLastframeTimeNanos + 2 * mRefreshDur * mInterval; 769 mTargetFrameID = mFrameID + mInterval; 770 771 return ret; 772 } 773 774 /*************************************************************************************************** 775 * 776 * Per-Device concrete/derived class for the "Android fallback" path (uses 777 * Choreographer to try to get presents to occur at the desired time). 778 * 779 ***************************************************************************************************/ 780 781 /** 782 * Concrete/derived class that sits on top of the Vulkan API 783 */ 784 #ifdef ANDROID 785 class SwappyVkAndroidFallback : public SwappyVkBase 786 { 787 public: 788 SwappyVkAndroidFallback(VkPhysicalDevice physicalDevice, 789 VkDevice device, 790 SwappyVk &swappyVk, 791 void *libVulkan) : 792 SwappyVkBase(physicalDevice, device, 0, 1, swappyVk, libVulkan) { 793 startChoreographerThread(); 794 } 795 796 ~SwappyVkAndroidFallback() { 797 stopChoreographerThread(); 798 } 799 800 virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain, 801 uint64_t* pRefreshDuration) override 802 { 803 std::unique_lock<std::mutex> lock(mWaitingMutex); 804 mWaitingCondition.wait(lock, [&]() { 805 if (mRefreshDur == 0) { 806 postChoreographerCallback(); 807 return false; 808 } 809 return true; 810 }); 811 812 *pRefreshDuration = mRefreshDur; 813 814 double refreshRate = mRefreshDur; 815 refreshRate = 1.0 / (refreshRate / 1000000000.0); 816 ALOGI("Returning refresh duration of %" PRIu64 " nsec (approx %f Hz)", mRefreshDur, refreshRate); 817 return true; 818 } 819 820 virtual VkResult doQueuePresent(VkQueue queue, 821 uint32_t queueFamilyIndex, 822 const VkPresentInfoKHR* pPresentInfo) override 823 { 824 { 825 std::unique_lock<std::mutex> lock(mWaitingMutex); 826 827 mWaitingCondition.wait(lock, [&]() { 828 if (mFrameID < mTargetFrameID) { 829 postChoreographerCallback(); 830 return false; 831 } 832 return true; 833 }); 834 } 835 mTargetFrameID = mFrameID + mInterval; 836 return mpfnQueuePresentKHR(queue, pPresentInfo); 837 } 838 }; 839 #endif 840 841 /*************************************************************************************************** 842 * 843 * Per-Device concrete/derived class for the "Vulkan fallback" path (i.e. no API/OS timing support; 844 * just generic Vulkan) 845 * 846 ***************************************************************************************************/ 847 848 /** 849 * Concrete/derived class that sits on top of the Vulkan API 850 */ 851 class SwappyVkVulkanFallback : public SwappyVkBase 852 { 853 public: 854 SwappyVkVulkanFallback(VkPhysicalDevice physicalDevice, 855 VkDevice device, 856 SwappyVk &swappyVk, 857 void *libVulkan) : 858 SwappyVkBase(physicalDevice, device, k16_6msec, 1, swappyVk, libVulkan) {} 859 virtual bool doGetRefreshCycleDuration(VkSwapchainKHR swapchain, 860 uint64_t* pRefreshDuration) override 861 { 862 *pRefreshDuration = mRefreshDur; 863 return true; 864 } 865 virtual VkResult doQueuePresent(VkQueue queue, 866 uint32_t queueFamilyIndex, 867 const VkPresentInfoKHR* pPresentInfo) override 868 { 869 return mpfnQueuePresentKHR(queue, pPresentInfo); 870 } 871 }; 872 873 874 875 876 /*************************************************************************************************** 877 * 878 * Singleton class that provides the high-level implementation of the Swappy entrypoints. 879 * 880 ***************************************************************************************************/ 881 /** 882 * Singleton class that provides the high-level implementation of the Swappy entrypoints. 883 * 884 * This class determines which low-level implementation to use for each physical 885 * device, and then calls that class's do-method for the entrypoint. 886 */ 887 class SwappyVk 888 { 889 public: 890 static SwappyVk& getInstance() 891 { 892 static SwappyVk instance; 893 return instance; 894 } 895 ~SwappyVk() { 896 if(mLibVulkan) 897 dlclose(mLibVulkan); 898 } 899 900 void swappyVkDetermineDeviceExtensions(VkPhysicalDevice physicalDevice, 901 uint32_t availableExtensionCount, 902 VkExtensionProperties* pAvailableExtensions, 903 uint32_t* pRequiredExtensionCount, 904 char** pRequiredExtensions); 905 void SetQueueFamilyIndex(VkDevice device, 906 VkQueue queue, 907 uint32_t queueFamilyIndex); 908 bool GetRefreshCycleDuration(VkPhysicalDevice physicalDevice, 909 VkDevice device, 910 VkSwapchainKHR swapchain, 911 uint64_t* pRefreshDuration); 912 void SetSwapInterval(VkDevice device, 913 VkSwapchainKHR swapchain, 914 uint32_t interval); 915 VkResult QueuePresent(VkQueue queue, 916 const VkPresentInfoKHR* pPresentInfo); 917 void DestroySwapchain(VkDevice device, 918 VkSwapchainKHR swapchain); 919 920 private: 921 std::map<VkPhysicalDevice, bool> doesPhysicalDeviceHaveGoogleDisplayTiming; 922 std::map<VkDevice, std::shared_ptr<SwappyVkBase>> perDeviceImplementation; 923 std::map<VkSwapchainKHR, std::shared_ptr<SwappyVkBase>> perSwapchainImplementation; 924 925 struct QueueFamilyIndex { 926 VkDevice device; 927 uint32_t queueFamilyIndex; 928 }; 929 std::map<VkQueue, QueueFamilyIndex> perQueueFamilyIndex; 930 931 void *mLibVulkan = nullptr; 932 933 private: 934 SwappyVk() {} // Need to implement this constructor 935 SwappyVk(SwappyVk const&); // Don't implement a copy constructor--no copies 936 void operator=(SwappyVk const&); // Don't implement--no copies 937 }; 938 939 /** 940 * Generic/Singleton implementation of swappyVkDetermineDeviceExtensions. 941 */ 942 void SwappyVk::swappyVkDetermineDeviceExtensions( 943 VkPhysicalDevice physicalDevice, 944 uint32_t availableExtensionCount, 945 VkExtensionProperties* pAvailableExtensions, 946 uint32_t* pRequiredExtensionCount, 947 char** pRequiredExtensions) 948 { 949 // TODO: Refactor this to be more concise: 950 if (!pRequiredExtensions) { 951 for (uint32_t i = 0; i < availableExtensionCount; i++) { 952 if (!strcmp(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME, 953 pAvailableExtensions[i].extensionName)) { 954 (*pRequiredExtensionCount)++; 955 } 956 } 957 } else { 958 doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] = false; 959 for (uint32_t i = 0, j = 0; i < availableExtensionCount; i++) { 960 if (!strcmp(VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME, 961 pAvailableExtensions[i].extensionName)) { 962 if (j < *pRequiredExtensionCount) { 963 strcpy(pRequiredExtensions[j++], VK_GOOGLE_DISPLAY_TIMING_EXTENSION_NAME); 964 doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice] = true; 965 } 966 } 967 } 968 } 969 } 970 971 void SwappyVk::SetQueueFamilyIndex(VkDevice device, 972 VkQueue queue, 973 uint32_t queueFamilyIndex) 974 { 975 perQueueFamilyIndex[queue] = {device, queueFamilyIndex}; 976 } 977 978 979 /** 980 * Generic/Singleton implementation of swappyVkGetRefreshCycleDuration. 981 */ 982 bool SwappyVk::GetRefreshCycleDuration(VkPhysicalDevice physicalDevice, 983 VkDevice device, 984 VkSwapchainKHR swapchain, 985 uint64_t* pRefreshDuration) 986 { 987 auto& pImplementation = perDeviceImplementation[device]; 988 if (!pImplementation) { 989 // We have not seen this device yet. 990 if (!mLibVulkan) { 991 // This is the first time we've been called--initialize function pointers: 992 mLibVulkan = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); 993 if (!mLibVulkan) 994 { 995 // If Vulkan doesn't exist, bail-out early: 996 return false; 997 } 998 } 999 1000 // First, based on whether VK_GOOGLE_display_timing is available 1001 // (determined and cached by swappyVkDetermineDeviceExtensions), 1002 // determine which derived class to use to implement the rest of the API 1003 if (doesPhysicalDeviceHaveGoogleDisplayTiming[physicalDevice]) { 1004 #ifdef ANDROID 1005 pImplementation = std::make_shared<SwappyVkGoogleDisplayTimingAndroid> 1006 (physicalDevice, device, getInstance(), mLibVulkan); 1007 ALOGV("SwappyVk initialized for VkDevice %p using VK_GOOGLE_display_timing on Android", device); 1008 #else 1009 // Instantiate the class that sits on top of VK_GOOGLE_display_timing 1010 pImplementation = std::make_shared<SwappyVkGoogleDisplayTiming> 1011 (physicalDevice, device, getInstance(), mLibVulkan); 1012 ALOGV("SwappyVk initialized for VkDevice %p using VK_GOOGLE_display_timing", device); 1013 #endif 1014 } else { 1015 // Instantiate the class that sits on top of the basic Vulkan APIs 1016 #ifdef ANDROID 1017 pImplementation = std::make_shared<SwappyVkAndroidFallback> 1018 (physicalDevice, device, getInstance(), mLibVulkan); 1019 ALOGV("SwappyVk initialized for VkDevice %p using Android fallback", device); 1020 #else // ANDROID 1021 pImplementation = std::make_shared<SwappyVkVulkanFallback> 1022 (physicalDevice, device, getInstance(), mLibVulkan); 1023 ALOGV("SwappyVk initialized for VkDevice %p using Vulkan-only fallback", device); 1024 #endif // ANDROID 1025 } 1026 1027 if (!pImplementation) { 1028 // This shouldn't happen, but if it does, something is really wrong. 1029 return false; 1030 } 1031 } 1032 1033 // Cache the per-swapchain pointer to the derived class: 1034 perSwapchainImplementation[swapchain] = pImplementation; 1035 1036 // Now, call that derived class to get the refresh duration to return 1037 return pImplementation->doGetRefreshCycleDuration(swapchain, pRefreshDuration); 1038 } 1039 1040 1041 /** 1042 * Generic/Singleton implementation of swappyVkSetSwapInterval. 1043 */ 1044 void SwappyVk::SetSwapInterval(VkDevice device, 1045 VkSwapchainKHR swapchain, 1046 uint32_t interval) 1047 { 1048 auto& pImplementation = perDeviceImplementation[device]; 1049 if (!pImplementation) { 1050 return; 1051 } 1052 pImplementation->doSetSwapInterval(swapchain, interval); 1053 } 1054 1055 1056 /** 1057 * Generic/Singleton implementation of swappyVkQueuePresent. 1058 */ 1059 VkResult SwappyVk::QueuePresent(VkQueue queue, 1060 const VkPresentInfoKHR* pPresentInfo) 1061 { 1062 if (perQueueFamilyIndex.find(queue) == perQueueFamilyIndex.end()) { 1063 ALOGE("Unknown queue %p. Did you call SwappyVkSetQueueFamilyIndex ?", queue); 1064 return VK_INCOMPLETE; 1065 } 1066 1067 // This command doesn't have a VkDevice. It should have at least one VkSwapchainKHR's. For 1068 // this command, all VkSwapchainKHR's will have the same VkDevice and VkQueue. 1069 if ((pPresentInfo->swapchainCount == 0) || (!pPresentInfo->pSwapchains)) { 1070 // This shouldn't happen, but if it does, something is really wrong. 1071 return VK_ERROR_DEVICE_LOST; 1072 } 1073 auto& pImplementation = perSwapchainImplementation[*pPresentInfo->pSwapchains]; 1074 if (pImplementation) { 1075 return pImplementation->doQueuePresent(queue, 1076 perQueueFamilyIndex[queue].queueFamilyIndex, 1077 pPresentInfo); 1078 } else { 1079 // This should only happen if the API was used wrong (e.g. they never 1080 // called swappyVkGetRefreshCycleDuration). 1081 // NOTE: Technically, a Vulkan library shouldn't protect a user from 1082 // themselves, but we'll be friendlier 1083 return VK_ERROR_DEVICE_LOST; 1084 } 1085 } 1086 1087 void SwappyVk::DestroySwapchain(VkDevice device, 1088 VkSwapchainKHR swapchain) { 1089 auto it = perQueueFamilyIndex.begin(); 1090 while (it != perQueueFamilyIndex.end()) { 1091 if (it->second.device == device) { 1092 it = perQueueFamilyIndex.erase(it); 1093 } else { 1094 ++it; 1095 } 1096 } 1097 1098 perDeviceImplementation[device] = nullptr; 1099 perSwapchainImplementation[swapchain] = nullptr; 1100 } 1101 1102 1103 /*************************************************************************************************** 1104 * 1105 * API ENTRYPOINTS 1106 * 1107 ***************************************************************************************************/ 1108 1109 extern "C" { 1110 1111 void SwappyVk_determineDeviceExtensions( 1112 VkPhysicalDevice physicalDevice, 1113 uint32_t availableExtensionCount, 1114 VkExtensionProperties* pAvailableExtensions, 1115 uint32_t* pRequiredExtensionCount, 1116 char** pRequiredExtensions) 1117 { 1118 TRACE_CALL(); 1119 SwappyVk& swappy = SwappyVk::getInstance(); 1120 swappy.swappyVkDetermineDeviceExtensions(physicalDevice, 1121 availableExtensionCount, pAvailableExtensions, 1122 pRequiredExtensionCount, pRequiredExtensions); 1123 } 1124 1125 void SwappyVk_setQueueFamilyIndex( 1126 VkDevice device, 1127 VkQueue queue, 1128 uint32_t queueFamilyIndex) 1129 { 1130 TRACE_CALL(); 1131 SwappyVk& swappy = SwappyVk::getInstance(); 1132 swappy.SetQueueFamilyIndex(device, queue, queueFamilyIndex); 1133 } 1134 1135 bool SwappyVk_initAndGetRefreshCycleDuration( 1136 VkPhysicalDevice physicalDevice, 1137 VkDevice device, 1138 VkSwapchainKHR swapchain, 1139 uint64_t* pRefreshDuration) 1140 { 1141 TRACE_CALL(); 1142 SwappyVk& swappy = SwappyVk::getInstance(); 1143 return swappy.GetRefreshCycleDuration(physicalDevice, device, swapchain, pRefreshDuration); 1144 } 1145 1146 void SwappyVk_setSwapInterval( 1147 VkDevice device, 1148 VkSwapchainKHR swapchain, 1149 uint32_t interval) 1150 { 1151 TRACE_CALL(); 1152 SwappyVk& swappy = SwappyVk::getInstance(); 1153 swappy.SetSwapInterval(device, swapchain, interval); 1154 } 1155 1156 VkResult SwappyVk_queuePresent( 1157 VkQueue queue, 1158 const VkPresentInfoKHR* pPresentInfo) 1159 { 1160 TRACE_CALL(); 1161 SwappyVk& swappy = SwappyVk::getInstance(); 1162 return swappy.QueuePresent(queue, pPresentInfo); 1163 } 1164 1165 void SwappyVk_destroySwapchain( 1166 VkDevice device, 1167 VkSwapchainKHR swapchain) 1168 { 1169 TRACE_CALL(); 1170 SwappyVk& swappy = SwappyVk::getInstance(); 1171 swappy.DestroySwapchain(device, swapchain); 1172 } 1173 1174 } // extern "C" 1175