CSMetalRenderer.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. //
  2. // Copyright © 2022 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. @import simd;
  17. @import MetalKit;
  18. #import "CSMetalRenderer.h"
  19. #import "CSRenderSource.h"
  20. #import "CSRenderer.h"
  21. // Header shared between C code here, which executes Metal API commands, and .metal files, which
  22. // uses these types as inputs to the shaders
  23. #import "CSShaderTypes.h"
  24. NS_ASSUME_NONNULL_BEGIN
  25. /// Helper class to retain fields from a renderer source
  26. @interface _CSRendererSourceData : NSObject<CSRenderSource>
  27. @property (nonatomic, readonly) CGPoint viewportOrigin;
  28. @property (nonatomic, readonly) CGFloat viewportScale;
  29. @property (nonatomic, readonly) id<MTLBuffer> vertices;
  30. @property (nonatomic, readonly) NSUInteger numVertices;
  31. @property (nonatomic, readonly) id<MTLTexture> texture;
  32. @property (nonatomic, readonly) BOOL hasAlpha;
  33. @property (nonatomic, readonly) BOOL isInverted;
  34. @property (nonatomic, readonly) BOOL isVisible;
  35. @property (nonatomic, readonly) id<CSRenderSource> cursorSource;
  36. - (instancetype)init NS_UNAVAILABLE;
  37. - (nullable instancetype)initWithRenderSource:(id<CSRenderSource>)renderSource;
  38. - (nullable instancetype)initWithRenderSource:(id<CSRenderSource>)renderSource atOffset:(CGPoint)offset NS_DESIGNATED_INITIALIZER;
  39. @end
  40. NS_ASSUME_NONNULL_END
  41. @implementation _CSRendererSourceData
  42. /// Retain a copy of the render source data
  43. /// - Parameter renderSource: Render source to read from
  44. - (nullable instancetype)initWithRenderSource:(id<CSRenderSource>)renderSource {
  45. return [self initWithRenderSource:renderSource atOffset:CGPointZero];
  46. }
  47. /// Retain a copy of the render source data
  48. /// - Parameters:
  49. /// - renderSource: Render source to read from
  50. /// - offset: Offset to add to `viewportOrigin`, can be zero
  51. - (nullable instancetype)initWithRenderSource:(id<CSRenderSource>)renderSource atOffset:(CGPoint)offset {
  52. if (self = [super init]) {
  53. _viewportOrigin = CGPointMake(renderSource.viewportOrigin.x +
  54. offset.x,
  55. renderSource.viewportOrigin.y +
  56. offset.y);
  57. _viewportScale = renderSource.viewportScale;
  58. _vertices = renderSource.vertices;
  59. _numVertices = renderSource.numVertices;
  60. _texture = renderSource.texture;
  61. _hasAlpha = renderSource.hasAlpha;
  62. _isInverted = renderSource.isInverted;
  63. _isVisible = renderSource.isVisible;
  64. if (!_vertices || !_texture) {
  65. return nil;
  66. }
  67. if (renderSource.cursorSource) {
  68. _cursorSource = [[_CSRendererSourceData alloc] initWithRenderSource:renderSource.cursorSource
  69. atOffset:renderSource.viewportOrigin];
  70. }
  71. }
  72. return self;
  73. }
  74. @end
  75. @interface CSMetalRenderer ()
  76. @property (nonatomic) dispatch_queue_t rendererQueue;
  77. @property (nonatomic, nullable) _CSRendererSourceData *sourceData;
  78. @end
  79. // Main class performing the rendering
  80. @implementation CSMetalRenderer
  81. {
  82. // The device (aka GPU) we're using to render
  83. id<MTLDevice> _device;
  84. // Our render pipeline composed of our vertex and fragment shaders in the .metal shader file
  85. id<MTLRenderPipelineState> _pipelineState;
  86. // The command Queue from which we'll obtain command buffers
  87. id<MTLCommandQueue> _commandQueue;
  88. // The current size of our view so we can use this in our render pipeline
  89. vector_uint2 _viewportSize;
  90. // Sampler object
  91. id<MTLSamplerState> _sampler;
  92. }
  93. @synthesize device = _device;
  94. /// Initialize with the MetalKit view from which we'll obtain our Metal device
  95. - (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView
  96. {
  97. self = [super init];
  98. if(self)
  99. {
  100. NSError *error = NULL;
  101. _device = mtkView.device;
  102. _viewportSize.x = mtkView.drawableSize.width;
  103. _viewportSize.y = mtkView.drawableSize.height;
  104. dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0);
  105. self.rendererQueue = dispatch_queue_create("CocoaSpice Renderer Queue", attr);
  106. /// Create our render pipeline
  107. // Load all the shader files with a .metal file extension in the project
  108. // FIXME: on Swift we have `Bundle.module` generated by SPM but it doesn't appear to be the case for Obj-C
  109. NSURL *modulePath = [NSBundle.mainBundle.resourceURL URLByAppendingPathComponent:@"CocoaSpice_CocoaSpiceRenderer.bundle"];
  110. NSBundle *bundle = [NSBundle bundleWithURL:modulePath];
  111. id<MTLLibrary> defaultLibrary = [_device newDefaultLibraryWithBundle:bundle error:&error];
  112. NSAssert(defaultLibrary, @"Failed to get library from bundle: %@", error);
  113. // Load the vertex function from the library
  114. id<MTLFunction> vertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"];
  115. // Load the fragment function from the library
  116. id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"samplingShader"];
  117. // Set up a descriptor for creating a pipeline state object
  118. MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
  119. pipelineStateDescriptor.label = @"Renderer Pipeline";
  120. pipelineStateDescriptor.vertexFunction = vertexFunction;
  121. pipelineStateDescriptor.fragmentFunction = fragmentFunction;
  122. pipelineStateDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat;
  123. pipelineStateDescriptor.colorAttachments[0].blendingEnabled = YES;
  124. pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
  125. pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
  126. pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
  127. pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
  128. pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne;
  129. pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOne;
  130. pipelineStateDescriptor.depthAttachmentPixelFormat = mtkView.depthStencilPixelFormat;
  131. pipelineStateDescriptor.vertexBuffers[CSRenderVertexInputIndexVertices].mutability = MTLMutabilityImmutable;
  132. _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineStateDescriptor
  133. error:&error];
  134. NSAssert(_pipelineState, @"Failed to create pipeline state to render to texture: %@", error);
  135. // Create the command queue
  136. _commandQueue = [_device newCommandQueue];
  137. // Sampler
  138. [self _initializeUpscaler:MTLSamplerMinMagFilterLinear downscaler:MTLSamplerMinMagFilterLinear];
  139. }
  140. return self;
  141. }
  142. - (void)_initializeUpscaler:(MTLSamplerMinMagFilter)upscaler downscaler:(MTLSamplerMinMagFilter)downscaler {
  143. MTLSamplerDescriptor *samplerDescriptor = [MTLSamplerDescriptor new];
  144. samplerDescriptor.minFilter = downscaler;
  145. samplerDescriptor.magFilter = upscaler;
  146. _sampler = [_device newSamplerStateWithDescriptor:samplerDescriptor];
  147. }
  148. /// Scalers from VM settings
  149. - (void)changeUpscaler:(MTLSamplerMinMagFilter)upscaler downscaler:(MTLSamplerMinMagFilter)downscaler {
  150. dispatch_async(self.rendererQueue, ^{
  151. [self _initializeUpscaler:upscaler downscaler:downscaler];
  152. });
  153. }
  154. /// Called whenever view changes orientation or is resized
  155. - (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
  156. dispatch_async(self.rendererQueue, ^{
  157. // Save the size of the drawable as we'll pass these
  158. // values to our vertex shader when we draw
  159. _viewportSize.x = size.width;
  160. _viewportSize.y = size.height;
  161. });
  162. }
  163. /// Create a translation+scale matrix
  164. static matrix_float4x4 matrix_scale_translate(CGFloat scale, CGPoint translate)
  165. {
  166. matrix_float4x4 m = {
  167. .columns[0] = {
  168. scale,
  169. 0,
  170. 0,
  171. 0
  172. },
  173. .columns[1] = {
  174. 0,
  175. scale,
  176. 0,
  177. 0
  178. },
  179. .columns[2] = {
  180. 0,
  181. 0,
  182. 1,
  183. 0
  184. },
  185. .columns[3] = {
  186. translate.x,
  187. -translate.y, // y flipped
  188. 0,
  189. 1
  190. }
  191. };
  192. return m;
  193. }
  194. /// Called whenever the view needs to render a frame
  195. - (void)drawInMTKView:(nonnull MTKView *)view
  196. {
  197. // Obtain a renderPassDescriptor generated from the view's drawable textures
  198. MTLRenderPassDescriptor *renderPassDescriptor = view.currentRenderPassDescriptor;
  199. id<CAMetalDrawable> currentDrawable = view.currentDrawable;
  200. if (renderPassDescriptor == nil || currentDrawable == nil) {
  201. return;
  202. }
  203. // synchronize with rendererQueue in order to access currentCommandBuffer
  204. dispatch_sync(self.rendererQueue, ^{
  205. id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
  206. commandBuffer.label = @"Renderer Command Buffer";
  207. if (self.sourceData.isVisible) {
  208. [self _renderCommand:commandBuffer
  209. source:self.sourceData
  210. renderPassDescriptor:renderPassDescriptor];
  211. [commandBuffer presentDrawable:currentDrawable];
  212. }
  213. // Finalize rendering here & push the command buffer to the GPU
  214. [commandBuffer commit];
  215. // clear buffer
  216. self.sourceData = nil;
  217. });
  218. }
  219. - (void)renderSouce:(id<CSRenderSource>)renderSource
  220. copyBuffer:(id<MTLBuffer>)sourceBuffer
  221. region:(MTLRegion)region
  222. sourceOffset:(NSUInteger)sourceOffset
  223. sourceBytesPerRow:(NSUInteger)sourceBytesPerRow
  224. completion:(copyCompletionCallback_t)completion {
  225. _CSRendererSourceData *sourceData = [[_CSRendererSourceData alloc] initWithRenderSource:renderSource];
  226. if (!self.isBlitOnGpu) {
  227. dispatch_async(self.rendererQueue, ^{
  228. [sourceData.texture replaceRegion:region
  229. mipmapLevel:0
  230. withBytes:(sourceBuffer.contents + sourceOffset)
  231. bytesPerRow:sourceBytesPerRow];
  232. self.sourceData = sourceData;
  233. completion();
  234. });
  235. return;
  236. }
  237. dispatch_async(self.rendererQueue, ^{
  238. id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
  239. commandBuffer.label = @"Blit Command Buffer";
  240. id<MTLBlitCommandEncoder> blitEncoder = [commandBuffer blitCommandEncoder];
  241. blitEncoder.label = @"Renderer Canvas Updates";
  242. [blitEncoder copyFromBuffer:sourceBuffer
  243. sourceOffset:sourceOffset
  244. sourceBytesPerRow:sourceBytesPerRow
  245. sourceBytesPerImage:0
  246. sourceSize:region.size
  247. toTexture:sourceData.texture
  248. destinationSlice:0
  249. destinationLevel:0
  250. destinationOrigin:region.origin];
  251. [blitEncoder endEncoding];
  252. [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> commandBuffer) {
  253. completion();
  254. }];
  255. [commandBuffer commit];
  256. self.sourceData = sourceData;
  257. });
  258. }
  259. - (void)invalidateRenderSource:(id<CSRenderSource>)renderSource {
  260. _CSRendererSourceData *sourceData = [[_CSRendererSourceData alloc] initWithRenderSource:renderSource];
  261. if (!sourceData.isVisible) {
  262. return;
  263. }
  264. dispatch_async(self.rendererQueue, ^{
  265. self.sourceData = sourceData;
  266. });
  267. }
  268. - (void)_renderCommand:(id<MTLCommandBuffer>)commandBuffer
  269. source:(id<CSRenderSource>)source
  270. renderPassDescriptor:(MTLRenderPassDescriptor *)renderPassDescriptor {
  271. id<MTLRenderCommandEncoder> renderEncoder =
  272. [commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor];
  273. renderEncoder.label = @"View Presentation";
  274. [renderEncoder setRenderPipelineState:_pipelineState];
  275. // Render the screen first
  276. NSAssert(source.isVisible, @"Screen should be visible at this point!");
  277. [self _renderEncoder:renderEncoder
  278. drawAtOrigin:source.viewportOrigin
  279. scale:source.viewportScale
  280. vertices:source.vertices
  281. numVerticies:source.numVertices
  282. hasAlpha:source.hasAlpha
  283. isInverted:source.isInverted
  284. texture:source.texture];
  285. // Draw cursor
  286. if (source.cursorSource.isVisible) {
  287. // Next render the cursor
  288. [self _renderEncoder:renderEncoder
  289. drawAtOrigin:source.cursorSource.viewportOrigin
  290. scale:source.cursorSource.viewportScale
  291. vertices:source.cursorSource.vertices
  292. numVerticies:source.cursorSource.numVertices
  293. hasAlpha:source.cursorSource.hasAlpha
  294. isInverted:source.cursorSource.isInverted
  295. texture:source.cursorSource.texture];
  296. }
  297. [renderEncoder endEncoding];
  298. }
  299. - (void)_renderEncoder:(id<MTLRenderCommandEncoder>)renderEncoder
  300. drawAtOrigin:(CGPoint)origin
  301. scale:(CGFloat)scale
  302. vertices:(id<MTLBuffer>)vertices
  303. numVerticies:(NSUInteger)numVerticies
  304. hasAlpha:(BOOL)hasAlpha
  305. isInverted:(BOOL)isInverted
  306. texture:(id<MTLTexture>)texture {
  307. matrix_float4x4 transform = matrix_scale_translate(scale,
  308. origin);
  309. [renderEncoder setVertexBuffer:vertices
  310. offset:0
  311. atIndex:CSRenderVertexInputIndexVertices];
  312. [renderEncoder setVertexBytes:&_viewportSize
  313. length:sizeof(_viewportSize)
  314. atIndex:CSRenderVertexInputIndexViewportSize];
  315. [renderEncoder setVertexBytes:&transform
  316. length:sizeof(transform)
  317. atIndex:CSRenderVertexInputIndexTransform];
  318. [renderEncoder setVertexBytes:&hasAlpha
  319. length:sizeof(hasAlpha)
  320. atIndex:CSRenderVertexInputIndexHasAlpha];
  321. // Set the texture object. The CSRenderTextureIndexBaseColor enum value corresponds
  322. /// to the 'colorMap' argument in our 'samplingShader' function because its
  323. // texture attribute qualifier also uses CSRenderTextureIndexBaseColor for its index
  324. [renderEncoder setFragmentTexture:texture
  325. atIndex:CSRenderTextureIndexBaseColor];
  326. [renderEncoder setFragmentSamplerState:_sampler
  327. atIndex:CSRenderSamplerIndexTexture];
  328. [renderEncoder setFragmentBytes:&isInverted
  329. length:sizeof(isInverted)
  330. atIndex:CSRenderFragmentBufferIndexIsInverted];
  331. // Draw the vertices of our triangles
  332. [renderEncoder drawPrimitives:MTLPrimitiveTypeTriangle
  333. vertexStart:0
  334. vertexCount:numVerticies];
  335. }
  336. @end