import Foundation import Metal import QuartzCore.CAMetalLayer import simd import ShaderTypes fileprivate let numFramesInFlight: Int = 3 fileprivate let colorFormat: MTLPixelFormat = .bgra8Unorm_srgb fileprivate let depthFormat: MTLPixelFormat = .depth32Float public class Renderer { private var device: MTLDevice private var _layer: CAMetalLayer private var backBufferSize: Size private var _clearColor: Color private var _aspectRatio: Float private var queue: MTLCommandQueue private var lib: MTLLibrary private var _defaultShader: Shader, _shader2D: Shader private let passDescription = MTLRenderPassDescriptor() private var _psos: [PipelineOptions: MTLRenderPipelineState] private var _depthStencilEnabled: MTLDepthStencilState, _depthStencilDisabled: MTLDepthStencilState private let _defaultStorageMode: MTLResourceOptions private var depthTextures: [MTLTexture] //private var _instances: [MTLBuffer?] private var _cameraPos: SIMD3 = .zero, _directionalDir: SIMD3 = .zero private var _encoder: MTLRenderCommandEncoder! = nil private var defaultTexture: RendererTexture2D private var cubeTexture: RendererTexture2D? = nil private let inFlightSemaphore = DispatchSemaphore(value: numFramesInFlight) private var _currentFrame = 0 internal var currentFrame: Int { self._currentFrame } #if os(macOS) internal var isManagedStorage: Bool { self._defaultStorageMode == .storageModeManaged } #else internal var isManagedStorage: Bool { false } #endif var frame: Rect { .init(origin: .zero, size: self.backBufferSize) } var aspectRatio: Float { self._aspectRatio } var clearColor: Color { get { self._clearColor } set { self._clearColor = newValue } } fileprivate static func createMetalDevice() -> MTLDevice? { #if os(macOS) MTLCopyAllDevices().reduce(nil, { best, dev in if best == nil { dev } else if !best!.isLowPower || dev.isLowPower { best } else if best!.supportsRaytracing || !dev.supportsRaytracing { best } else { dev } }) #else MTLCreateSystemDefaultDevice() #endif } internal init(layer metalLayer: CAMetalLayer, size: Size) throws { self._layer = metalLayer // Select best Metal device guard let device = Self.createMetalDevice() else { throw RendererError.initFailure("Failed to create Metal device") } self.device = device #if os(macOS) self._defaultStorageMode = if #available(macOS 100.100, iOS 12.0, *) { .storageModeShared } else if #available(macOS 10.15, iOS 13.0, *) { self.device.hasUnifiedMemory ? .storageModeShared : .storageModeManaged } else { // https://developer.apple.com/documentation/metal/gpu_devices_and_work_submission/multi-gpu_systems/finding_multiple_gpus_on_an_intel-based_mac#3030770 (self.device.isLowPower && !self.device.isRemovable) ? .storageModeShared : .storageModeManaged } #else self._defaultStorageMode = .storageModeShared #endif self._layer.device = device self._layer.pixelFormat = colorFormat // Setup command queue guard let queue = device.makeCommandQueue() else { throw RendererError.initFailure("Failed to create command queue") } self.queue = queue self.backBufferSize = size self._aspectRatio = Float(self.backBufferSize.w) / Float(self.backBufferSize.w) self._clearColor = .black passDescription.colorAttachments[0].loadAction = .clear passDescription.colorAttachments[0].storeAction = .store passDescription.depthAttachment.loadAction = .clear passDescription.depthAttachment.storeAction = .dontCare passDescription.depthAttachment.clearDepth = 1.0 self.depthTextures = try (0..) -> RendererMesh? { if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil } let vertices = mesh.vertices.map { ShaderVertex(position: $0.position, normal: $0.normal, color: $0.color, texCoord: $0.texCoord) } return self.createMesh(vertices, mesh.indices) } func createMesh(_ mesh: Mesh) -> RendererMesh? { if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil } let color = Color.white let vertices = mesh.vertices.map { ShaderVertex(position: $0.position, normal: $0.normal, color: SIMD4(color), texCoord: $0.texCoord) } return self.createMesh(vertices, mesh.indices) } private func createMesh(_ vertices: [ShaderVertex], _ indices: [UInt16]) -> RendererMesh? { autoreleasepool { let vtxSize = vertices.count * MemoryLayout.stride guard let vtxSource = self.device.makeBuffer(bytes: vertices, length: vtxSize, options: self._defaultStorageMode) else { printErr("Failed to create vertex buffer source") return nil } let numIndices = indices.count let idxSize = numIndices * MemoryLayout.stride guard let idxSource = self.device.makeBuffer(bytes: indices, length: idxSize, options: self._defaultStorageMode) else { printErr("Failed to create index buffer source") return nil } guard let vtxDestination = self.device.makeBuffer(length: vtxSize, options: .storageModePrivate) else { printErr("Failed to create vertex buffer destination") return nil } guard let idxDestination = self.device.makeBuffer(length: idxSize, options: .storageModePrivate) else { printErr("Failed to create index buffer destination") return nil } guard let cmdBuffer = queue.makeCommandBuffer(), let blitEncoder = cmdBuffer.makeBlitCommandEncoder() else { printErr("Failed to create blit command encoder") return nil } blitEncoder.copy(from: vtxSource, sourceOffset: 0, to: vtxDestination, destinationOffset: 0, size: vtxSize) blitEncoder.copy(from: idxSource, sourceOffset: 0, to: idxDestination, destinationOffset: 0, size: idxSize) blitEncoder.endEncoding() cmdBuffer.addCompletedHandler { _ in //FIXME: look into if this needs to be synchronised //printErr("Mesh data was added?") } cmdBuffer.commit() return .init(_vertBuf: vtxDestination, _idxBuf: idxDestination, numIndices: numIndices) } } internal func createDynamicMesh( vertexCapacity: Int, indexCapacity: Int ) -> RendererDynamicMesh? { let vertexBuffers: [MTLBuffer], indexBuffers: [MTLBuffer] do { let byteCapacity = MemoryLayout.stride * vertexCapacity vertexBuffers = try Self.createDynamicBuffer(self.device, capacity: byteCapacity, self._defaultStorageMode) } catch { printErr("Failed to create vertex buffer") return nil } do { let byteCapacity = MemoryLayout.stride * indexCapacity indexBuffers = try Self.createDynamicBuffer(self.device, capacity: byteCapacity, self._defaultStorageMode) } catch { printErr("Failed to create index buffer") return nil } return .init(renderer: self, vertexBuffers, indexBuffers) } private static func createDynamicBuffer(_ device: MTLDevice, capacity: Int, _ transitoryOpt: MTLResourceOptions ) throws -> [MTLBuffer] { try autoreleasepool { try (0.. RendererTexture2D? { do { return try Self.loadTexture(self.device, self.queue, resourcePath: path, self._defaultStorageMode) } catch { printErr(error) return nil } } static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, resourcePath path: String, _ transitoryOpt: MTLResourceOptions ) throws -> RendererTexture2D { do { return try loadTexture(device, queue, url: Bundle.main.getResource(path), transitoryOpt) } catch ContentError.resourceNotFound(let message) { throw RendererError.loadFailure(message) } } static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, url imageUrl: URL, _ transitoryOpt: MTLResourceOptions ) throws -> RendererTexture2D { do { return try loadTexture(device, queue, image2D: try NSImageLoader.open(url: imageUrl), transitoryOpt) } catch ImageLoaderError.openFailed(let message) { throw RendererError.loadFailure(message) } } static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, image2D image: Image2D, _ transitoryOpt: MTLResourceOptions ) throws -> RendererTexture2D { try autoreleasepool { let texDesc = MTLTextureDescriptor() texDesc.width = image.width texDesc.height = image.height texDesc.pixelFormat = .rgba8Unorm_srgb texDesc.textureType = .type2D texDesc.storageMode = .private texDesc.usage = .shaderRead guard let newTexture = device.makeTexture(descriptor: texDesc) else { throw RendererError.loadFailure("Failed to create texture descriptor") } guard let texData = image.data.withUnsafeBytes({ bytes in device.makeBuffer(bytes: bytes.baseAddress!, length: bytes.count, options: transitoryOpt) }) else { throw RendererError.loadFailure("Failed to create shared texture data buffer") } guard let cmdBuffer = queue.makeCommandBuffer(), let blitEncoder = cmdBuffer.makeBlitCommandEncoder() else { throw RendererError.loadFailure("Failed to create blit command encoder") } blitEncoder.copy( from: texData, sourceOffset: 0, sourceBytesPerRow: image.stride, sourceBytesPerImage: image.stride * image.height, sourceSize: .init(width: image.width, height: image.height, depth: 1), to: newTexture, destinationSlice: 0, destinationLevel: 0, destinationOrigin: .init(x: 0, y: 0, z: 0)) blitEncoder.endEncoding() cmdBuffer.addCompletedHandler { _ in //FIXME: look into if this needs to be synchronised //printErr("Texture was added?") } cmdBuffer.commit() return .init(metalTexture: newTexture, size: .init(image.width, image.height)) } } private static func createDepthTexture(_ device: MTLDevice, _ size: Size, format: MTLPixelFormat ) -> MTLTexture? { autoreleasepool { let texDescriptor = MTLTextureDescriptor.texture2DDescriptor( pixelFormat: format, width: size.w, height: size.h, mipmapped: false) texDescriptor.depth = 1 texDescriptor.sampleCount = 1 texDescriptor.usage = [ .renderTarget, .shaderRead ] #if !NDEBUG texDescriptor.storageMode = .private #else texDescriptor.storageMode = .memoryless #endif guard let depthStencilTexture = device.makeTexture(descriptor: texDescriptor) else { return nil } depthStencilTexture.label = "Depth buffer" return depthStencilTexture } } static func makeViewport(rect: Rect, znear: Double = 0.0, zfar: Double = 1.0) -> MTLViewport { MTLViewport( originX: Double(rect.x), originY: Double(rect.y), width: Double(rect.w), height: Double(rect.h), znear: znear, zfar: zfar) } func resize(size: Size) { if self.backBufferSize.w != size.w || self.backBufferSize.h != size.h { self.depthTextures = (0.. Void) throws { try autoreleasepool { guard let rt = self._layer.nextDrawable() else { throw RendererError.drawFailure("Failed to get next drawable render target") } passDescription.colorAttachments[0].clearColor = MTLClearColor(self._clearColor) passDescription.colorAttachments[0].texture = rt.texture passDescription.depthAttachment.texture = self.depthTextures[self._currentFrame] // Lock the semaphore here if too many frames are "in flight" _ = inFlightSemaphore.wait(timeout: .distantFuture) guard let commandBuf: MTLCommandBuffer = queue.makeCommandBuffer() else { throw RendererError.drawFailure("Failed to make command buffer from queue") } commandBuf.addCompletedHandler { _ in self.inFlightSemaphore.signal() } guard let encoder = commandBuf.makeRenderCommandEncoder(descriptor: passDescription) else { throw RendererError.drawFailure("Failed to make render encoder from command buffer") } encoder.setFrontFacing(.counterClockwise) // OpenGL default encoder.setViewport(Self.makeViewport(rect: self.frame)) encoder.setFragmentTexture(cubeTexture?._textureBuffer ?? defaultTexture._textureBuffer, index: 0) self._encoder = encoder frameFunc(self) self._encoder = nil encoder.endEncoding() commandBuf.present(rt) commandBuf.commit() self._currentFrame &+= 1 if self._currentFrame == numFramesInFlight { self._currentFrame = 0 } } } func createModelBatch() -> ModelBatch { return ModelBatch(self) } func createSpriteBatch() -> SpriteBatch { return SpriteBatch(self, Rect(origin: .zero, size: Size(self.backBufferSize))) } internal func setupBatch(environment: Environment, camera: Camera) { assert(self._encoder != nil, "setupBatch can't be called outside of a frame being rendered") do { try self.usePipeline(options: PipelineOptions( colorFormat: self._layer.pixelFormat, depthFormat: depthFormat, shader: self._defaultShader, blendFunc: .off)) } catch { printErr(error) } var vertUniforms = VertexShaderUniforms(projView: camera.viewProjection) self._cameraPos = camera.position self._directionalDir = simd_normalize(environment.lightDirection) self._encoder.setDepthStencilState(self._depthStencilEnabled) self._encoder.setCullMode(.init(environment.cullFace)) // Ideal as long as our uniforms total 4 KB or less self._encoder.setVertexBytes(&vertUniforms, length: MemoryLayout.stride, index: VertexShaderInputIdx.uniforms.rawValue) } internal func setupBatch(blendMode: BlendMode, frame: Rect) { assert(self._encoder != nil, "setupBatch can't be called outside of a frame being rendered") do { try self.usePipeline(options: PipelineOptions( colorFormat: self._layer.pixelFormat, depthFormat: depthFormat, shader: self._shader2D, blendFunc: blendMode.function)) } catch { printErr(error) } self._encoder.setDepthStencilState(self._depthStencilDisabled) self._encoder.setCullMode(.none) var uniforms = Shader2DUniforms(projection: .orthographic( left: frame.left, right: frame.right, bottom: frame.down, top: frame.up, near: 1, far: -1)) // Ideal as long as our uniforms total 4 KB or less self._encoder.setVertexBytes(&uniforms, length: MemoryLayout.stride, index: VertexShaderInputIdx.uniforms.rawValue) } internal func submit( mesh: RendererDynamicMesh, texture: RendererTexture2D?, offset: Int, count: Int ) { assert(self._encoder != nil, "submit can't be called outside of a frame being rendered") self._encoder.setFragmentTexture(texture?._textureBuffer ?? defaultTexture._textureBuffer, index: 0) self._encoder.setVertexBuffer(mesh._vertBufs[self._currentFrame], offset: 0, index: VertexShaderInputIdx.vertices.rawValue) self._encoder.drawIndexedPrimitives( type: .triangle, indexCount: count, indexType: .uint16, // Careful! indexBuffer: mesh._idxBufs[self._currentFrame], indexBufferOffset: MemoryLayout.stride * offset) } internal func submit(mesh: RendererMesh, instance: ModelBatch.Instance, material: Material) { assert(self._encoder != nil, "submit can't be called outside of a frame being rendered") var instanceData = VertexShaderInstance( model: instance.world, normalModel: instance.world.inverse.transpose, color: SIMD4(instance.color)) var fragUniforms = FragmentShaderUniforms( cameraPosition: self._cameraPos, directionalLight: self._directionalDir, ambientColor: SIMD4(material.ambient), diffuseColor: SIMD4(material.diffuse), specularColor: SIMD4(material.specular), specularIntensity: material.gloss) self._encoder.setVertexBuffer(mesh._vertBuf, offset: 0, index: VertexShaderInputIdx.vertices.rawValue) // Ideal as long as our uniforms total 4 KB or less self._encoder.setVertexBytes(&instanceData, length: MemoryLayout.stride, index: VertexShaderInputIdx.instance.rawValue) self._encoder.setFragmentBytes(&fragUniforms, length: MemoryLayout.stride, index: FragmentShaderInputIdx.uniforms.rawValue) self._encoder.drawIndexedPrimitives( type: .triangle, indexCount: mesh.numIndices, indexType: .uint16, indexBuffer: mesh._idxBuf, indexBufferOffset: 0) } internal func submitBatch(mesh: RendererMesh, instances: [ModelBatch.Instance], material: Material) { assert(self._encoder != nil, "submitBatch can't be called outside of a frame being rendered") let numInstances = instances.count assert(numInstances > 0, "submitBatch called with zero instances") /* let instancesBytes = numInstances * MemoryLayout.stride // (Re)create instance buffer if needed if self._instances[self._currentFrame] == nil || instancesBytes > self._instances[self._currentFrame]!.length { guard let instanceBuffer = self.device.makeBuffer( length: instancesBytes, options: self._defaultStorageMode) else { fatalError("Failed to (re)create instance buffer") } self._instances[self._currentFrame] = instanceBuffer } let instanceBuffer = self._instances[self._currentFrame]! // Convert & upload instance data to the GPU //FIXME: currently will misbehave if batch is called more than once instanceBuffer.contents().withMemoryRebound(to: VertexShaderInstance.self, capacity: numInstances) { data in for i in 0...stride, index: VertexShaderInputIdx.instance.rawValue) self._encoder.setFragmentBytes(&fragUniforms, length: MemoryLayout.stride, index: FragmentShaderInputIdx.uniforms.rawValue) self._encoder.drawIndexedPrimitives( type: .triangle, indexCount: mesh.numIndices, indexType: .uint16, indexBuffer: mesh._idxBuf, indexBufferOffset: 0, instanceCount: numInstances) } }