From f0cb31a12b59f05177f07ab5a46bc9084ba5fbc9 Mon Sep 17 00:00:00 2001 From: Bill Hollings Date: Fri, 15 Sep 2023 09:54:48 -0400 Subject: [PATCH] Rework workaround to force incomplete CAMetalDrawable presentations to complete. - To force any incomplete CAMetalDrawable presentations to complete, don't force the creation of another transient drawable, as this can stall the creation of future drawables. Instead, when a swapchain is destroyed, or replaced by a new swapchain, set the CAMetalLayer drawableSize, which will force presentation completion. - Add presentation completion handler in command buffer scheduling callback, move marking available to presentation completion handler, and minimize mutex locking. - MVKQueue::waitIdle() remove wait for swapchain presentations, and remove callbacks to MVKQueue from drawable completions. - MVKQueue::submit() don't bypass submitting a misconfigured submission, so that semaphores and fences will be signalled, and ensure misconfigured submissions are well behaved. - Add MVKSwapchain::getCAMetalLayer() to streamline layer access (unrelated). --- .../MoltenVK/Commands/MVKCommandBuffer.mm | 2 +- MoltenVK/MoltenVK/GPUObjects/MVKImage.h | 10 +- MoltenVK/MoltenVK/GPUObjects/MVKImage.mm | 99 ++++++++++--------- MoltenVK/MoltenVK/GPUObjects/MVKQueue.h | 11 --- MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm | 81 +++------------ MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h | 9 +- MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm | 53 +++++++--- 7 files changed, 118 insertions(+), 147 deletions(-) diff --git a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm index 8ac91c26..72dde4f1 100644 --- a/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm +++ b/MoltenVK/MoltenVK/Commands/MVKCommandBuffer.mm @@ -260,7 +260,7 @@ bool MVKCommandBuffer::canExecute() { } _wasExecuted = true; - return true; + return wasConfigurationSuccessful(); } // Return the number of bits set in the view mask, with a minimum value of 1. diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h index 1479f724..ef606b03 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.h @@ -460,10 +460,9 @@ public: void beginPresentation(const MVKImagePresentInfo& presentInfo); /** Called via callback when the presentation completes. */ - void endPresentation(const MVKImagePresentInfo& presentInfo, uint64_t actualPresentTime = 0); - - /** If this image is stuck in-flight, attempt to force it to complete. */ - void forcePresentationCompletion(); + void endPresentation(const MVKImagePresentInfo& presentInfo, + const MVKSwapchainSignaler& signaler, + uint64_t actualPresentTime = 0); #pragma mark Construction @@ -478,12 +477,13 @@ protected: friend MVKSwapchain; id getCAMetalDrawable() override; - void addPresentedHandler(id mtlDrawable, MVKImagePresentInfo presentInfo); + void addPresentedHandler(id mtlDrawable, MVKImagePresentInfo presentInfo, MVKSwapchainSignaler signaler); void releaseMetalDrawable(); MVKSwapchainImageAvailability getAvailability(); void makeAvailable(const MVKSwapchainSignaler& signaler); void makeAvailable(); VkResult acquireAndSignalWhenAvailable(MVKSemaphore* semaphore, MVKFence* fence); + MVKSwapchainSignaler getPresentationSignaler(); id _mtlDrawable = nil; MVKSwapchainImageAvailability _availability; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm index 1769df11..f09495c7 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKImage.mm @@ -1248,16 +1248,18 @@ static void signalAndUnmarkAsTracked(const MVKSwapchainSignaler& signaler) { } VkResult MVKPresentableSwapchainImage::acquireAndSignalWhenAvailable(MVKSemaphore* semaphore, MVKFence* fence) { + + // Now that this image is being acquired, release the existing drawable and its texture. + // This is not done earlier so the texture is retained for any post-processing such as screen captures, etc. + // This may trigger a delayed presentation callback, which uses the _availabilityLock, also used below. + releaseMetalDrawable(); + lock_guard lock(_availabilityLock); // Upon acquisition, update acquisition ID immediately, to move it to the back of the chain, // so other images will be preferred if either all images are available or no images are available. _availability.acquisitionID = _swapchain->getNextAcquisitionID(); - // Now that this image is being acquired, release the existing drawable and its texture. - // This is not done earlier so the texture is retained for any post-processing such as screen captures, etc. - releaseMetalDrawable(); - auto signaler = MVKSwapchainSignaler{fence, semaphore, semaphore ? semaphore->deferSignal() : 0}; if (_availability.isAvailable) { _availability.isAvailable = false; @@ -1292,10 +1294,10 @@ id MVKPresentableSwapchainImage::getCAMetalDrawable() { if ( !_mtlDrawable ) { @autoreleasepool { bool hasInvalidFormat = false; - uint32_t attemptCnt = _swapchain->getImageCount() * 2; // Attempt a resonable number of times + uint32_t attemptCnt = _swapchain->getImageCount(); // Attempt a resonable number of times for (uint32_t attemptIdx = 0; !_mtlDrawable && attemptIdx < attemptCnt; attemptIdx++) { uint64_t startTime = _device->getPerformanceTimestamp(); - _mtlDrawable = [_swapchain->_surface->getCAMetalLayer().nextDrawable retain]; // retained + _mtlDrawable = [_swapchain->getCAMetalLayer().nextDrawable retain]; // retained _device->addPerformanceInterval(_device->_performanceStatistics.queue.retrieveCAMetalDrawable, startTime); hasInvalidFormat = _mtlDrawable && !_mtlDrawable.texture.pixelFormat; if (hasInvalidFormat) { releaseMetalDrawable(); } @@ -1314,8 +1316,6 @@ id MVKPresentableSwapchainImage::getCAMetalDrawable() { // Pass MVKImagePresentInfo by value because it may not exist when the callback runs. VkResult MVKPresentableSwapchainImage::presentCAMetalDrawable(id mtlCmdBuff, MVKImagePresentInfo presentInfo) { - lock_guard lock(_availabilityLock); - _swapchain->renderWatermark(getMTLTexture(0), mtlCmdBuff); // According to Apple, it is more performant to call MTLDrawable present from within a @@ -1323,8 +1323,11 @@ VkResult MVKPresentableSwapchainImage::presentCAMetalDrawable(id mtlDrwbl = getCAMetalDrawable(); - addPresentedHandler(mtlDrwbl, presentInfo); + MVKSwapchainSignaler signaler = getPresentationSignaler(); [mtlCmdBuff addScheduledHandler: ^(id mcb) { + + addPresentedHandler(mtlDrwbl, presentInfo, signaler); + // Try to do any present mode transitions as late as possible in an attempt // to avoid visual disruptions on any presents already on the queue. if (presentInfo.presentMode != VK_PRESENT_MODE_MAX_ENUM_KHR) { @@ -1337,7 +1340,32 @@ VkResult MVKPresentableSwapchainImage::presentCAMetalDrawable(idretain(); } + [mtlCmdBuff addCompletedHandler: ^(id mcb) { + if (fence) { + fence->signal(); + fence->release(); + } + [mtlDrwbl release]; + release(); + }]; + + signalPresentationSemaphore(signaler, mtlCmdBuff); + + return getConfigurationResult(); +} + +MVKSwapchainSignaler MVKPresentableSwapchainImage::getPresentationSignaler() { + lock_guard lock(_availabilityLock); + // Mark this image as available if no semaphores or fences are waiting to be signaled. _availability.isAvailable = _availabilitySignalers.empty(); if (_availability.isAvailable) { @@ -1346,52 +1374,34 @@ VkResult MVKPresentableSwapchainImage::presentCAMetalDrawable(idretain(); } - [mtlCmdBuff addCompletedHandler: ^(id mcb) { - [mtlDrwbl release]; - makeAvailable(signaler); - release(); - if (fence) { - fence->signal(); - fence->release(); - } - }]; - - signalPresentationSemaphore(signaler, mtlCmdBuff); - - return getConfigurationResult(); } -// Pass MVKImagePresentInfo by value because it may not exist when the callback runs. +// Pass MVKImagePresentInfo & MVKSwapchainSignaler by value because they may not exist when the callback runs. void MVKPresentableSwapchainImage::addPresentedHandler(id mtlDrawable, - MVKImagePresentInfo presentInfo) { + MVKImagePresentInfo presentInfo, + MVKSwapchainSignaler signaler) { beginPresentation(presentInfo); #if !MVK_OS_SIMULATOR if ([mtlDrawable respondsToSelector: @selector(addPresentedHandler:)]) { [mtlDrawable addPresentedHandler: ^(id mtlDrwbl) { - endPresentation(presentInfo, mtlDrwbl.presentedTime * 1.0e9); + endPresentation(presentInfo, signaler, mtlDrwbl.presentedTime * 1.0e9); }]; } else #endif { // If MTLDrawable.presentedTime/addPresentedHandler isn't supported, // treat it as if the present happened when requested. - endPresentation(presentInfo); + endPresentation(presentInfo, signaler); } } @@ -1399,11 +1409,11 @@ void MVKPresentableSwapchainImage::addPresentedHandler(id mtlDr void MVKPresentableSwapchainImage::beginPresentation(const MVKImagePresentInfo& presentInfo) { retain(); _swapchain->beginPresentation(presentInfo); - presentInfo.queue->beginPresentation(presentInfo); _presentationStartTime = getDevice()->getPerformanceTimestamp(); } void MVKPresentableSwapchainImage::endPresentation(const MVKImagePresentInfo& presentInfo, + const MVKSwapchainSignaler& signaler, uint64_t actualPresentTime) { { // Scope to avoid deadlock if release() is run within detachment lock // If I have become detached from the swapchain, it means the swapchain, and possibly the @@ -1412,7 +1422,7 @@ void MVKPresentableSwapchainImage::endPresentation(const MVKImagePresentInfo& pr if (_device) { _device->addPerformanceInterval(_device->_performanceStatistics.queue.presentSwapchains, _presentationStartTime); } if (_swapchain) { _swapchain->endPresentation(presentInfo, actualPresentTime); } } - presentInfo.queue->endPresentation(presentInfo); + makeAvailable(signaler); release(); } @@ -1432,7 +1442,9 @@ void MVKPresentableSwapchainImage::makeAvailable(const MVKSwapchainSignaler& sig } // Signal, untrack, and release any signalers that are tracking. +// Release the drawable before the lock, as it may trigger completion callback. void MVKPresentableSwapchainImage::makeAvailable() { + releaseMetalDrawable(); lock_guard lock(_availabilityLock); if ( !_availability.isAvailable ) { @@ -1445,14 +1457,6 @@ void MVKPresentableSwapchainImage::makeAvailable() { } } -// Clear the existing CAMetalDrawable and retrieve and release a new transient one, -// in an attempt to trigger the existing CAMetalDrawable to complete it's callback. -void MVKPresentableSwapchainImage::forcePresentationCompletion() { - releaseMetalDrawable(); - if (_swapchain) { @autoreleasepool { [_swapchain->_surface->getCAMetalLayer() nextDrawable]; } } -} - - #pragma mark Construction MVKPresentableSwapchainImage::MVKPresentableSwapchainImage(MVKDevice* device, @@ -1467,14 +1471,13 @@ MVKPresentableSwapchainImage::MVKPresentableSwapchainImage(MVKDevice* device, void MVKPresentableSwapchainImage::destroy() { - forcePresentationCompletion(); + releaseMetalDrawable(); MVKSwapchainImage::destroy(); } // Unsignaled signalers will exist if this image is acquired more than it is presented. // Ensure they are signaled and untracked so the fences and semaphores will be released. MVKPresentableSwapchainImage::~MVKPresentableSwapchainImage() { - releaseMetalDrawable(); makeAvailable(); } diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h index 0de3d2b8..c3b1d242 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.h @@ -100,13 +100,6 @@ public: /** Block the current thread until this queue is idle. */ VkResult waitIdle(MVKCommandUse cmdUse); - /** Mark the beginning of a swapchain image presentation. */ - void beginPresentation(const MVKImagePresentInfo& presentInfo); - - /** Mark the end of a swapchain image presentation. */ - void endPresentation(const MVKImagePresentInfo& presentInfo); - - #pragma mark Metal /** Returns the Metal queue underlying this queue. */ @@ -150,11 +143,8 @@ protected: VkResult submit(MVKQueueSubmission* qSubmit); NSString* getMTLCommandBufferLabel(MVKCommandUse cmdUse); void handleMTLCommandBufferError(id mtlCmdBuff); - void waitSwapchainPresentations(MVKCommandUse cmdUse); MVKQueueFamily* _queueFamily; - MVKSemaphoreImpl _presentationCompletionBlocker; - std::unordered_map _presentedImages; std::string _name; dispatch_queue_t _execQueue; id _mtlQueue = nil; @@ -166,7 +156,6 @@ protected: NSString* _mtlCmdBuffLabelAcquireNextImage = nil; NSString* _mtlCmdBuffLabelInvalidateMappedMemoryRanges = nil; MVKGPUCaptureScope* _submissionCaptureScope = nil; - std::mutex _presentedImagesLock; float _priority; uint32_t _index; }; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm index 1c28f63f..401fa8b2 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKQueue.mm @@ -80,13 +80,14 @@ VkResult MVKQueue::submit(MVKQueueSubmission* qSubmit) { if ( !qSubmit ) { return VK_SUCCESS; } // Ignore nils - VkResult rslt = qSubmit->getConfigurationResult(); // Extract result before submission to avoid race condition with early destruction - if (rslt == VK_SUCCESS) { - if (_execQueue) { - dispatch_async(_execQueue, ^{ execute(qSubmit); } ); - } else { - rslt = execute(qSubmit); - } + // Extract result before submission to avoid race condition with early destruction + // Submit regardless of config result, to ensure submission semaphores and fences are signalled. + // The submissions will ensure a misconfiguration will be safe to execute. + VkResult rslt = qSubmit->getConfigurationResult(); + if (_execQueue) { + dispatch_async(_execQueue, ^{ execute(qSubmit); } ); + } else { + rslt = execute(qSubmit); } return rslt; } @@ -140,50 +141,9 @@ VkResult MVKQueue::waitIdle(MVKCommandUse cmdUse) { [mtlCmdBuff commit]; [mtlCmdBuff waitUntilCompleted]; - waitSwapchainPresentations(cmdUse); - return VK_SUCCESS; } -// If there are any swapchain presentations in flight, wait a few frames for them to complete. -// If they don't complete within a few frames, attempt to force them to complete, and wait another -// few frames for that to happen. If there are still swapchain presentations that haven't completed, -// log a warning, and force them to end presentation, so the images and drawables will be released. -void MVKQueue::waitSwapchainPresentations(MVKCommandUse cmdUse) { - uint32_t waitFrames = _device->_pMetalFeatures->maxSwapchainImageCount + 2; - uint64_t waitNanos = waitFrames * _device->_performanceStatistics.queue.frameInterval.average * 1e6; - if (_presentationCompletionBlocker.wait(waitNanos)) { return; } - - auto imgCnt = _presentationCompletionBlocker.getReservationCount(); - MVKPresentableSwapchainImage* images[imgCnt]; - mvkClear(images, imgCnt); - - { - // Scope of image lock limited to creating array copy of uncompleted presentations - // Populate a working array of the unpresented images. - lock_guard lock(_presentedImagesLock); - size_t imgIdx = 0; - for (auto imgPair : _presentedImages) { images[imgIdx++] = imgPair.first; } - } - - // Attempt to force each image to complete presentation through the callback. - for (size_t imgIdx = 0; imgIdx < imgCnt && _presentationCompletionBlocker.getReservationCount(); imgIdx++) { - auto* img = images[imgIdx]; - if (img) { img->forcePresentationCompletion(); } - } - - // Wait for forced presentation completions. If we still have unfinished swapchain image - // presentations, log a warning, and force each image to end, so that it can be released. - if ( !_presentationCompletionBlocker.wait(waitNanos) ) { - reportWarning(VK_TIMEOUT, "%s timed out after %d frames while awaiting %d swapchain image presentations to complete.", - mvkVkCommandName(cmdUse), waitFrames * 2, _presentationCompletionBlocker.getReservationCount()); - for (size_t imgIdx = 0; imgIdx < imgCnt; imgIdx++) { - auto* img = images[imgIdx]; - if (_presentedImages.count(img)) { img->endPresentation({.queue = this, .presentableImage = img}); } - } - } -} - id MVKQueue::getMTLCommandBuffer(MVKCommandUse cmdUse, bool retainRefs) { id mtlCmdBuff = nil; MVKDevice* mvkDev = getDevice(); @@ -312,25 +272,6 @@ void MVKQueue::handleMTLCommandBufferError(id mtlCmdBuff) { #endif } -// _presentedImages counts presentations per swapchain image, because the presentation of an image can -// begin before the previous presentation of that image has indicated that it has completed via a callback. -void MVKQueue::beginPresentation(const MVKImagePresentInfo& presentInfo) { - lock_guard lock(_presentedImagesLock); - _presentationCompletionBlocker.reserve(); - _presentedImages[presentInfo.presentableImage]++; -} - -void MVKQueue::endPresentation(const MVKImagePresentInfo& presentInfo) { - lock_guard lock(_presentedImagesLock); - _presentationCompletionBlocker.release(); - if (_presentedImages[presentInfo.presentableImage]) { - _presentedImages[presentInfo.presentableImage]--; - } - if ( !_presentedImages[presentInfo.presentableImage] ) { - _presentedImages.erase(presentInfo.presentableImage); - } -} - #pragma mark Construction #define MVK_DISPATCH_QUEUE_QOS_CLASS QOS_CLASS_USER_INITIATED @@ -488,7 +429,7 @@ VkResult MVKQueueCommandBufferSubmission::commitActiveMTLCommandBuffer(bool sign // If we need to signal completion, use getActiveMTLCommandBuffer() to ensure at least // one MTLCommandBuffer is used, otherwise if this instance has no content, it will not - // finish(), signal the fence and semaphores ,and be destroyed. + // finish(), signal the fence and semaphores, and be destroyed. // Use temp var for MTLCommandBuffer commit and release because completion callback // may destroy this instance before this function ends. id mtlCmdBuff = signalCompletion ? getActiveMTLCommandBuffer() : _activeMTLCommandBuffer; @@ -501,6 +442,8 @@ VkResult MVKQueueCommandBufferSubmission::commitActiveMTLCommandBuffer(bool sign if (signalCompletion) { this->finish(); } // Must be the last thing the completetion callback does. }]; + // Retrieve the result before committing MTLCommandBuffer, because finish() will destroy this instance. + VkResult rslt = mtlCmdBuff ? getConfigurationResult() : VK_ERROR_OUT_OF_POOL_MEMORY; [mtlCmdBuff commit]; [mtlCmdBuff release]; // retained @@ -508,7 +451,7 @@ VkResult MVKQueueCommandBufferSubmission::commitActiveMTLCommandBuffer(bool sign // was not created, call the finish() function directly. if (signalCompletion && !mtlCmdBuff) { finish(); } - return mtlCmdBuff ? VK_SUCCESS : VK_ERROR_OUT_OF_POOL_MEMORY; + return rslt; } // Be sure to retain() any API objects referenced in this function, and release() them in the diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h index 7e7cff8c..cd418bd1 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h +++ b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.h @@ -43,11 +43,14 @@ public: /** Returns the debug report object type of this object. */ VkDebugReportObjectTypeEXT getVkDebugReportObjectType() override { return VK_DEBUG_REPORT_OBJECT_TYPE_SWAPCHAIN_KHR_EXT; } + /** Returns the CAMetalLayer underlying the surface used by this swapchain. */ + CAMetalLayer* getCAMetalLayer(); + /** Returns the number of images in this swapchain. */ - inline uint32_t getImageCount() { return (uint32_t)_presentableImages.size(); } + uint32_t getImageCount() { return (uint32_t)_presentableImages.size(); } /** Returns the image at the specified index. */ - inline MVKPresentableSwapchainImage* getPresentableImage(uint32_t index) { return _presentableImages[index]; } + MVKPresentableSwapchainImage* getPresentableImage(uint32_t index) { return _presentableImages[index]; } /** * Returns the array of presentable images associated with this swapchain. @@ -112,6 +115,7 @@ protected: void markFrameInterval(); void beginPresentation(const MVKImagePresentInfo& presentInfo); void endPresentation(const MVKImagePresentInfo& presentInfo, uint64_t actualPresentTime = 0); + void forceUnpresentedImageCompletion(); MVKSurface* _surface = nullptr; MVKWatermark* _licenseWatermark = nullptr; @@ -123,6 +127,7 @@ protected: std::mutex _presentHistoryLock; uint64_t _lastFrameTime = 0; VkExtent2D _mtlLayerDrawableExtent = {0, 0}; + std::atomic _unpresentedImageCount = 0; uint32_t _currentPerfLogFrameCount = 0; uint32_t _presentHistoryCount = 0; uint32_t _presentHistoryIndex = 0; diff --git a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm index 159c2edf..63c3ac78 100644 --- a/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm +++ b/MoltenVK/MoltenVK/GPUObjects/MVKSwapchain.mm @@ -47,6 +47,8 @@ void MVKSwapchain::propagateDebugName() { } } +CAMetalLayer* MVKSwapchain::getCAMetalLayer() { return _surface->getCAMetalLayer(); } + VkResult MVKSwapchain::getImages(uint32_t* pCount, VkImage* pSwapchainImages) { // Get the number of surface images @@ -104,7 +106,7 @@ VkResult MVKSwapchain::releaseImages(const VkReleaseSwapchainImagesInfoEXT* pRel getPresentableImage(pReleaseInfo->pImageIndices[imgIdxIdx])->makeAvailable(); } - return VK_SUCCESS; + return _surface->getConfigurationResult(); } uint64_t MVKSwapchain::getNextAcquisitionID() { return ++_currentAcquisitionID; } @@ -128,7 +130,7 @@ VkResult MVKSwapchain::getSurfaceStatus() { bool MVKSwapchain::hasOptimalSurface() { if (_isDeliberatelyScaled) { return true; } - auto* mtlLayer = _surface->getCAMetalLayer(); + auto* mtlLayer = getCAMetalLayer(); VkExtent2D drawExtent = mvkVkExtent2DFromCGSize(mtlLayer.drawableSize); return (mvkVkExtent2DsAreEqual(drawExtent, _mtlLayerDrawableExtent) && mvkVkExtent2DsAreEqual(drawExtent, mvkGetNaturalExtent(mtlLayer))); @@ -185,7 +187,7 @@ void MVKSwapchain::markFrameInterval() { VkResult MVKSwapchain::getRefreshCycleDuration(VkRefreshCycleDurationGOOGLE *pRefreshCycleDuration) { if (_device->getConfigurationResult() != VK_SUCCESS) { return _device->getConfigurationResult(); } - auto* mtlLayer = _surface->getCAMetalLayer(); + auto* mtlLayer = getCAMetalLayer(); #if MVK_VISIONOS // TODO: See if this can be obtained from OS instead NSInteger framesPerSecond = 90; @@ -242,9 +244,13 @@ VkResult MVKSwapchain::getPastPresentationTiming(uint32_t *pCount, VkPastPresent return res; } -void MVKSwapchain::beginPresentation(const MVKImagePresentInfo& presentInfo) {} +void MVKSwapchain::beginPresentation(const MVKImagePresentInfo& presentInfo) { + _unpresentedImageCount++; +} void MVKSwapchain::endPresentation(const MVKImagePresentInfo& presentInfo, uint64_t actualPresentTime) { + _unpresentedImageCount--; + std::lock_guard lock(_presentHistoryLock); markFrameInterval(); @@ -269,8 +275,18 @@ void MVKSwapchain::endPresentation(const MVKImagePresentInfo& presentInfo, uint6 _presentHistoryIndex = (_presentHistoryIndex + 1) % kMaxPresentationHistory; } +// Because of a regression in Metal, the most recent one or two presentations may not complete +// and call back. To work around this, if there are any uncompleted presentations, change the +// drawableSize of the CAMetalLayer, which will trigger presentation completion and callbacks. +// The drawableSize will be set to a correct size by the next swapchain created on the same surface. +void MVKSwapchain::forceUnpresentedImageCompletion() { + if (_unpresentedImageCount) { + getCAMetalLayer().drawableSize = { 1,1 }; + } +} + void MVKSwapchain::setLayerNeedsDisplay(const VkPresentRegionKHR* pRegion) { - auto* mtlLayer = _surface->getCAMetalLayer(); + auto* mtlLayer = getCAMetalLayer(); if (!pRegion || pRegion->rectangleCount == 0) { [mtlLayer setNeedsDisplay]; return; @@ -350,7 +366,7 @@ void MVKSwapchain::setHDRMetadataEXT(const VkHdrMetadataEXT& metadata) { CAEDRMetadata* caMetadata = [CAEDRMetadata HDR10MetadataWithDisplayInfo: colorVolData contentInfo: lightLevelData opticalOutputScale: 1]; - auto* mtlLayer = _surface->getCAMetalLayer(); + auto* mtlLayer = getCAMetalLayer(); mtlLayer.EDRMetadata = caMetadata; mtlLayer.wantsExtendedDynamicRangeContent = YES; [caMetadata release]; @@ -456,7 +472,7 @@ void MVKSwapchain::initCAMetalLayer(const VkSwapchainCreateInfoKHR* pCreateInfo, if ( getIsSurfaceLost() ) { return; } - auto* mtlLayer = _surface->getCAMetalLayer(); + auto* mtlLayer = getCAMetalLayer(); auto minMagFilter = mvkConfig().swapchainMinMagFilterUseNearest ? kCAFilterNearest : kCAFilterLinear; mtlLayer.device = getMTLDevice(); mtlLayer.pixelFormat = getPixelFormats()->getMTLPixelFormat(pCreateInfo->imageFormat); @@ -469,6 +485,16 @@ void MVKSwapchain::initCAMetalLayer(const VkSwapchainCreateInfoKHR* pCreateInfo, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT)); + + // Because of a regression in Metal, the most recent one or two presentations may not + // complete and call back. Changing the CAMetalLayer drawableSize will force any incomplete + // presentations on the oldSwapchain to complete and call back, but if the drawableSize + // is not changing from the previous, we force those completions first. + auto* oldSwapchain = (MVKSwapchain*)pCreateInfo->oldSwapchain; + if (oldSwapchain && mvkVkExtent2DsAreEqual(pCreateInfo->imageExtent, mvkVkExtent2DFromCGSize(mtlLayer.drawableSize))) { + oldSwapchain->forceUnpresentedImageCompletion(); + } + // Remember the extent to later detect if it has changed under the covers, // and set the drawable size of the CAMetalLayer from the extent. _mtlLayerDrawableExtent = pCreateInfo->imageExtent; @@ -559,7 +585,7 @@ void MVKSwapchain::initSurfaceImages(const VkSwapchainCreateInfoKHR* pCreateInfo } } - auto* mtlLayer = _surface->getCAMetalLayer(); + auto* mtlLayer = getCAMetalLayer(); VkExtent2D imgExtent = pCreateInfo->imageExtent; VkImageCreateInfo imgInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, @@ -598,12 +624,17 @@ void MVKSwapchain::initSurfaceImages(const VkSwapchainCreateInfoKHR* pCreateInfo screenName = mtlLayer.screenMVK.localizedName; } #endif - MVKLogInfo("Created %d swapchain images with initial size (%d, %d) and contents scale %.1f for screen %s.", - imgCnt, imgExtent.width, imgExtent.height, mtlLayer.contentsScale, screenName.UTF8String); + MVKLogInfo("Created %d swapchain images with size (%d, %d) and contents scale %.1f in layer %s (%p) on screen %s.", + imgCnt, imgExtent.width, imgExtent.height, mtlLayer.contentsScale, mtlLayer.name.UTF8String, mtlLayer, screenName.UTF8String); } void MVKSwapchain::destroy() { - if (_surface->_activeSwapchain == this) { _surface->_activeSwapchain = nullptr; } + // If this swapchain was not replaced by a new swapchain, remove this swapchain + // from the surface, and force any outstanding presentations to complete. + if (_surface->_activeSwapchain == this) { + _surface->_activeSwapchain = nullptr; + forceUnpresentedImageCompletion(); + } for (auto& img : _presentableImages) { _device->destroyPresentableSwapchainImage(img, NULL); } MVKVulkanAPIDeviceObject::destroy(); }