From 54b7f94d180adebba76cc166b6e5f813eb2c455a Mon Sep 17 00:00:00 2001 From: Ariel Abreu Date: Thu, 29 Dec 2022 02:24:00 -0500 Subject: [PATCH] Implement MTKView --- .gitignore | 1 + CMakeLists.txt | 8 +- include/MetalKit/MTKView.h | 79 ++++++++ include/MetalKit/MetalKit.h | 2 +- src/MetalKit/MTKView.m | 386 ++++++++++++++++++++++++++++++++++++ 5 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 include/MetalKit/MTKView.h create mode 100644 src/MetalKit/MTKView.m diff --git a/.gitignore b/.gitignore index 6a97f3d..99537af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vscode tmp +build diff --git a/CMakeLists.txt b/CMakeLists.txt index d333050..e0b3d88 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,7 @@ add_framework(MetalKit VERSION ${FRAMEWORK_VERSION} SOURCES - src/dummy.c + src/MetalKit/MTKView.m DEPENDENCIES AppKit @@ -107,12 +107,18 @@ add_framework(MetalKit objc Foundation Metal + QuartzCore ) target_include_directories(MetalKit PUBLIC include ) +target_include_directories(MetalKit PRIVATE + private-include + ${CMAKE_SOURCE_DIR}/src/external/foundation/internal_include +) + # # MetalPerformanceShaders # diff --git a/include/MetalKit/MTKView.h b/include/MetalKit/MTKView.h new file mode 100644 index 0000000..579592d --- /dev/null +++ b/include/MetalKit/MTKView.h @@ -0,0 +1,79 @@ +/* + * This file is part of Darling. + * + * Copyright (C) 2022 Darling developers + * + * Darling is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Darling is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Darling. If not, see . + */ + +#ifndef _METALKIT_MTKVIEW_H_ +#define _METALKIT_MTKVIEW_H_ + +#import +#import +#import + +@class MTKView; + +@protocol MTKViewDelegate + +- (void) mtkView: (MTKView*)view + drawableSizeWillChange: (CGSize)size; + +- (void)drawInMTKView: (MTKView*)view; + +@end + +MTL_EXPORT +@interface MTKView : NSView + +#if __OBJC2__ +@property(nonatomic, weak, nullable) id delegate; +#else +@property(nonatomic, assign, nullable) id delegate; +#endif + +@property(nonatomic, retain, nullable) id device; +@property(readonly) id preferredDevice; +@property(nonatomic) MTLPixelFormat colorPixelFormat; +@property(nonatomic) CGColorSpaceRef colorspace; +@property(nonatomic) BOOL framebufferOnly; +@property(nonatomic) CGSize drawableSize; +@property(nonatomic, readonly) CGSize preferredDrawableSize; +@property(nonatomic) BOOL autoResizeDrawable; +@property(nonatomic) MTLClearColor clearColor; +@property(nonatomic) MTLPixelFormat depthStencilPixelFormat; +@property(nonatomic) MTLTextureUsage depthStencilAttachmentTextureUsage; +@property(nonatomic) double clearDepth; +@property(nonatomic) uint32_t clearStencil; +@property(nonatomic) NSUInteger sampleCount; +@property(nonatomic) MTLTextureUsage multisampleColorAttachmentTextureUsage; +@property(nonatomic, readonly, nullable) MTLRenderPassDescriptor* currentRenderPassDescriptor; +@property(nonatomic, readonly, nullable) id currentDrawable; +@property(nonatomic, readonly, nullable) id depthStencilTexture; +@property(nonatomic, readonly, nullable) id multisampleColorTexture; +@property(nonatomic) NSInteger preferredFramesPerSecond; +@property(nonatomic, getter=isPaused) BOOL paused; +@property(nonatomic) BOOL enableSetNeedsDisplay; +@property(nonatomic) BOOL presentsWithTransaction; +@property(nonatomic) MTLStorageMode depthStencilStorageMode; + +- (instancetype)initWithFrame: (NSRect)frame + device: (id)device; +- (void)draw; +- (void)releaseDrawables; + +@end + +#endif // _METALKIT_MTKVIEW_H_ diff --git a/include/MetalKit/MetalKit.h b/include/MetalKit/MetalKit.h index ce2a4c3..fd9814e 100644 --- a/include/MetalKit/MetalKit.h +++ b/include/MetalKit/MetalKit.h @@ -20,6 +20,6 @@ #ifndef _METALKIT_METALKIT_H_ #define _METALKIT_METALKIT_H_ -// TODO +#import #endif // _METALKIT_METALKIT_H_ diff --git a/src/MetalKit/MTKView.m b/src/MetalKit/MTKView.m new file mode 100644 index 0000000..94e50ba --- /dev/null +++ b/src/MetalKit/MTKView.m @@ -0,0 +1,386 @@ +/* + * This file is part of Darling. + * + * Copyright (C) 2022 Darling developers + * + * Darling is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Darling is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Darling. If not, see . + */ + +#import +#import +#import + +@implementation MTKView + +#if __OBJC2__ + +{ + id _delegate; + NSInteger _preferredFramesPerSecond; + BOOL _paused; + BOOL _enableSetNeedsDisplay; + id _currentDrawable; + NSTimer* _frameTimer; +} + +// +// properties +// + +@synthesize autoResizeDrawable = _autoResizeDrawable; +@synthesize clearColor = _clearColor; +@synthesize depthStencilPixelFormat = _depthStencilPixelFormat; +@synthesize depthStencilAttachmentTextureUsage = _depthStencilAttachmentTextureUsage; +@synthesize depthStencilStorageMode = _depthStencilStorageMode; +@synthesize clearDepth = _clearDepth; +@synthesize clearStencil = _clearStencil; +@synthesize sampleCount = _sampleCount; +@synthesize multisampleColorAttachmentTextureUsage = _multisampleColorAttachmentTextureUsage; +@synthesize preferredFramesPerSecond = _preferredFramesPerSecond; +@synthesize paused = _paused; +@synthesize enableSetNeedsDisplay = _enableSetNeedsDisplay; + +- (id)delegate +{ + return objc_loadWeak(&_delegate); +} + +- (void)setDelegate: (id)delegate +{ + objc_storeWeak(&_delegate, delegate); +} + +- (id)device +{ + return ((CAMetalLayer*)_layer).device; +} + +- (void)setDevice: (id)device +{ + ((CAMetalLayer*)_layer).device = device; +} + +- (id)preferredDevice +{ + return ((CAMetalLayer*)_layer).preferredDevice; +} + +- (MTLPixelFormat)colorPixelFormat +{ + return ((CAMetalLayer*)_layer).pixelFormat; +} + +- (void)setColorPixelFormat: (MTLPixelFormat)colorPixelFormat +{ + ((CAMetalLayer*)_layer).pixelFormat = colorPixelFormat; +} + +- (CGColorSpaceRef)colorspace +{ + return ((CAMetalLayer*)_layer).colorspace; +} + +- (void)setColorspace: (CGColorSpaceRef)colorspace +{ + ((CAMetalLayer*)_layer).colorspace = colorspace; +} + +- (BOOL)framebufferOnly +{ + return ((CAMetalLayer*)_layer).framebufferOnly; +} + +- (void)setFramebufferOnly: (BOOL)framebufferOnly +{ + ((CAMetalLayer*)_layer).framebufferOnly = framebufferOnly; +} + +- (CGSize)drawableSize +{ + return ((CAMetalLayer*)_layer).drawableSize; +} + +- (void)setDrawableSize: (CGSize)drawableSize +{ + ((CAMetalLayer*)_layer).drawableSize = drawableSize; + [_delegate mtkView: self drawableSizeWillChange: drawableSize]; +} + +- (CGSize)preferredDrawableSize +{ + // TODO: multiply by contentsScale + return self.bounds.size; +} + +- (MTLRenderPassDescriptor*)currentRenderPassDescriptor +{ + id device = self.device; + + if (!device) { + return nil; + } + + id drawable = self.currentDrawable; + + if (!drawable) { + return nil; + } + + MTLRenderPassDescriptor* desc = [MTLRenderPassDescriptor renderPassDescriptor]; + + desc.colorAttachments[0].texture = drawable.texture; + desc.colorAttachments[0].clearColor = _clearColor; + desc.colorAttachments[0].loadAction = MTLLoadActionClear; + desc.colorAttachments[0].storeAction = MTLStoreActionStore; + + return desc; +} + +- (id)currentDrawable +{ + if (!_currentDrawable) { + _currentDrawable = [(CAMetalLayer*)_layer nextDrawable]; + } + return _currentDrawable; +} + +- (id)depthStencilTexture +{ + // TODO + return nil; +} + +- (id)multisampleColorTexture +{ + // TODO + return nil; +} + +// use the synthesized `preferredFramesPerSecond` getter + +- (void)setPreferredFramesPerSecond: (NSInteger)preferredFramesPerSecond +{ + if (preferredFramesPerSecond > 60) { + preferredFramesPerSecond = 60; + } + + if (preferredFramesPerSecond < 1) { + preferredFramesPerSecond = 1; + } + + if (preferredFramesPerSecond == _preferredFramesPerSecond) { + return; + } + + _preferredFramesPerSecond = preferredFramesPerSecond; + + [self _resetTimer]; +} + +// use the synthesized `paused` getter + +- (void)setPaused: (BOOL)paused +{ + if (paused == _paused) { + return; + } + + _paused = paused; + + [self _resetTimer]; +} + +// use the synthesized `enableSetNeedsDisplay` getter + +- (void)setEnableSetNeedsDisplay: (BOOL)enableSetNeedsDisplay +{ + if (enableSetNeedsDisplay == _enableSetNeedsDisplay) { + return; + } + + _enableSetNeedsDisplay = enableSetNeedsDisplay; + + [self _resetTimer]; +} + +- (BOOL)presentsWithTransaction +{ + return ((CAMetalLayer*)_layer).presentsWithTransaction; +} + +- (void)setPresentsWithTransaction: (BOOL)presentsWithTransaction +{ + ((CAMetalLayer*)_layer).presentsWithTransaction = presentsWithTransaction; +} + +// +// methods +// + +- (void)_initCommon +{ + _autoResizeDrawable = YES; + _clearColor = MTLClearColorMake(0, 0, 0, 1); + _depthStencilPixelFormat = MTLPixelFormatInvalid; + _depthStencilAttachmentTextureUsage = MTLTextureUsageRenderTarget; + _depthStencilStorageMode = MTLStorageModeShared; // TODO: check what the actual default is + _clearDepth = 1; + _clearStencil = 0; + _sampleCount = 1; + _multisampleColorAttachmentTextureUsage = MTLTextureUsageRenderTarget; + _preferredFramesPerSecond = 60; + _paused = NO; + _enableSetNeedsDisplay = NO; + + [self _resetTimer]; +} + +- (instancetype)initWithCoder: (NSCoder*)decoder +{ + self.wantsLayer = YES; + // TODO: check if MTKView has any coding stuff of its own + self = [super initWithCoder: decoder]; + if (self != nil) { + [self _initCommon]; + } + return self; +} + +- (instancetype)initWithFrame: (NSRect)frame +{ + self.wantsLayer = YES; + self = [super initWithFrame: frame]; + if (self != nil) { + [self _initCommon]; + } + return self; +} + +- (instancetype)initWithFrame: (NSRect)frame + device: (id)device +{ + self.wantsLayer = YES; + self = [super initWithFrame: frame]; + if (self != nil) { + self.device = device; + [self _initCommon]; + } + return self; +} + +- (void)dealloc +{ + objc_storeWeak(&_delegate, nil); + [_currentDrawable release]; + [_frameTimer invalidate]; + [_frameTimer release]; + [super dealloc]; +} + +- (CALayer*)makeBackingLayer +{ + return [CAMetalLayer layer]; +} + +- (void)draw +{ + [_currentDrawable release]; + _currentDrawable = [(CAMetalLayer*)_layer nextDrawable]; + + if ([self methodForSelector: @selector(drawRect:)] != [MTKView instanceMethodForSelector: @selector(drawRect:)]) { + // a subclass has overridden this method; invoke it. + [self drawRect: self.bounds]; + } else { + // `drawRect:` has not been overridden; invoke the delegate's method. + [_delegate drawInMTKView: self]; + } + + [_currentDrawable release]; + _currentDrawable = nil; +} + +- (void)releaseDrawables +{ + // TODO + // we don't need this yet since we don't have depth, stencil, or multisample textures +} + +- (void)_resetTimer +{ + [_frameTimer invalidate]; + [_frameTimer release]; + + if (_paused) { + return; + } + + _NSWeakRef* weakSelf = [[[_NSWeakRef alloc] initWithObject: self] autorelease]; + + _frameTimer = [[NSTimer scheduledTimerWithTimeInterval: 1.0 / _preferredFramesPerSecond + repeats: YES + block: ^(NSTimer* timer) { + MTKView* me = weakSelf.object; + [me draw]; + }] retain]; +} + +// FIXME: this is a lazy (and probably half-assed) implementation. +// currently, subclassing an MTKView and implementing `drawRect:` +// will result in the method always being invoked after `setNeedsDisplay:`, +// regardless of whether or not `enableSetNeedsDisplay` is set. +// +// we need to check what the official behavior regarding this method +// is with subclasses: we need to check whether `drawRect:` is always +// invoked from `setNeedsDisplay:` or if setting `enableSetNeedsDisplay` +// to NO will prevent the subclass' `drawRect:` from being invoked due +// to `setNeedsDisplay`. +// +// the reason i didn't go ahead and do what sounds like the right behavior +// right now is because that would requiring overridding various display-related +// methods, not just `setNeedsDisplay`. +- (void)drawRect: (NSRect)rect +{ + // if we're not paused, we only redraw when the timer fires. + // if we're not supposed to respond to setNeedsDisplay, we only redraw via manual calls of `draw`. + if (!_paused || !_enableSetNeedsDisplay) { + return; + } + [self draw]; +} + +- (void)setFrame: (NSRect)frame +{ + [super setFrame: frame]; + + if (_autoResizeDrawable) { + self.drawableSize = _frame.size; + } +} + +- (void)setBounds: (NSRect)bounds +{ + [super setBounds: bounds]; + + if (_autoResizeDrawable) { + self.drawableSize = _bounds.size; + } +} + +#else + +MTL_UNSUPPORTED_CLASS + +#endif + +@end