import Foundation import Metal #if os(iOS) import Display import UIKit #else import AppKit import TGUIKit #endif import IOSurface import ShelfPack public final class Placeholder { var contents: Resolved? } private let noInputPlaceholder: Placeholder = { let value = Placeholder() value.contents = Void() return value }() private struct PlaceholderResolveError: Error { } private func resolvePlaceholder(_ value: Placeholder) throws -> T { guard let contents = value.contents else { throw PlaceholderResolveError() } return contents } public struct TextureSpec: Equatable { public enum PixelFormat { case r8UnsignedNormalized case rgba8UnsignedNormalized } public var width: Int public var height: Int public var pixelFormat: PixelFormat public init(width: Int, height: Int, pixelFormat: PixelFormat) { self.width = width self.height = height self.pixelFormat = pixelFormat } } extension TextureSpec.PixelFormat { var metalFormat: MTLPixelFormat { switch self { case .r8UnsignedNormalized: return .r8Unorm case .rgba8UnsignedNormalized: return .rgba8Unorm } } } public final class TexturePlaceholder { public let placeholer: Placeholder public let spec: TextureSpec init(placeholer: Placeholder, spec: TextureSpec) { self.placeholer = placeholer self.spec = spec } } public struct RenderSize: Equatable { public var width: Int public var height: Int public init(width: Int, height: Int) { self.width = width self.height = height } } public struct RenderLayerSpec: Equatable { public var size: RenderSize public var edgeInset: Int public init(size: RenderSize, edgeInset: Int = 0) { self.size = size self.edgeInset = edgeInset } } private extension RenderLayerSpec { var allocationWidth: Int { return self.size.width + self.edgeInset * 2 } var allocationHeight: Int { return self.size.height + self.edgeInset * 2 } } public struct RenderLayerPlacement: Equatable { public var effectiveRect: CGRect public init(effectiveRect: CGRect) { self.effectiveRect = effectiveRect } } public protocol RenderToLayerState: AnyObject { var pipelineState: MTLRenderPipelineState { get } init?(device: MTLDevice) } public protocol ComputeState: AnyObject { init?(device: MTLDevice) } open class MetalEngineSubjectLayer: SimpleLayer { fileprivate var internalId: Int = -1 fileprivate var surfaceAllocation: MetalEngine.SurfaceAllocation? #if DEBUG fileprivate var surfaceChangeFrameCount: Int = 0 #endif public var cloneLayers: [CALayer] = [] override open var contents: Any? { didSet { if !self.cloneLayers.isEmpty { for cloneLayer in self.cloneLayers { cloneLayer.contents = self.contents } } } } override open var contentsRect: CGRect { didSet { if !self.cloneLayers.isEmpty { for cloneLayer in self.cloneLayers { cloneLayer.contentsRect = self.contentsRect } } } } public override init() { super.init() self.setNeedsDisplay() } deinit { MetalEngine.shared.impl.removeLayerSurfaceAllocation(layer: self) } override public init(layer: Any) { super.init(layer: layer) } required public init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override public func setNeedsDisplay() { if let subject = self as? MetalEngineSubject { subject.setNeedsUpdate() } } } protocol MetalEngineResource: AnyObject { func free() } public final class PooledTexture { final class Texture: MetalEngineResource { let value: MTLTexture var isInUse: Bool = false init(value: MTLTexture) { self.value = value } public func free() { self.isInUse = false } } public let spec: TextureSpec private let textures: [Texture] init(device: MTLDevice, spec: TextureSpec) { self.spec = spec self.textures = (0 ..< 3).compactMap { _ -> Texture? in let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: spec.pixelFormat.metalFormat, width: spec.width, height: spec.height, mipmapped: false) descriptor.storageMode = .private descriptor.usage = [.shaderRead, .shaderWrite] guard let texture = device.makeTexture(descriptor: descriptor) else { return nil } return Texture(value: texture) } } public func get(context: MetalEngineSubjectContext) -> TexturePlaceholder? { #if DEBUG if context.freeResourcesOnCompletion.contains(where: { $0 === self }) { assertionFailure("Trying to get PooledTexture more than once per update cycle") } #endif for texture in self.textures { if !texture.isInUse { texture.isInUse = true let placeholder = Placeholder() placeholder.contents = texture.value context.freeResourcesOnCompletion.append(texture) return TexturePlaceholder(placeholer: placeholder, spec: self.spec) } } print("PooledTexture: all textures are in use") return nil } } public struct BufferSpec: Equatable { public var length: Int public init(length: Int) { self.length = length } } public final class BufferPlaceholder { public let placeholer: Placeholder public let spec: BufferSpec init(placeholer: Placeholder, spec: BufferSpec) { self.placeholer = placeholer self.spec = spec } } public final class PooledBuffer { final class Buffer: MetalEngineResource { let value: MTLBuffer var isInUse: Bool = false init(value: MTLBuffer) { self.value = value } public func free() { self.isInUse = false } } public let spec: BufferSpec private let buffers: [Buffer] init(device: MTLDevice, spec: BufferSpec) { self.spec = spec self.buffers = (0 ..< 3).compactMap { _ -> Buffer? in guard let texture = device.makeBuffer(length: spec.length, options: [.storageModePrivate]) else { return nil } return Buffer(value: texture) } } public func get(context: MetalEngineSubjectContext) -> BufferPlaceholder? { #if DEBUG if context.freeResourcesOnCompletion.contains(where: { $0 === self }) { assertionFailure("Trying to get PooledTexture more than once per update cycle") } #endif for buffer in self.buffers { if !buffer.isInUse { buffer.isInUse = true let placeholder = Placeholder() placeholder.contents = buffer.value context.freeResourcesOnCompletion.append(buffer) return BufferPlaceholder(placeholer: placeholder, spec: self.spec) } } print("PooledBuffer: all textures are in use") return nil } } public final class SharedBuffer { public let buffer: MTLBuffer init?(device: MTLDevice, spec: BufferSpec) { guard let buffer = device.makeBuffer(length: spec.length, options: [.storageModeShared]) else { return nil } self.buffer = buffer } } public final class MetalEngineSubjectContext { fileprivate final class ComputeOperation { let commands: (MTLCommandBuffer) -> Void init(commands: @escaping (MTLCommandBuffer) -> Void) { self.commands = commands } } fileprivate final class RenderToLayerOperation { let spec: RenderLayerSpec let state: RenderToLayerState weak var layer: MetalEngineSubjectLayer? let commands: (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void init( spec: RenderLayerSpec, state: RenderToLayerState, layer: MetalEngineSubjectLayer, commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void ) { self.spec = spec self.state = state self.layer = layer self.commands = commands } } private let device: MTLDevice private let impl: MetalEngine.Impl fileprivate var computeOperations: [ComputeOperation] = [] fileprivate var renderToLayerOperationsGroupedByState: [ObjectIdentifier: [RenderToLayerOperation]] = [:] fileprivate var freeResourcesOnCompletion: [MetalEngineResource] = [] fileprivate init(device: MTLDevice, impl: MetalEngine.Impl) { self.device = device self.impl = impl } public func renderToLayer( spec: RenderLayerSpec, state: RenderToLayerStateType.Type, layer: MetalEngineSubjectLayer, inputs: repeat Placeholder, commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement, repeat each Resolved) -> Void ) { let stateTypeId = ObjectIdentifier(state) let resolvedState: RenderToLayerStateType if let current = self.impl.renderStates[stateTypeId] as? RenderToLayerStateType { resolvedState = current } else { guard let value = RenderToLayerStateType(device: self.device) else { assertionFailure("Could not initialize render state \(state)") return } resolvedState = value self.impl.renderStates[stateTypeId] = resolvedState } let operation = RenderToLayerOperation( spec: spec, state: resolvedState, layer: layer, commands: { encoder, placement in let resolvedInputs: (repeat each Resolved) do { resolvedInputs = (repeat try resolvePlaceholder(each inputs)) } catch { print("Could not resolve renderToLayer inputs") return } commands(encoder, placement, repeat each resolvedInputs) } ) if self.renderToLayerOperationsGroupedByState[stateTypeId] == nil { self.renderToLayerOperationsGroupedByState[stateTypeId] = [operation] } else { self.renderToLayerOperationsGroupedByState[stateTypeId]?.append(operation) } } public func renderToLayer( spec: RenderLayerSpec, state: RenderToLayerStateType.Type, layer: MetalEngineSubjectLayer, commands: @escaping (MTLRenderCommandEncoder, RenderLayerPlacement) -> Void ) { self.renderToLayer(spec: spec, state: state, layer: layer, inputs: noInputPlaceholder, commands: { encoder, placement, _ in commands(encoder, placement) }) } public func compute( state: ComputeStateType.Type, inputs: repeat Placeholder, commands: @escaping (MTLCommandBuffer, ComputeStateType, repeat each Resolved) -> Output ) -> Placeholder { let stateTypeId = ObjectIdentifier(state) let resolvedState: ComputeStateType if let current = self.impl.computeStates[stateTypeId] as? ComputeStateType { resolvedState = current } else { guard let value = ComputeStateType(device: self.device) else { assertionFailure("Could not initialize compute state \(state)") return Placeholder() } resolvedState = value self.impl.computeStates[stateTypeId] = resolvedState } let resultPlaceholder = Placeholder() self.computeOperations.append(ComputeOperation(commands: { commandBuffer in let resolvedInputs: (repeat each Resolved) do { resolvedInputs = (repeat try resolvePlaceholder(each inputs)) } catch { print("Could not resolve renderToLayer inputs") return } resultPlaceholder.contents = commands(commandBuffer, resolvedState, repeat each resolvedInputs) })) return resultPlaceholder } public func compute( state: ComputeStateType.Type, commands: @escaping (MTLCommandBuffer, ComputeStateType) -> Output ) -> Placeholder { return self.compute(state: state, inputs: noInputPlaceholder, commands: { commandBuffer, state, _ in return commands(commandBuffer, state) }) } } public final class MetalEngineSubjectInternalData { var internalId: Int = -1 var renderSurfaceAllocation: MetalEngine.SurfaceAllocation? init() { } } public protocol MetalEngineSubject: AnyObject { var internalData: MetalEngineSubjectInternalData? { get set } func setNeedsUpdate() func update(context: MetalEngineSubjectContext) } public extension MetalEngineSubject { func setNeedsUpdate() { MetalEngine.shared.impl.addSubjectNeedsUpdate(subject: self) } } #if targetEnvironment(simulator) @available(iOS 13.0, *) #endif private final class MetalEventLayer: CAMetalLayer { var onDisplay: (() -> Void)? override func display() { self.onDisplay?() } } public final class MetalEngine { struct SurfaceAllocation { struct Phase { let subRect: CGRect let renderingRect: CGRect let contentsRect: CGRect } let surfaceId: Int let allocationId0: Int32 let allocationId1: Int32 let renderingParameters: RenderLayerSpec let phase0: Phase let phase1: Phase var currentPhase: Int = 0 var effectivePhase: Phase { if self.currentPhase == 0 { return self.phase0 } else { return self.phase1 } } init(surfaceId: Int, allocationId0: Int32, allocationId1: Int32, renderingParameters: RenderLayerSpec, phase0: Phase, phase1: Phase) { self.surfaceId = surfaceId self.allocationId0 = allocationId0 self.allocationId1 = allocationId1 self.renderingParameters = renderingParameters self.phase0 = phase0 self.phase1 = phase1 } } fileprivate final class Surface { let id: Int let width: Int let height: Int let ioSurface: IOSurface let texture: MTLTexture let packContext: ShelfPackContext var isEmpty: Bool { return self.packContext.isEmpty } init?(id: Int, device: MTLDevice, width: Int, height: Int) { self.id = id self.width = width self.height = height self.packContext = ShelfPackContext(width: Int32(width), height: Int32(height)) let ioSurfaceProperties: [String: Any] = [ kIOSurfaceWidth as String: width, kIOSurfaceHeight as String: height, kIOSurfaceBytesPerElement as String: 4, kIOSurfacePixelFormat as String: kCVPixelFormatType_32BGRA ] guard let ioSurface = IOSurfaceCreate(ioSurfaceProperties as CFDictionary) else { return nil } self.ioSurface = ioSurface let textureDescriptor = MTLTextureDescriptor() textureDescriptor.pixelFormat = .bgra8Unorm textureDescriptor.width = Int(width) textureDescriptor.height = Int(height) textureDescriptor.storageMode = .shared textureDescriptor.usage = .renderTarget guard let texture = device.makeTexture(descriptor: textureDescriptor, iosurface: ioSurface, plane: 0) else { return nil } self.texture = texture } private struct AllocationLayout { let subRect: CGRect let renderingRect: CGRect let contentsRect: CGRect init(baseRect: CGRect, edgeSize: CGFloat, surfaceWidth: Int, surfaceHeight: Int) { self.subRect = CGRect(origin: CGPoint(x: baseRect.minX, y: baseRect.minY), size: CGSize(width: baseRect.width, height: baseRect.height)) self.renderingRect = CGRect(origin: CGPoint(x: self.subRect.minX / CGFloat(surfaceWidth), y: self.subRect.minY / CGFloat(surfaceHeight)), size: CGSize(width: self.subRect.width / CGFloat(surfaceWidth), height: self.subRect.height / CGFloat(surfaceHeight))) let subRectWithInset = self.subRect.insetBy(dx: edgeSize, dy: edgeSize) self.contentsRect = CGRect(origin: CGPoint(x: subRectWithInset.minX / CGFloat(surfaceWidth), y: 1.0 - subRectWithInset.minY / CGFloat(surfaceHeight) - subRectWithInset.height / CGFloat(surfaceHeight)), size: CGSize(width: subRectWithInset.width / CGFloat(surfaceWidth), height: subRectWithInset.height / CGFloat(surfaceHeight))) } } func allocateIfPossible(renderingParameters: RenderLayerSpec) -> SurfaceAllocation? { let width = renderingParameters.allocationWidth let height = renderingParameters.allocationHeight let item0 = self.packContext.addItem(withWidth: Int32(width), height: Int32(height)) let item1 = self.packContext.addItem(withWidth: Int32(width), height: Int32(height)) if item0.itemId != -1 && item1.itemId != -1 { let layout0 = AllocationLayout( baseRect: CGRect(origin: CGPoint(x: CGFloat(item0.x), y: CGFloat(item0.y)), size: CGSize(width: CGFloat(item0.width), height: CGFloat(item0.height))), edgeSize: CGFloat(renderingParameters.edgeInset), surfaceWidth: self.width, surfaceHeight: self.height ) let layout1 = AllocationLayout( baseRect: CGRect(origin: CGPoint(x: CGFloat(item1.x), y: CGFloat(item1.y)), size: CGSize(width: CGFloat(item1.width), height: CGFloat(item1.height))), edgeSize: CGFloat(renderingParameters.edgeInset), surfaceWidth: self.width, surfaceHeight: self.height ) return SurfaceAllocation( surfaceId: self.id, allocationId0: item0.itemId, allocationId1: item1.itemId, renderingParameters: renderingParameters, phase0: SurfaceAllocation.Phase( subRect: layout0.subRect, renderingRect: layout0.renderingRect, contentsRect: layout0.contentsRect ), phase1: SurfaceAllocation.Phase( subRect: layout1.subRect, renderingRect: layout1.renderingRect, contentsRect: layout1.contentsRect ) ) } else { if item0.itemId != -1 { self.packContext.removeItem(item0.itemId) } if item1.itemId != -1 { self.packContext.removeItem(item1.itemId) } return nil } } func removeAllocation(id: Int32) { self.packContext.removeItem(id) } } private final class SubjectReference { weak var subject: MetalEngineSubject? init(subject: MetalEngineSubject) { self.subject = subject } } fileprivate final class Impl { let device: MTLDevice let library: MTLLibrary let commandQueue: MTLCommandQueue let clearPipelineState: MTLRenderPipelineState #if targetEnvironment(simulator) let _layer: CALayer @available(iOS 13.0, *) var layer: MetalEventLayer { return self._layer as! MetalEventLayer } #else let layer: MetalEventLayer #endif var nextSurfaceId: Int = 0 var surfaces: [Int: Surface] = [:] private var nextLayerId: Int = 0 private var nextSubjectId: Int = 0 private var updatedSubjectIds: [Int] = [] private var updatedSubjects: [SubjectReference] = [] private var scheduledClearAllocations: [SurfaceAllocation] = [] fileprivate var renderStates: [ObjectIdentifier: RenderToLayerState] = [:] fileprivate var computeStates: [ObjectIdentifier: ComputeState] = [:] init?(device: MTLDevice) { self.device = device guard let commandQueue = device.makeCommandQueue() else { return nil } self.commandQueue = commandQueue let library: MTLLibrary? #if os(iOS) let mainBundle = Bundle(for: Impl.self) guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else { return nil } guard let bundle = Bundle(path: path) else { return nil } library = try? device.makeDefaultLibrary(bundle: bundle) #else let mainBundle = Bundle(for: Impl.self) guard let path = mainBundle.path(forResource: "MetalEngineMetalSourcesBundle", ofType: "bundle") else { return nil } guard let bundle = Bundle(path: path) else { return nil } guard let path = bundle.path(forResource: "MetalEngineShaders", ofType: "metallib") else { return nil } library = try? device.makeLibrary(URL: .init(fileURLWithPath: path)) #endif guard let lib = library else { return nil } self.library = lib guard let vertexFunction = lib.makeFunction(name: "clearVertex") else { return nil } guard let fragmentFunction = lib.makeFunction(name: "clearFragment") else { return nil } let pipelineDescriptor = MTLRenderPipelineDescriptor() pipelineDescriptor.vertexFunction = vertexFunction pipelineDescriptor.fragmentFunction = fragmentFunction pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm guard let clearPipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) else { return nil } self.clearPipelineState = clearPipelineState #if targetEnvironment(simulator) if #available(iOS 13.0, *) { self._layer = MetalEventLayer() } else { self._layer = CALayer() } #else self.layer = MetalEventLayer() #endif #if targetEnvironment(simulator) @available(iOS 13.0, *) #endif func configureLayer(layer: MetalEventLayer, device: MTLDevice, impl: Impl) { layer.drawableSize = CGSize(width: 32, height: 32) layer.contentsScale = 1.0 layer.device = device layer.presentsWithTransaction = true layer.framebufferOnly = false layer.onDisplay = { [unowned impl] in impl.display() } } #if targetEnvironment(simulator) if #available(iOS 13.0, *) { configureLayer(layer: self.layer, device: self.device, impl: self) } #else configureLayer(layer: self.layer, device: self.device, impl: self) #endif } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func addSurface(width: Int, height: Int) -> Surface? { let surfaceId = self.nextSurfaceId self.nextSurfaceId += 1 let surface = Surface(id: surfaceId, device: self.device, width: width, height: height) self.surfaces[surfaceId] = surface return surface } private func refreshLayerAllocation(layer: MetalEngineSubjectLayer, renderSpec: RenderLayerSpec) { var previousSurfaceId: Int? var updatedSurfaceId: Int? if let allocation = layer.surfaceAllocation { previousSurfaceId = allocation.surfaceId if renderSpec != allocation.renderingParameters { layer.surfaceAllocation = nil self.scheduledClearAllocations.append(allocation) } } if layer.internalId != -1 { let renderingParameters = renderSpec if let currentAllocation = layer.surfaceAllocation { var updatedAllocation = currentAllocation updatedAllocation.currentPhase = updatedAllocation.currentPhase == 0 ? 1 : 0 layer.surfaceAllocation = updatedAllocation updatedSurfaceId = updatedAllocation.surfaceId layer.contentsRect = updatedAllocation.effectivePhase.contentsRect } else { if renderingParameters.allocationWidth >= 1024 || renderingParameters.allocationHeight >= 1024 { let surfaceWidth = max(1024, alignUp(renderingParameters.allocationWidth * 2, alignment: 64)) let surfaceHeight = max(512, alignUp(renderingParameters.allocationHeight, alignment: 64)) if let surface = self.addSurface(width: surfaceWidth, height: surfaceHeight) { if let allocation = surface.allocateIfPossible(renderingParameters: renderingParameters) { layer.surfaceAllocation = allocation layer.contentsRect = allocation.effectivePhase.contentsRect updatedSurfaceId = allocation.surfaceId } } } else { for (_, surface) in self.surfaces { if let allocation = surface.allocateIfPossible(renderingParameters: renderingParameters) { layer.surfaceAllocation = allocation layer.contentsRect = allocation.effectivePhase.contentsRect updatedSurfaceId = allocation.surfaceId break } } } if updatedSurfaceId == nil { let surfaceWidth = alignUp(2048, alignment: 64) let surfaceHeight = alignUp(2048, alignment: 64) if let surface = self.addSurface(width: surfaceWidth, height: surfaceHeight) { if let allocation = surface.allocateIfPossible(renderingParameters: renderingParameters) { layer.surfaceAllocation = allocation layer.contentsRect = allocation.effectivePhase.contentsRect updatedSurfaceId = allocation.surfaceId } } } } } else { if let currentAllocation = layer.surfaceAllocation { layer.surfaceAllocation = nil self.scheduledClearAllocations.append(currentAllocation) } } if previousSurfaceId != updatedSurfaceId { if let updatedSurfaceId { layer.contents = self.surfaces[updatedSurfaceId]?.ioSurface if previousSurfaceId != nil { #if DEBUG layer.surfaceChangeFrameCount += 1 if layer.surfaceChangeFrameCount > 100 { print("Changing surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight))") } #endif } } else { layer.contents = nil if layer.internalId != -1 { #if DEBUG print("Unable to allocate rendering surface for layer \(layer) (\(renderSpec.allocationWidth)x\(renderSpec.allocationHeight)") #endif } } } else { #if DEBUG layer.surfaceChangeFrameCount = max(0, layer.surfaceChangeFrameCount - 1) #endif } } func removeLayerSurfaceAllocation(layer: MetalEngineSubjectLayer) { if let allocation = layer.surfaceAllocation { self.scheduledClearAllocations.append(allocation) } } func addSubjectNeedsUpdate(subject: MetalEngineSubject) { let internalData: MetalEngineSubjectInternalData if let current = subject.internalData { internalData = current } else { internalData = MetalEngineSubjectInternalData() subject.internalData = internalData } let internalId: Int if internalData.internalId != -1 { internalId = internalData.internalId } else { internalId = self.nextSubjectId self.nextSubjectId += 1 internalData.internalId = internalId } let isFirst = self.updatedSubjectIds.isEmpty if !self.updatedSubjectIds.contains(internalId) { self.updatedSubjectIds.append(internalId) self.updatedSubjects.append(SubjectReference(subject: subject)) } if isFirst { #if targetEnvironment(simulator) if #available(iOS 13.0, *) { self.layer.setNeedsDisplay() } #else self.layer.setNeedsDisplay() #endif } } func display() { if !self.scheduledClearAllocations.isEmpty { for allocation in self.scheduledClearAllocations { if let surface = self.surfaces[allocation.surfaceId] { surface.removeAllocation(id: allocation.allocationId0) surface.removeAllocation(id: allocation.allocationId1) } } self.scheduledClearAllocations.removeAll() //TODO:remove clear empty surfaces } if self.updatedSubjects.isEmpty { return } let wereActionsDisabled = CATransaction.disableActions() CATransaction.setDisableActions(true) defer { CATransaction.setDisableActions(wereActionsDisabled) } guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { return } let subjectContext = MetalEngineSubjectContext(device: device, impl: self) for subjectReference in self.updatedSubjects { guard let subject = subjectReference.subject else { continue } subject.update(context: subjectContext) } self.updatedSubjects.removeAll() self.updatedSubjectIds.removeAll() if !subjectContext.computeOperations.isEmpty { for computeOperation in subjectContext.computeOperations { computeOperation.commands(commandBuffer) } } if !subjectContext.renderToLayerOperationsGroupedByState.isEmpty { for (_, renderToLayerOperations) in subjectContext.renderToLayerOperationsGroupedByState { for renderToLayerOperation in renderToLayerOperations { guard let layer = renderToLayerOperation.layer else { continue } if layer.internalId == -1 { layer.internalId = self.nextLayerId self.nextLayerId += 1 } self.refreshLayerAllocation(layer: layer, renderSpec: renderToLayerOperation.spec) } } var surfaceIds: [Int] = [] for (id, surface) in self.surfaces { surfaceIds.append(id) var clearQuads: [SIMD2] = [] for (_, renderToLayerOperations) in subjectContext.renderToLayerOperationsGroupedByState { for renderToLayerOperation in renderToLayerOperations { guard let layer = renderToLayerOperation.layer else { continue } guard let surfaceAllocation = layer.surfaceAllocation, surfaceAllocation.surfaceId == id else { continue } let layerRect = surfaceAllocation.effectivePhase.renderingRect //let edgeSize = surfaceAllocation.renderingParameters.edgeInset //let renderSize = CGSize(width: surfaceAllocation.renderingParameters.size.width, height: surfaceAllocation.renderingParameters.size.height) //let kx = (CGFloat(edgeSize) * 2.0 + renderSize.width) / layerRect.width //let ky = (CGFloat(edgeSize) * 2.0 + renderSize.height) / layerRect.height //let insetX = CGFloat(edgeSize) / kx //let insetY = CGFloat(edgeSize) / ky let quadVertices: [SIMD2] = [ SIMD2(Float(layerRect.minX), Float(layerRect.minY)), SIMD2(Float(layerRect.maxX), Float(layerRect.minY)), SIMD2(Float(layerRect.minX), Float(layerRect.maxY)), SIMD2(Float(layerRect.maxX), Float(layerRect.minY)), SIMD2(Float(layerRect.minX), Float(layerRect.maxY)), SIMD2(Float(layerRect.maxX), Float(layerRect.maxY)) ].map { v in var v = v v.y = -1.0 + v.y * 2.0 v.x = -1.0 + v.x * 2.0 return v } clearQuads.append(contentsOf: quadVertices) } } if !subjectContext.renderToLayerOperationsGroupedByState.isEmpty || !clearQuads.isEmpty { let renderPass = MTLRenderPassDescriptor() renderPass.colorAttachments[0].texture = surface.texture renderPass.colorAttachments[0].loadAction = .load renderPass.colorAttachments[0].storeAction = .store guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPass) else { return } if !clearQuads.isEmpty { renderEncoder.setRenderPipelineState(self.clearPipelineState) //TODO:use buffer if too many vertices renderEncoder.setVertexBytes(clearQuads, length: 4 * clearQuads.count * 2, index: 0) var renderingBackgroundColor = SIMD4(0.0, 0.0, 0.0, 0.0) renderEncoder.setFragmentBytes(&renderingBackgroundColor, length: 4 * 4, index: 0) renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: clearQuads.count) } for (stateId, renderToLayerOperations) in subjectContext.renderToLayerOperationsGroupedByState { guard let state = self.renderStates[stateId] else { continue } if !renderToLayerOperations.isEmpty { renderEncoder.setRenderPipelineState(state.pipelineState) } for renderToLayerOperation in renderToLayerOperations { guard let layer = renderToLayerOperation.layer else { continue } guard let surfaceAllocation = layer.surfaceAllocation, surfaceAllocation.surfaceId == id else { continue } let subRect = surfaceAllocation.effectivePhase.subRect renderEncoder.setScissorRect(MTLScissorRect(x: Int(subRect.minX), y: surface.height - Int(subRect.maxY), width: Int(subRect.width), height: Int(subRect.height))) renderToLayerOperation.commands(renderEncoder, RenderLayerPlacement(effectiveRect: surfaceAllocation.effectivePhase.renderingRect)) } } renderEncoder.endEncoding() } } } if !subjectContext.freeResourcesOnCompletion.isEmpty { let freeResourcesOnCompletion = subjectContext.freeResourcesOnCompletion commandBuffer.addCompletedHandler { _ in DispatchQueue.main.async { for resource in freeResourcesOnCompletion { resource.free() } } } } var removeSurfaceIds: [Int] = [] for (id, surface) in self.surfaces { if surface.isEmpty { removeSurfaceIds.append(id) } } for id in removeSurfaceIds { self.surfaces.removeValue(forKey: id) } #if DEBUG #if targetEnvironment(simulator) if #available(iOS 13.0, *) { if let drawable = self.layer.nextDrawable() { commandBuffer.present(drawable) } } #else if let drawable = self.layer.nextDrawable() { commandBuffer.present(drawable) } #endif #endif commandBuffer.commit() commandBuffer.waitUntilScheduled() } } public static let shared = MetalEngine() fileprivate let impl: Impl public var rootLayer: CALayer { #if targetEnvironment(simulator) return self.impl._layer #else return self.impl.layer #endif } public var device: MTLDevice { return self.impl.device } private init() { self.impl = Impl(device: MTLCreateSystemDefaultDevice()!)! } public func pooledTexture(spec: TextureSpec) -> PooledTexture { return PooledTexture(device: self.device, spec: spec) } public func pooledBuffer(spec: BufferSpec) -> PooledBuffer { return PooledBuffer(device: self.device, spec: spec) } public func sharedBuffer(spec: BufferSpec) -> SharedBuffer? { return SharedBuffer(device: self.device, spec: spec) } }