// This file is part of VSTGUI. It is subject to the license terms // in the LICENSE file found in the top-level directory of this // distribution and at http://github.com/steinbergmedia/vstgui/LICENSE #pragma once #import "externalview_nsview.h" #import #import #import #import //------------------------------------------------------------------------ using VSTGUIMetalLayerDelegateDrawCallback = std::function; using VSTGUIMetalViewScreenChangedCallack = std::function; @interface NSObject () - (void)setDrawCallback:(const VSTGUIMetalLayerDelegateDrawCallback&)callback; - (void)setScreenChangedCallback:(const VSTGUIMetalViewScreenChangedCallack&)callback; @end //------------------------------------------------------------------------ namespace VSTGUI { namespace ExternalView { //------------------------------------------------------------------------ struct IMetalView { virtual ~IMetalView () noexcept = default; virtual void render () = 0; }; //------------------------------------------------------------------------ /** metal render interface to be used as the renderer of the MetalView * * The renderer has to set the metal device of the metal layer before it can draw to it. */ struct IMetalRenderer { virtual ~IMetalRenderer () noexcept = default; virtual bool init (IMetalView* metalView, CAMetalLayer* metalLayer) = 0; virtual void draw (id drawable) = 0; virtual void onSizeUpdate (int32_t width, int32_t height, double scaleFactor) = 0; virtual void onAttached () = 0; virtual void onRemoved () = 0; virtual void onScreenChanged (NSScreen* screen) = 0; }; using MetalRendererPtr = std::shared_ptr; //------------------------------------------------------------------------ struct MetalLayerDelegate : RuntimeObjCClass { static constexpr auto CallbackVarName = "callback"; static Class CreateClass () { return ObjCClassBuilder () .init ("MetalLayerDelegate", [NSObject class]) .addMethod (@selector (displayLayer:), displayLayer) .addMethod (@selector (actionForLayer:forKey:), actionForLayer) .addMethod (@selector (setDrawCallback:), setCallback) .addProtocol ("CALayerDelegate") .addIvar (CallbackVarName) .finalize (); } static void setCallback (id self, SEL cmd, VSTGUIMetalLayerDelegateDrawCallback callback) { auto instance = makeInstance (self); if (auto var = instance.getVariable (CallbackVarName)) var->set (callback); } static void displayLayer (id self, SEL cmd, CALayer* layer) { auto instance = makeInstance (self); if (auto var = instance.getVariable (CallbackVarName)) { if (auto callback = var->get ()) callback (); } } static id actionForLayer (CALayer* layer, NSString* key) { return [NSNull null]; } }; //------------------------------------------------------------------------ struct MetalNSView : RuntimeObjCClass { static constexpr auto CallbackVarName = "callback"; static Class CreateClass () { return ObjCClassBuilder () .init ("MetalNSView", [NSView class]) .addIvar (CallbackVarName) .addMethod (@selector (viewDidMoveToWindow), viewDidMoveToWindow) .addMethod (@selector (viewWillMoveToWindow:), viewWillMoveToWindow) .addMethod (@selector (windowDidChangeScreen:), windowDidChangeScreen) .addMethod (@selector (setScreenChangedCallback:), setCallback) .finalize (); } static void setCallback (id self, SEL cmd, VSTGUIMetalViewScreenChangedCallack callback) { auto instance = makeInstance (self); if (auto var = instance.getVariable (CallbackVarName)) var->set (callback); } static void viewDidMoveToWindow (id self, SEL cmd) { windowDidChangeScreen (self, cmd, nullptr); makeInstance (self).callSuper (cmd); } static void viewWillMoveToWindow (id self, SEL cmd, NSWindow* window) { if (auto prevWindow = [self window]) { [NSNotificationCenter.defaultCenter removeObserver:self]; } if (window) { [NSNotificationCenter.defaultCenter addObserver:self selector:@selector (windowDidChangeScreen:) name:NSWindowDidChangeScreenNotification object:window]; } makeInstance (self).callSuper (cmd, window); } static void windowDidChangeScreen (id self, SEL cmd, NSNotification* n) { if (NSScreen* screen = [[self window] screen]) { auto instance = makeInstance (self); if (auto var = instance.getVariable (CallbackVarName)) { if (auto callback = var->get ()) callback (screen); } } } }; //------------------------------------------------------------------------ struct MetalView : ExternalNSViewBase, IMetalView { /** make a new metal view. * * The metal view can render on a background thread (only use one thread for rendering) or on * the main thread. * Rendering and view resizing is automatically guarded by a mutex. * The view will automatically trigger a rendering when the view is resized. */ static std::shared_ptr make (const MetalRendererPtr& renderer) { if (!renderer) return {}; if (auto metalView = std::shared_ptr (new MetalView (renderer))) { if (renderer->init (metalView.get (), metalView->metalLayer)) return metalView; } return {}; } /** immediately render the view [thread safe] */ void render () override { doLocked ([&] () { renderer->draw (metalLayer.nextDrawable); }); } /** do something locked [thread safe] */ template void doLocked (Proc proc) { LockGuard g (mutex); @autoreleasepool { proc (); } } private: CAMetalLayer* metalLayer {nullptr}; id metalLayerDelegate {nullptr}; double contentScaleFactor {1.}; using Mutex = std::recursive_mutex; using LockGuard = std::lock_guard; Mutex mutex; MetalRendererPtr renderer; MetalView (const MetalRendererPtr& renderer) : Base ([MetalNSView::alloc () init]), renderer (renderer) { metalLayerDelegate = [MetalLayerDelegate::alloc () init]; metalLayer = [CAMetalLayer new]; metalLayer.delegate = metalLayerDelegate; view.layer = metalLayer; metalLayer.needsDisplayOnBoundsChange = YES; metalLayer.geometryFlipped = YES; metalLayer.opaque = NO; metalLayer.contentsGravity = kCAGravityBottomLeft; [metalLayerDelegate setDrawCallback:[this] () { render (); }]; [view setScreenChangedCallback:[this] (NSScreen* screen) { this->renderer->onScreenChanged (screen); }]; } bool attach (void* parent, PlatformViewType parentViewType) override { if (Base::attach (parent, parentViewType)) { renderer->onAttached (); return true; } return false; } bool remove () override { if (Base::remove ()) { renderer->onRemoved (); return true; } return false; } void setContentScaleFactor (double scaleFactor) override { contentScaleFactor = scaleFactor; metalLayer.contentsScale = scaleFactor; [metalLayer setNeedsDisplay]; onSizeUpdate (); } void setViewSize (IntRect frame, IntRect visible) override { Base::setViewSize (frame, visible); onSizeUpdate (); } void onSizeUpdate () { doLocked ([this] () { auto size = view.frame.size; metalLayer.drawableSize = NSMakeSize (size.width * contentScaleFactor, size.height * contentScaleFactor); renderer->onSizeUpdate (size.width, size.height, contentScaleFactor); }); } #if !__has_feature(objc_arc) public: ~MetalView () noexcept override { [metalLayerDelegate release]; [metalLayer release]; } #endif }; //------------------------------------------------------------------------ } // ExternalView } // VSTGUI