Merge pull request #1669 from billhollings/fix-retrieve-screen-refresh-duration

Fix retrieval of accurate refresh duration across multiple display screens.
diff --git a/Docs/ b/Docs/
index 155e1fd..ad840f5 100644
--- a/Docs/
+++ b/Docs/
@@ -143,7 +143,7 @@
           - `MoltenVK/dylib/macOS` *(macOS)*
           - `MoltenVK/dylib/iOS` *(iOS)*
           - `MoltenVK/dylib/tvOS` *(tvOS)*
     3. In the **Runpath Search Paths** (aka `LD_RUNPATH_SEARCH_PATHS`) setting, 
        add an entry that matches where the dynamic library will be located in your runtime
        environment. If the dynamic library is to be embedded within your application, 
@@ -188,11 +188,11 @@
    - To copy the `libMoltenVK.dylib` file into your application or component library:
 	   1. On the *Build Phases* tab, add a new *Copy Files* build phase.
 	   2. Set the *Destination* into which you want to place  the `libMoltenVK.dylib` file.
 	      Typically this will be *Frameworks* (and it should match the **Runpath Search Paths** 
 	      (aka `LD_RUNPATH_SEARCH_PATHS`) build setting you added above).
 	   3. Drag **_one_** of the following files to the *Copy Files* list in this new build phase:
 	     - `MoltenVK/dylib/macOS/libMoltenVK.dylib` *(macOS)* 
 	     - `MoltenVK/dylib/iOS/libMoltenVK.dylib` *(iOS)* 
@@ -229,7 +229,7 @@
 When a *Metal* app is running from *Xcode*, the default ***Scheme*** settings may reduce performance. 
 To improve performance and gain the benefits of *Metal*, perform the following in *Xcode*:
 1. Open the ***Scheme Editor*** for building your main application. You can do 
    this by selecting ***Edit Scheme...*** from the ***Scheme*** drop-down menu, or select 
    ***Product -> Scheme -> Edit Scheme...*** from the main menu.
@@ -337,8 +337,25 @@
 In order to visibly display your content on *macOS*, *iOS*, or *tvOS*, you must enable the
 `VK_EXT_metal_surface` extension, and use the function defined in that extension to create a 
-*Vulkan* rendering surface. You can enable the `VK_EXT_metal_surface` extension by defining the `VK_USE_PLATFORM_METAL_EXT` guard macro in your compiler build settings. See the description of 
-the `mvk_vulkan.h` file below for  a convenient way to enable this extension automatically.
+*Vulkan* rendering surface. You can enable the `VK_EXT_metal_surface` extension by defining 
+the `VK_USE_PLATFORM_METAL_EXT` guard macro in your compiler build settings. See the description 
+of the `mvk_vulkan.h` file below for  a convenient way to enable this extension automatically.
+When creating a `CAMetalLayer` to underpin the *Vulkan* surface to render to, it is strongly 
+recommended that you ensure the `delegate` of the `CAMetalLayer` is the `NSView/UIView` in 
+which the layer is contained, to ensure correct and optimized *Vulkan* swapchain and refresh 
+timing behavior across multiple display screens that might have different properties.
+The view will automatically be the `delegate` of the layer when the view creates the 
+`CAMetalLayer`, as per Apple's documentation:
+>If the layer object was created by a view, the view typically assigns itself as the layer’s 
+delegate automatically, and you should not change that relationship. For layers you create 
+yourself, you can assign a delegate object and use that object to provide the contents of 
+the layer dynamically and perform other tasks.
+But in the case where you create the `CAMetalLayer` yourself and assign it to the view, 
+you should also assign the view as the `delegate` of the layer. 
 Because **MoltenVK** supports the `VK_KHR_portability_subset` extension, when using the 
 *Vulkan Loader* from the *Vulkan SDK* to run **MoltenVK** on *macOS*, the *Vulkan Loader* 
diff --git a/Docs/ b/Docs/
index 1a6bef6..4f82bfb 100644
--- a/Docs/
+++ b/Docs/
@@ -27,10 +27,11 @@
 - Work around MTLCounterSet crash on additional Intel Iris Plus Graphics drivers.
 - Check `MTLDevice` to enable support for `VK_KHR_fragment_shader_barycentric` 
   and `VK_NV_fragment_shader_barycentric` extensions.
+- Ignore sampler update in descriptor set bindings that use immutable samplers.
 - Fix query pool wait block when query is not encoded to be written to.
 - Fix `vkUpdateDescriptorSetWithTemplate()` for inline block descriptors.
-- Ignore sampler update in descriptor set bindings that use immutable samplers.
-- Update _macOS Cube_ demo to demonstrate optimizing swapchain across multiple screens.
+- Fix retrieval of accurate `vkGetRefreshCycleDurationGOOGLE()` across multiple display screens.
+- Update _macOS Cube_ demo to demonstrate optimizing the swapchain across multiple display screens.
 - Update `VK_MVK_MOLTENVK_SPEC_VERSION` to version `35`.
