From cb343c329a7580954eebf2b7c68bc7f6e10a56ba Mon Sep 17 00:00:00 2001 From: Stephen Saunders Date: Wed, 1 Jun 2022 12:46:41 -0400 Subject: [PATCH] Fixes for vulkanExample: frame timing now equals diff between frames for Win & macOS portability, support vsync off rendering on macOS, support swapchain image count change on resize, handle macOS fullscreen; Fixes for xcode example: use PanGestureRecognizer on iOS, add macOS cursor tracking, cleanup Vulkan on shutdown --- base/VulkanSwapChain.cpp | 9 ++- base/VulkanSwapChain.h | 2 +- base/vulkanexamplebase.cpp | 100 +++++++++++++++++++------ base/vulkanexamplebase.h | 7 +- xcode/MVKExample.cpp | 11 +-- xcode/MVKExample.h | 5 +- xcode/examples.h | 6 +- xcode/ios/AppDelegate.h | 1 + xcode/ios/AppDelegate.m | 2 + xcode/ios/DemoViewController.h | 1 + xcode/ios/DemoViewController.mm | 117 ++++++++++++++++++++---------- xcode/macos/AppDelegate.h | 2 + xcode/macos/AppDelegate.m | 2 + xcode/macos/DemoViewController.h | 1 + xcode/macos/DemoViewController.mm | 43 ++++++++--- 15 files changed, 222 insertions(+), 87 deletions(-) diff --git a/base/VulkanSwapChain.cpp b/base/VulkanSwapChain.cpp index 432fee10..9d2f92b5 100644 --- a/base/VulkanSwapChain.cpp +++ b/base/VulkanSwapChain.cpp @@ -229,7 +229,7 @@ void VulkanSwapChain::connect(VkInstance instance, VkPhysicalDevice physicalDevi * @param height Pointer to the height of the swapchain (may be adjusted to fit the requirements of the swapchain) * @param vsync (Optional) Can be used to force vsync-ed rendering (by using VK_PRESENT_MODE_FIFO_KHR as presentation mode) */ -void VulkanSwapChain::create(uint32_t *width, uint32_t *height, bool vsync) +void VulkanSwapChain::create(uint32_t *width, uint32_t *height, bool vsync, bool fullscreen) { // Store the current swap chain handle so we can use it later on to ease up recreation VkSwapchainKHR oldSwapchain = swapChain; @@ -290,6 +290,13 @@ void VulkanSwapChain::create(uint32_t *width, uint32_t *height, bool vsync) // Determine the number of images uint32_t desiredNumberOfSwapchainImages = surfCaps.minImageCount + 1; +#if (defined(VK_USE_PLATFORM_MACOS_MVK) && defined(VK_EXAMPLE_XCODE_GENERATED)) + // SRS - Work around known MoltenVK issue re 2x frame rate when vsync (VK_PRESENT_MODE_FIFO_KHR) enabled in windowed mode + if (vsync && !fullscreen) + { + desiredNumberOfSwapchainImages = surfCaps.minImageCount; + } +#endif if ((surfCaps.maxImageCount > 0) && (desiredNumberOfSwapchainImages > surfCaps.maxImageCount)) { desiredNumberOfSwapchainImages = surfCaps.maxImageCount; diff --git a/base/VulkanSwapChain.h b/base/VulkanSwapChain.h index 686bf498..29eaeb1b 100644 --- a/base/VulkanSwapChain.h +++ b/base/VulkanSwapChain.h @@ -73,7 +73,7 @@ public: #endif #endif void connect(VkInstance instance, VkPhysicalDevice physicalDevice, VkDevice device); - void create(uint32_t* width, uint32_t* height, bool vsync = false); + void create(uint32_t* width, uint32_t* height, bool vsync = false, bool fullscreen = false); VkResult acquireNextImage(VkSemaphore presentCompleteSemaphore, uint32_t* imageIndex); VkResult queuePresent(VkQueue queue, uint32_t imageIndex, VkSemaphore waitSemaphore = VK_NULL_HANDLE); void cleanup(); diff --git a/base/vulkanexamplebase.cpp b/base/vulkanexamplebase.cpp index bbafb239..b25dc05c 100644 --- a/base/vulkanexamplebase.cpp +++ b/base/vulkanexamplebase.cpp @@ -255,7 +255,7 @@ VkPipelineShaderStageCreateInfo VulkanExampleBase::loadShader(std::string fileNa void VulkanExampleBase::nextFrame() { - auto tStart = std::chrono::high_resolution_clock::now(); + //auto tStart = std::chrono::high_resolution_clock::now(); if (viewUpdated) { viewUpdated = false; @@ -265,12 +265,10 @@ void VulkanExampleBase::nextFrame() render(); frameCounter++; auto tEnd = std::chrono::high_resolution_clock::now(); - auto tDiff = std::chrono::duration(tEnd - tStart).count(); -#if (defined(VK_USE_PLATFORM_IOS_MVK) || defined(VK_USE_PLATFORM_MACOS_MVK)) - frameTimer = (float)tDiff * refreshPeriod; // SRS - Multiply by refresh period due to displayLink callback rendering on iOS and macOS -#else + // SRS - Calculate tDiff as time between frames vs. rendering time for Win32/macOS/iOS vsync portability + //auto tDiff = std::chrono::duration(tEnd - tStart).count(); + auto tDiff = std::chrono::duration(tEnd - tPrevEnd).count(); frameTimer = (float)tDiff / 1000.0f; -#endif camera.update(frameTimer); if (camera.moving()) { @@ -298,6 +296,8 @@ void VulkanExampleBase::nextFrame() frameCounter = 0; lastTimestamp = tEnd; } + tPrevEnd = tEnd; + // TODO: Cap UI overlay update rates updateOverlay(); } @@ -316,6 +316,7 @@ void VulkanExampleBase::renderLoop() destWidth = width; destHeight = height; lastTimestamp = std::chrono::high_resolution_clock::now(); + tPrevEnd = lastTimestamp; #if defined(_WIN32) MSG msg; bool quitMessageReceived = false; @@ -733,9 +734,12 @@ void VulkanExampleBase::prepareFrame() { // Acquire the next image from the swap chain VkResult result = swapChain.acquireNextImage(semaphores.presentComplete, ¤tBuffer); - // Recreate the swapchain if it's no longer compatible with the surface (OUT_OF_DATE) or no longer optimal for presentation (SUBOPTIMAL) + // Recreate the swapchain if it's no longer compatible with the surface (OUT_OF_DATE) + // SRS - If no longer optimal (VK_SUBOPTIMAL_KHR), wait until submitFrame() in case number of swapchain images will change on resize if ((result == VK_ERROR_OUT_OF_DATE_KHR) || (result == VK_SUBOPTIMAL_KHR)) { - windowResize(); + if (result == VK_ERROR_OUT_OF_DATE_KHR) { + windowResize(); + } } else { VK_CHECK_RESULT(result); @@ -745,15 +749,16 @@ void VulkanExampleBase::prepareFrame() void VulkanExampleBase::submitFrame() { VkResult result = swapChain.queuePresent(queue, currentBuffer, semaphores.renderComplete); - if (!((result == VK_SUCCESS) || (result == VK_SUBOPTIMAL_KHR))) { + // Recreate the swapchain if it's no longer compatible with the surface (OUT_OF_DATE) or no longer optimal for presentation (SUBOPTIMAL) + if ((result == VK_ERROR_OUT_OF_DATE_KHR) || (result == VK_SUBOPTIMAL_KHR)) { + windowResize(); if (result == VK_ERROR_OUT_OF_DATE_KHR) { - // Swap chain is no longer compatible with the surface and needs to be recreated - windowResize(); return; - } else { - VK_CHECK_RESULT(result); } } + else { + VK_CHECK_RESULT(result); + } VK_CHECK_RESULT(vkQueueWaitIdle(queue)); } @@ -1524,6 +1529,8 @@ void VulkanExampleBase::handleAppCommand(android_app * app, int32_t cmd) #if defined(VK_EXAMPLE_XCODE_GENERATED) @interface AppDelegate : NSObject { +@public + VulkanExampleBase *vulkanExample; } @end @@ -1532,11 +1539,35 @@ void VulkanExampleBase::handleAppCommand(android_app * app, int32_t cmd) { } +// SRS - Dispatch rendering loop onto a queue for max frame rate concurrent rendering vs displayLink vsync rendering +// - vsync command line option (-vs) on macOS now works like other platforms (using VK_PRESENT_MODE_FIFO_KHR) +dispatch_group_t concurrentGroup; + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification +{ + concurrentGroup = dispatch_group_create(); + dispatch_queue_t concurrentQueue = dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0); + dispatch_group_async(concurrentGroup, concurrentQueue, ^{ + + while (!vulkanExample->quit) { + vulkanExample->displayLinkOutputCb(); + } + }); +} + - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { return YES; } +// SRS - Tell rendering loop to quit, then wait for concurrent queue to terminate before deleting vulkanExample +- (void)applicationWillTerminate:(NSNotification *)aNotification +{ + vulkanExample->quit = YES; + dispatch_group_wait(concurrentGroup, DISPATCH_TIME_FOREVER); + delete vulkanExample; +} + @end const std::string getAssetPath() { @@ -1582,11 +1613,10 @@ static CVReturn displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV - (void)viewDidMoveToWindow { CVDisplayLinkCreateWithActiveCGDisplays(&displayLink); - CVDisplayLinkSetOutputCallback(displayLink, &displayLinkOutputCallback, vulkanExample); + // SRS - Disable displayLink vsync rendering in favour of max frame rate concurrent rendering + // - vsync command line option (-vs) on macOS now works like other platforms (using VK_PRESENT_MODE_FIFO_KHR) + //CVDisplayLinkSetOutputCallback(displayLink, &displayLinkOutputCallback, vulkanExample); CVDisplayLinkStart(displayLink); - // SRS - Pause 1 ms for displayLink startup then get the actual refresh period of the display - usleep(1000); - vulkanExample->refreshPeriod = CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink); } - (BOOL)acceptsFirstResponder @@ -1661,13 +1691,13 @@ static CVReturn displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV - (void)mouseUp:(NSEvent *)event { - auto point = [self getMouseLocalPoint:event]; - vulkanExample->mousePos = glm::vec2(point.x, point.y); vulkanExample->mouseButtons.left = false; } - (void)rightMouseDown:(NSEvent *)event { + auto point = [self getMouseLocalPoint:event]; + vulkanExample->mousePos = glm::vec2(point.x, point.y); vulkanExample->mouseButtons.right = true; } @@ -1678,6 +1708,8 @@ static CVReturn displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV - (void)otherMouseDown:(NSEvent *)event { + auto point = [self getMouseLocalPoint:event]; + vulkanExample->mousePos = glm::vec2(point.x, point.y); vulkanExample->mouseButtons.middle = true; } @@ -1717,6 +1749,9 @@ static CVReturn displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV -(float)wheelDelta * 0.05f * vulkanExample->camera.movementSpeed)); } +// SRS - Window resizing already handled by windowResize() in VulkanExampleBase::submitFrame() +// - handling window resize events here is redundant and can cause interaction problems +/* - (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize { CVDisplayLinkStop(displayLink); @@ -1729,6 +1764,17 @@ static CVReturn displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV vulkanExample->windowDidResize(); CVDisplayLinkStart(displayLink); } +*/ + +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + vulkanExample->settings.fullscreen = true; +} + +- (void)windowWillExitFullScreen:(NSNotification *)notification +{ + vulkanExample->settings.fullscreen = false; +} - (BOOL)windowShouldClose:(NSWindow *)sender { @@ -1738,6 +1784,7 @@ static CVReturn displayLinkOutputCallback(CVDisplayLinkRef displayLink, const CV - (void)windowWillClose:(NSNotification *)notification { CVDisplayLinkStop(displayLink); + CVDisplayLinkRelease(displayLink); } @end @@ -1748,7 +1795,9 @@ void* VulkanExampleBase::setupWindow(void* view) #if defined(VK_EXAMPLE_XCODE_GENERATED) NSApp = [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; - [NSApp setDelegate:[AppDelegate new]]; + auto nsAppDelegate = [AppDelegate new]; + nsAppDelegate->vulkanExample = this; + [NSApp setDelegate:nsAppDelegate]; const auto kContentRect = NSMakeRect(0.0f, 0.0f, width, height); const auto kWindowStyle = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable; @@ -1761,6 +1810,9 @@ void* VulkanExampleBase::setupWindow(void* view) [window setAcceptsMouseMovedEvents:YES]; [window center]; [window makeKeyAndOrderFront:nil]; + if (settings.fullscreen) { + [window toggleFullScreen:nil]; + } auto nsView = [[View alloc] initWithFrame:kContentRect]; nsView->vulkanExample = this; @@ -2754,6 +2806,12 @@ void VulkanExampleBase::windowResize() destroyCommandBuffers(); createCommandBuffers(); buildCommandBuffers(); + + // SRS - Recreate fences in case number of swapchain images has changed on resize + for (auto& fence : waitFences) { + vkDestroyFence(device, fence, nullptr); + } + createSynchronizationPrimitives(); vkDeviceWaitIdle(device); @@ -2824,7 +2882,7 @@ void VulkanExampleBase::initSwapchain() void VulkanExampleBase::setupSwapChain() { - swapChain.create(&width, &height, settings.vsync); + swapChain.create(&width, &height, settings.vsync, settings.fullscreen); } void VulkanExampleBase::OnUpdateUIOverlay(vks::UIOverlay *overlay) {} diff --git a/base/vulkanexamplebase.h b/base/vulkanexamplebase.h index 4d5befcd..6d5b28b8 100644 --- a/base/vulkanexamplebase.h +++ b/base/vulkanexamplebase.h @@ -119,7 +119,7 @@ protected: // Frame counter to display fps uint32_t frameCounter = 0; uint32_t lastFPS = 0; - std::chrono::time_point lastTimestamp; + std::chrono::time_point lastTimestamp, tPrevEnd; // Vulkan instance, stores all per-application states VkInstance instance; std::vector supportedInstanceExtensions; @@ -254,7 +254,9 @@ public: int64_t lastTapTime = 0; #elif (defined(VK_USE_PLATFORM_IOS_MVK) || defined(VK_USE_PLATFORM_MACOS_MVK)) void* view; - double refreshPeriod = 1.0/60.0; // SRS - default refreshPeriod for 60 fps display +#if defined(VK_EXAMPLE_XCODE_GENERATED) + bool quit = false; +#endif #elif defined(VK_USE_PLATFORM_DIRECTFB_EXT) bool quit = false; IDirectFB *dfb = nullptr; @@ -534,7 +536,6 @@ int main(const int argc, const char *argv[]) \ vulkanExample->setupWindow(nullptr); \ vulkanExample->prepare(); \ vulkanExample->renderLoop(); \ - delete(vulkanExample); \ } \ return 0; \ } diff --git a/xcode/MVKExample.cpp b/xcode/MVKExample.cpp index 53c73a23..b4f5abb1 100644 --- a/xcode/MVKExample.cpp +++ b/xcode/MVKExample.cpp @@ -17,10 +17,6 @@ void MVKExample::displayLinkOutputCb() { // SRS - expose _vulkanExample->displayLinkOutputCb(); } -void MVKExample::setRefreshPeriod(double refreshPeriod) { // SRS - set VulkanExampleBase::refreshPeriod from DemoViewController displayLink - _vulkanExample->refreshPeriod = refreshPeriod; -} - void MVKExample::keyPressed(uint32_t keyChar) { // SRS - handle keyboard key presses only (e.g. Pause, Space, etc) switch (keyChar) { @@ -113,12 +109,17 @@ void MVKExample::scrollWheel(short wheelDelta) { _vulkanExample->camera.translate(glm::vec3(0.0f, 0.0f, wheelDelta * 0.05f * _vulkanExample->camera.movementSpeed)); } +void MVKExample::fullScreen(bool fullscreen) { + _vulkanExample->settings.fullscreen = fullscreen; +} + MVKExample::MVKExample(void* view) { _vulkanExample = new VulkanExample(); + _vulkanExample->settings.vsync = true; // SRS - this iOS/macOS example uses displayLink vsync rendering - set before calling prepare() _vulkanExample->initVulkan(); _vulkanExample->setupWindow(view); _vulkanExample->prepare(); - _vulkanExample->renderLoop(); // SRS - init VulkanExampleBase::destWidth & destHeight, then fall through and return + _vulkanExample->renderLoop(); // SRS - this inits destWidth/destHeight/lastTimestamp/tPrevEnd, then falls through and returns } MVKExample::~MVKExample() { diff --git a/xcode/MVKExample.h b/xcode/MVKExample.h index 5b647c63..c505a96d 100644 --- a/xcode/MVKExample.h +++ b/xcode/MVKExample.h @@ -15,7 +15,6 @@ class MVKExample { public: void renderFrame(); void displayLinkOutputCb(); // SRS - expose VulkanExampleBase::displayLinkOutputCb() to DemoViewController - void setRefreshPeriod(double refreshPeriod); // SRS - set VulkanExampleBase::refreshPeriod from DemoViewController displayLink void keyPressed(uint32_t keyChar); // SRS - expose keyboard events to DemoViewController void keyDown(uint32_t keyChar); @@ -29,7 +28,9 @@ public: void otherMouseUp(); void mouseDragged(double x, double y); void scrollWheel(short wheelDelta); - + + void fullScreen(bool fullscreen); // SRS - expose VulkanExampleBase::settings.fullscreen to DemoView (macOS only) + MVKExample(void* view); ~MVKExample(); diff --git a/xcode/examples.h b/xcode/examples.h index 4268deca..c6107e4f 100644 --- a/xcode/examples.h +++ b/xcode/examples.h @@ -114,7 +114,7 @@ # include "../examples/particlefire/particlefire.cpp" #endif -// Build issue when using this xcode examples project, builds/runs fine using vulkanExamples project. +// No headless target when using xcode examples project, builds/runs fine using vulkanExamples project. #ifdef MVK_renderheadless # include "../examples/renderheadless/renderheadless.cpp" #endif @@ -191,7 +191,7 @@ # include "../examples/vertexattributes/vertexattributes.cpp" #endif -// Runs but nothing displays. MoltenVK format VK_FORMAT_R32_UINT doesn't contain VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT +// Does not run. MoltenVK/Metal does not support stores and atomic operations in the fragment stage. #ifdef MVK_oit # include "../examples/oit/oit.cpp" #endif @@ -297,7 +297,7 @@ # include "../examples/computecloth/computecloth.cpp" #endif -// Build issue when using this xcode examples project, builds/runs fine using vulkanExamples project. +// No headless target when using xcode examples project, builds/runs fine using vulkanExamples project. #ifdef MVK_computeheadless # include "../examples/computeheadless/computeheadless.cpp" #endif diff --git a/xcode/ios/AppDelegate.h b/xcode/ios/AppDelegate.h index 5023d3dd..02bc20ab 100644 --- a/xcode/ios/AppDelegate.h +++ b/xcode/ios/AppDelegate.h @@ -10,6 +10,7 @@ @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; +@property (strong, nonatomic) UIViewController *viewController; @end diff --git a/xcode/ios/AppDelegate.m b/xcode/ios/AppDelegate.m index 8d69ac3b..430a9591 100644 --- a/xcode/ios/AppDelegate.m +++ b/xcode/ios/AppDelegate.m @@ -6,6 +6,7 @@ */ #import "AppDelegate.h" +#import "DemoViewController.h" @interface AppDelegate () @@ -39,6 +40,7 @@ - (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + [(DemoViewController *)_viewController shutdownExample]; } @end diff --git a/xcode/ios/DemoViewController.h b/xcode/ios/DemoViewController.h index 81d04021..bc908bbb 100644 --- a/xcode/ios/DemoViewController.h +++ b/xcode/ios/DemoViewController.h @@ -13,6 +13,7 @@ /** The main view controller for the demo storyboard. */ @interface DemoViewController : UIViewController +-(void) shutdownExample; @end diff --git a/xcode/ios/DemoViewController.mm b/xcode/ios/DemoViewController.mm index 2fa3d098..3e5e65d7 100644 --- a/xcode/ios/DemoViewController.mm +++ b/xcode/ios/DemoViewController.mm @@ -6,6 +6,7 @@ */ #import "DemoViewController.h" +#import "AppDelegate.h" #include "MVKExample.h" @@ -22,6 +23,7 @@ const std::string getAssetPath() { MVKExample* _mvkExample; CADisplayLink* _displayLink; BOOL _viewHasAppeared; + CGPoint _startPoint; } /** Since this is a single-view app, init Vulkan when the view is loaded. */ @@ -31,26 +33,37 @@ const std::string getAssetPath() { self.view.contentScaleFactor = UIScreen.mainScreen.nativeScale; _mvkExample = new MVKExample(self.view); - + + // SRS - Enable AppDelegate to call into DemoViewController for handling app lifecycle events (e.g. termination) + auto appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; + appDelegate.viewController = self; + uint32_t fps = 60; _displayLink = [CADisplayLink displayLinkWithTarget: self selector: @selector(renderFrame)]; [_displayLink setFrameInterval: 60 / fps]; [_displayLink addToRunLoop: NSRunLoop.currentRunLoop forMode: NSDefaultRunLoopMode]; - // SRS - set VulkanExampleBase::refreshPeriod to calibrate the frame animation rate - _mvkExample->setRefreshPeriod( 1.0 / fps ); - // Setup double tap gesture to toggle virtual keyboard - UITapGestureRecognizer* tapSelector = [[UITapGestureRecognizer alloc] - initWithTarget: self action: @selector(handleTapGesture:)]; + UITapGestureRecognizer* tapSelector = [[[UITapGestureRecognizer alloc] + initWithTarget: self action: @selector(handleTapGesture:)] autorelease]; tapSelector.numberOfTapsRequired = 2; tapSelector.cancelsTouchesInView = YES; + tapSelector.requiresExclusiveTouchType = YES; [self.view addGestureRecognizer: tapSelector]; + // SRS - Setup pan gesture to detect and activate translation + UIPanGestureRecognizer* panSelector = [[[UIPanGestureRecognizer alloc] + initWithTarget: self action: @selector(handlePanGesture:)] autorelease]; + panSelector.minimumNumberOfTouches = 2; + panSelector.cancelsTouchesInView = YES; + panSelector.requiresExclusiveTouchType = YES; + [self.view addGestureRecognizer: panSelector]; + // SRS - Setup pinch gesture to detect and activate zoom - UIPinchGestureRecognizer* pinchSelector = [[UIPinchGestureRecognizer alloc] - initWithTarget: self action: @selector(handlePinchGesture:)]; + UIPinchGestureRecognizer* pinchSelector = [[[UIPinchGestureRecognizer alloc] + initWithTarget: self action: @selector(handlePinchGesture:)] autorelease]; pinchSelector.cancelsTouchesInView = YES; + pinchSelector.requiresExclusiveTouchType = YES; [self.view addGestureRecognizer: pinchSelector]; _viewHasAppeared = NO; @@ -68,9 +81,9 @@ const std::string getAssetPath() { _mvkExample->displayLinkOutputCb(); // SRS - Call displayLinkOutputCb() to animate frames vs. renderFrame() for static image } --(void) dealloc { - delete _mvkExample; - [super dealloc]; +-(void) shutdownExample { + [_displayLink invalidate]; + delete _mvkExample; } // Toggle the display of the virtual keyboard @@ -109,58 +122,84 @@ const std::string getAssetPath() { #pragma mark UITouch methods -// SRS - Handle touch events -(CGPoint) getTouchLocalPoint:(UIEvent*) theEvent { UITouch *touch = [[theEvent allTouches] anyObject]; - CGPoint point = [touch locationInView:self.view]; - point.x = point.x * self.view.contentScaleFactor; - point.y = point.y * self.view.contentScaleFactor; + CGPoint point = [touch locationInView: self.view]; + point.x *= self.view.contentScaleFactor; + point.y *= self.view.contentScaleFactor; return point; } +// SRS - Handle touch events -(void) touchesBegan:(NSSet*) touches withEvent:(UIEvent*) theEvent { - auto point = [self getTouchLocalPoint:theEvent]; if (touches.count == 1) { - // Single touch for imgui select and camera rotation + auto point = [self getTouchLocalPoint: theEvent]; _mvkExample->mouseDown(point.x, point.y); } - else { - // Multi-touch for swipe translation (note: pinch gesture will cancel/override) - _mvkExample->otherMouseDown(point.x, point.y); - } } -(void) touchesMoved:(NSSet*) touches withEvent:(UIEvent*) theEvent { - auto point = [self getTouchLocalPoint:theEvent]; - _mvkExample->mouseDragged(point.x, point.y); + if (touches.count == 1) { + auto point = [self getTouchLocalPoint: theEvent]; + _mvkExample->mouseDragged(point.x, point.y); + } } -(void) touchesEnded:(NSSet*) touches withEvent:(UIEvent*) theEvent { _mvkExample->mouseUp(); - _mvkExample->otherMouseUp(); } -(void) touchesCancelled:(NSSet*) touches withEvent:(UIEvent*) theEvent { _mvkExample->mouseUp(); - _mvkExample->otherMouseUp(); +} + +#pragma mark UIGesture methods + +-(CGPoint) getGestureLocalPoint:(UIGestureRecognizer*) gestureRecognizer { + CGPoint point = [gestureRecognizer locationInView: self.view]; + point.x *= self.view.contentScaleFactor; + point.y *= self.view.contentScaleFactor; + return point; +} + +// SRS - Respond to pan gestures for translation +-(void) handlePanGesture: (UIPanGestureRecognizer*) gestureRecognizer { + switch (gestureRecognizer.state) { + case UIGestureRecognizerStateBegan: { + _startPoint = [self getGestureLocalPoint: gestureRecognizer]; + _mvkExample->otherMouseDown(_startPoint.x, _startPoint.y); + break; + } + case UIGestureRecognizerStateChanged: { + auto translation = [gestureRecognizer translationInView: self.view]; + translation.x *= self.view.contentScaleFactor; + translation.y *= self.view.contentScaleFactor; + _mvkExample->mouseDragged(_startPoint.x + translation.x, _startPoint.y + translation.y); + break; + } + default: { + _mvkExample->otherMouseUp(); + break; + } + } } // SRS - Respond to pinch gestures for zoom -(void) handlePinchGesture: (UIPinchGestureRecognizer*) gestureRecognizer { - if (gestureRecognizer.state == UIGestureRecognizerStateBegan) { - auto point = [gestureRecognizer locationInView:self.view]; - point.x = point.x * self.view.contentScaleFactor; - point.y = point.y * self.view.contentScaleFactor; - _mvkExample->rightMouseDown(point.x, point.y); - } - else if (gestureRecognizer.state == UIGestureRecognizerStateChanged) { - auto point = [gestureRecognizer locationInView:self.view]; - point.x = point.x * self.view.contentScaleFactor; - point.y = point.y * self.view.contentScaleFactor; - _mvkExample->mouseDragged(point.x, point.y - gestureRecognizer.view.frame.size.height / 2.0 * log(gestureRecognizer.scale)); - } - else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) { - _mvkExample->rightMouseUp(); + switch (gestureRecognizer.state) { + case UIGestureRecognizerStateBegan: { + _startPoint = [self getGestureLocalPoint: gestureRecognizer]; + _mvkExample->rightMouseDown(_startPoint.x, _startPoint.y); + break; + } + case UIGestureRecognizerStateChanged: { + _mvkExample->mouseDragged(_startPoint.x, _startPoint.y - self.view.frame.size.height * log(gestureRecognizer.scale)); + break; + } + default: { + _mvkExample->rightMouseUp(); + break; + } } } diff --git a/xcode/macos/AppDelegate.h b/xcode/macos/AppDelegate.h index 3eb638e9..852413ce 100644 --- a/xcode/macos/AppDelegate.h +++ b/xcode/macos/AppDelegate.h @@ -9,4 +9,6 @@ @interface AppDelegate : NSObject +@property (strong, nonatomic) NSViewController *viewController; + @end diff --git a/xcode/macos/AppDelegate.m b/xcode/macos/AppDelegate.m index c3ce3b24..2a97e646 100644 --- a/xcode/macos/AppDelegate.m +++ b/xcode/macos/AppDelegate.m @@ -6,6 +6,7 @@ */ #import "AppDelegate.h" +#import "DemoViewController.h" @interface AppDelegate () @@ -19,6 +20,7 @@ - (void)applicationWillTerminate:(NSNotification *)aNotification { // Insert code here to tear down your application + [(DemoViewController *)_viewController shutdownExample]; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { diff --git a/xcode/macos/DemoViewController.h b/xcode/macos/DemoViewController.h index 3cbedc48..c6337cfb 100644 --- a/xcode/macos/DemoViewController.h +++ b/xcode/macos/DemoViewController.h @@ -13,6 +13,7 @@ /** The main view controller for the demo storyboard. */ @interface DemoViewController : NSViewController +-(void) shutdownExample; @end diff --git a/xcode/macos/DemoViewController.mm b/xcode/macos/DemoViewController.mm index 333e0fe0..53d9e21c 100644 --- a/xcode/macos/DemoViewController.mm +++ b/xcode/macos/DemoViewController.mm @@ -6,6 +6,7 @@ */ #import "DemoViewController.h" +#import "AppDelegate.h" #import #include "MVKExample.h" @@ -43,25 +44,19 @@ MVKExample* _mvkExample; _mvkExample = new MVKExample(self.view); + // SRS - Enable AppDelegate to call into DemoViewController for handling application lifecycle events (e.g. termination) + auto appDelegate = (AppDelegate *)NSApplication.sharedApplication.delegate; + appDelegate.viewController = self; + CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); CVDisplayLinkSetOutputCallback(_displayLink, &DisplayLinkCallback, _mvkExample); CVDisplayLinkStart(_displayLink); } -// SRS - Center the window and set VulkanExampleBase::refreshPeriod from the active displayLink --(void) viewWillAppear { - [super viewWillAppear]; - - NSWindow* window = self.view.window; - [window center]; - - _mvkExample->setRefreshPeriod(CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink)); -} - --(void) dealloc { +-(void) shutdownExample { + CVDisplayLinkStop(_displayLink); CVDisplayLinkRelease(_displayLink); delete _mvkExample; - [super dealloc]; } @end @@ -86,6 +81,15 @@ MVKExample* _mvkExample; return layer; } +// SRS - Activate mouse cursor tracking within the view, set view as window delegate, and center the window +- (void) viewDidMoveToWindow { + auto trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] options: (NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil]; + [self addTrackingArea: trackingArea]; + + [self.window setDelegate: self.window.contentView]; + [self.window center]; +} + -(BOOL) acceptsFirstResponder { return YES; } // SRS - Handle keyboard events @@ -151,9 +155,24 @@ MVKExample* _mvkExample; _mvkExample->mouseDragged(point.x, point.y); } +-(void) mouseMoved:(NSEvent*) theEvent { + auto point = [self getMouseLocalPoint:theEvent]; + _mvkExample->mouseDragged(point.x, point.y); +} + -(void) scrollWheel:(NSEvent*) theEvent { short wheelDelta = [theEvent deltaY]; _mvkExample->scrollWheel(wheelDelta); } +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + _mvkExample->fullScreen(true); +} + +- (void)windowWillExitFullScreen:(NSNotification *)notification +{ + _mvkExample->fullScreen(false); +} + @end