diff --git a/MoltenVK/MoltenVK/GPUObjects/ b/MoltenVK/MoltenVK/GPUObjects/
index 137eaa1..d614790 100644
--- a/MoltenVK/MoltenVK/GPUObjects/
+++ b/MoltenVK/MoltenVK/GPUObjects/
@@ -26,20 +26,8 @@
 #include "MVKWatermarkTextureContent.h"
 #include "MVKWatermarkShaderSource.h"
 #include "mvk_datatypes.hpp"
-#import "CAMetalLayer+MoltenVK.h"
 #import "MVKBlockObserver.h"
-#	include <UIKit/UIScreen.h>
-#	include <AppKit/NSApplication.h>
-#	include <AppKit/NSScreen.h>
-#	include <AppKit/NSWindow.h>
-#	include <AppKit/NSView.h>
 #include <libkern/OSByteOrder.h>
 using namespace std;
@@ -409,8 +397,13 @@
 		_presentableImages.push_back(_device->createPresentableSwapchainImage(&imgInfo, this, imgIdx, NULL));
-    MVKLogInfo("Created %d swapchain images with initial size (%d, %d) and contents scale %.1f.",
-			   imgCnt, imgExtent.width, imgExtent.height, _mtlLayer.contentsScale);
+	NSString* screenName = _mtlLayer.screenMVK.localizedName;
+	NSString* screenName = @"Main Screen";
+    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);
 VkResult MVKSwapchain::getRefreshCycleDuration(VkRefreshCycleDurationGOOGLE *pRefreshCycleDuration) {
@@ -418,33 +411,17 @@
 	NSInteger framesPerSecond = 60;
-	UIScreen* screen = [UIScreen mainScreen];
+	UIScreen* screen = _mtlLayer.screenMVK;
 	if ([screen respondsToSelector: @selector(maximumFramesPerSecond)]) {
 		framesPerSecond = screen.maximumFramesPerSecond;
-	// Find the screen for the window whose content view is backed by the swapchains CAMetalLayer.
-	// Default to the mainScreen if no such window can be found.
-	NSScreen* screen = [NSScreen mainScreen];
-	CALayer* layer = _mtlLayer;
-	while (layer.superlayer) {
-		layer = layer.superlayer;
-	}
-	for (NSWindow* window in [[NSApplication sharedApplication] windows]) {
-		NSView *view = [window contentView];
-		if (view && ([view layer] == layer)) { // Check against layer and not _mtlLayer.
-			screen = [window screen];
-		}
-	}
+	NSScreen* screen = _mtlLayer.screenMVK;
 	CGDirectDisplayID displayId = [[[screen deviceDescription] objectForKey:@"NSScreenNumber"] unsignedIntValue];
 	CGDisplayModeRef mode = CGDisplayCopyDisplayMode(displayId);
 	double framesPerSecond = CGDisplayModeGetRefreshRate(mode);
 #if MVK_XCODE_13
 	if (framesPerSecond == 0 && [screen respondsToSelector: @selector(maximumFramesPerSecond)])
      	framesPerSecond = [screen maximumFramesPerSecond];
diff --git a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h
index afe487f..0cf039b 100644
--- a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h
+++ b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.h
@@ -18,8 +18,20 @@
 #pragma once
+#include "MVKCommonEnvironment.h"
 #import <QuartzCore/QuartzCore.h>
+#	include <UIKit/UIScreen.h>
+#	include <AppKit/NSScreen.h>
 /** Extensions to CAMetalLayer to support MoltenVK. */
 @interface CAMetalLayer (MoltenVK)
@@ -73,4 +85,7 @@
 @property(nonatomic, readwrite) CFStringRef colorspaceNameMVK;
+/** Returns the screen on which this layer is rendering. */
+@property(nonatomic, readonly) PLATFORM_SCREEN_CLASS* screenMVK;
diff --git a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.m b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.m
index 2da3118..0214e8b 100644
--- a/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.m
+++ b/MoltenVK/MoltenVK/OS/CAMetalLayer+MoltenVK.m
@@ -18,7 +18,13 @@
 #include "CAMetalLayer+MoltenVK.h"
-#include "MVKCommonEnvironment.h"
+#	include <AppKit/NSApplication.h>
+#	include <AppKit/NSWindow.h>
+#	include <AppKit/NSView.h>
 @implementation CAMetalLayer (MoltenVK)
@@ -84,4 +90,33 @@
+-(UIScreen*) screenMVK {
+	return UIScreen.mainScreen;
+-(NSScreen*) screenMVK {
+	// If this layer has a delegate that is an NSView, and the view is in a window, retrieve the screen from the window.
+	if ([self.delegate isKindOfClass: NSView.class]) {
+		NSWindow* window = ((NSView*)self.delegate).window;
+		if (window) { return window.screen; }
+	} else {
+		// Otherwise we need to iterate through all the windows used by this app, and
+		// check if the content view is using this layer or one of its ancestor layers.
+		// If a match is found, retrieve the screen from the window. It is not sufficient
+		// to first search for the top structural layer, because Core Animation may add
+		// a superlayer to the CAMetalLayer, independent of the content view.
+		for (NSWindow* window in {
+			CALayer* windowContentLayer = window.contentView.layer;
+			for (CALayer* layer = self; layer; layer = layer.superlayer) {
+				if (layer == windowContentLayer) { return window.screen; }
+			}
+		}
+	}
+	return NSScreen.mainScreen;		// Default to main screen if not found