From 6c8d0ea9b63275a23ddfb8b79450278ed4859a17 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 23 Jun 2024 16:06:42 +0700 Subject: [PATCH] chore: refactor render pipeline --- Memola.xcodeproj/project.pbxproj | 4 + Memola/Canvas/Abstracts/RenderPass.swift | 2 +- Memola/Canvas/Core/Renderer.swift | 10 +- Memola/Canvas/Elements/Core/Element.swift | 23 +++ .../Canvas/Elements/Core/ElementGroup.swift | 51 ++++++ .../Geometries/Stroke/Core/AnyStroke.swift | 4 + .../Geometries/Stroke/Core/Stroke.swift | 4 +- .../Geometries/Stroke/Strokes/PenStroke.swift | 2 - Memola/Canvas/Elements/Photo/Photo.swift | 6 +- .../Canvas/RenderPasses/CacheRenderPass.swift | 77 ++++----- .../RenderPasses/EraserRenderPass.swift | 85 +++++----- .../RenderPasses/GraphicRenderPass.swift | 119 ++++++-------- .../PhotoBackgroundRenderPass.swift | 4 + .../Canvas/RenderPasses/PhotoRenderPass.swift | 20 ++- .../RenderPasses/StrokeRenderPass.swift | 148 +++++++++++------- .../RenderPasses/ViewPortRenderPass.swift | 10 +- 16 files changed, 325 insertions(+), 244 deletions(-) create mode 100644 Memola/Canvas/Elements/Core/ElementGroup.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index b1ff510..be5032a 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; }; EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; }; EC37FB122C1B2DD90008D976 /* ToolSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC37FB112C1B2DD90008D976 /* ToolSelection.swift */; }; + EC42F7852C25267000E86E96 /* ElementGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC42F7842C25267000E86E96 /* ElementGroup.swift */; }; EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; }; EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; }; EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */; }; @@ -124,6 +125,7 @@ EC3565592BF060D900A4E0BF /* Quad.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Quad.metal; sourceTree = ""; }; EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = ""; }; EC37FB112C1B2DD90008D976 /* ToolSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolSelection.swift; sourceTree = ""; }; + EC42F7842C25267000E86E96 /* ElementGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementGroup.swift; sourceTree = ""; }; EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = ""; }; EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = ""; }; EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = ""; }; @@ -713,6 +715,7 @@ isa = PBXGroup; children = ( ECD12A892C19EFB000B96E12 /* Element.swift */, + EC42F7842C25267000E86E96 /* ElementGroup.swift */, ); path = Core; sourceTree = ""; @@ -906,6 +909,7 @@ ECA738F42BE612A000A4542E /* Array++.swift in Sources */, EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */, ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */, + EC42F7852C25267000E86E96 /* ElementGroup.swift in Sources */, ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */, ECD12A932C1B062000B96E12 /* Photo.metal in Sources */, ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */, diff --git a/Memola/Canvas/Abstracts/RenderPass.swift b/Memola/Canvas/Abstracts/RenderPass.swift index 56eaf20..9830e20 100644 --- a/Memola/Canvas/Abstracts/RenderPass.swift +++ b/Memola/Canvas/Abstracts/RenderPass.swift @@ -12,5 +12,5 @@ protocol RenderPass { var label: String { get } var descriptor: MTLRenderPassDescriptor? { get set } func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) - func draw(on canvas: Canvas, with renderer: Renderer) + func draw(into commandBuffer: MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) } diff --git a/Memola/Canvas/Core/Renderer.swift b/Memola/Canvas/Core/Renderer.swift index 6086dc7..6e36d98 100644 --- a/Memola/Canvas/Core/Renderer.swift +++ b/Memola/Canvas/Core/Renderer.swift @@ -72,13 +72,17 @@ final class Renderer { } func draw(in view: MTKView, on canvas: Canvas) { + guard let commandBuffer = commandQueue.makeCommandBuffer() else { + NSLog("[Memola] - Unable to create command buffer") + return + } if !updatesViewPort { strokeRenderPass.eraserRenderPass = eraserRenderPass graphicRenderPass.photoRenderPass = photoRenderPass graphicRenderPass.strokeRenderPass = strokeRenderPass graphicRenderPass.eraserRenderPass = eraserRenderPass graphicRenderPass.photoBackgroundRenderPass = photoBackgroundRenderPass - graphicRenderPass.draw(on: canvas, with: self) + graphicRenderPass.draw(into: commandBuffer, on: canvas, with: self) } cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture @@ -87,11 +91,11 @@ final class Renderer { cacheRenderPass.eraserRenderPass = eraserRenderPass cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture cacheRenderPass.graphicPipelineState = graphicRenderPass.graphicPipelineState - cacheRenderPass.draw(on: canvas, with: self) + cacheRenderPass.draw(into: commandBuffer, on: canvas, with: self) viewPortRenderPass.descriptor = view.currentRenderPassDescriptor viewPortRenderPass.photoBackgroundTexture = photoBackgroundRenderPass.photoBackgroundTexture viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture - viewPortRenderPass.draw(on: canvas, with: self) + viewPortRenderPass.draw(into: commandBuffer, on: canvas, with: self) } } diff --git a/Memola/Canvas/Elements/Core/Element.swift b/Memola/Canvas/Elements/Core/Element.swift index 5d05736..8ebae9c 100644 --- a/Memola/Canvas/Elements/Core/Element.swift +++ b/Memola/Canvas/Elements/Core/Element.swift @@ -41,6 +41,18 @@ enum Element: Equatable, Comparable { } } + var elementGroupType: ElementGroup.ElementGroupType { + switch self { + case .stroke(let anyStroke): + switch anyStroke.value.style { + case .marker: return .stroke + case .eraser: return .eraser + } + case .photo: + return .photo + } + } + static func < (lhs: Element, rhs: Element) -> Bool { switch (lhs, rhs) { case let (.stroke(leftStroke), .stroke(rightStroke)): @@ -53,4 +65,15 @@ enum Element: Equatable, Comparable { stroke.value.createdAt < photo.createdAt } } + + static func ^= (lhs: Element, rhs: Element) -> Bool { + switch (lhs, rhs) { + case let (.stroke(leftStroke), .stroke(rightStroke)): + leftStroke ^= rightStroke + case let (.photo(leftPhoto), .photo(rightPhoto)): + leftPhoto == rightPhoto + default: + false + } + } } diff --git a/Memola/Canvas/Elements/Core/ElementGroup.swift b/Memola/Canvas/Elements/Core/ElementGroup.swift new file mode 100644 index 0000000..67d6ff5 --- /dev/null +++ b/Memola/Canvas/Elements/Core/ElementGroup.swift @@ -0,0 +1,51 @@ +// +// ElementGroup.swift +// Memola +// +// Created by Dscyre Scotti on 6/21/24. +// + +import Foundation + +class ElementGroup { + var elements: [Element] = [] + var type: ElementGroupType + + init(_ element: Element) { + elements = [element] + type = element.elementGroupType + } + + var isEmpty: Bool { elements.isEmpty } + + func add(_ element: Element) { + elements.append(element) + } + + func isSameElement(_ element: Element) -> Bool { + guard let last = elements.last else { return false } + return element ^= last + } + + func getPenStyle() -> PenStyle? { + if let last = elements.last, case let .stroke(anyStroke) = last { + return anyStroke.value.penStyle + } + return nil + } + + func getPenColor() -> [CGFloat]? { + if let last = elements.last, case let .stroke(anyStroke) = last { + return anyStroke.value.color + } + return nil + } +} + +extension ElementGroup { + enum ElementGroupType { + case stroke + case eraser + case photo + } +} diff --git a/Memola/Canvas/Elements/Geometries/Stroke/Core/AnyStroke.swift b/Memola/Canvas/Elements/Geometries/Stroke/Core/AnyStroke.swift index c80810a..2975394 100644 --- a/Memola/Canvas/Elements/Geometries/Stroke/Core/AnyStroke.swift +++ b/Memola/Canvas/Elements/Geometries/Stroke/Core/AnyStroke.swift @@ -18,6 +18,10 @@ struct AnyStroke: Equatable, Comparable { lhs.value.id == rhs.value.id } + static func ^= (lhs: AnyStroke, rhs: AnyStroke) -> Bool { + lhs.value.color == rhs.value.color && lhs.value.style == rhs.value.style + } + static func < (lhs: AnyStroke, rhs: AnyStroke) -> Bool { lhs.value.createdAt < rhs.value.createdAt } diff --git a/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift b/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift index d6682d0..f7f6892 100644 --- a/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift +++ b/Memola/Canvas/Elements/Geometries/Stroke/Core/Stroke.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -protocol Stroke: AnyObject, Drawable, Hashable, Equatable, Comparable { +protocol Stroke: AnyObject, Drawable, Hashable, Equatable { var id: UUID { get set } var bounds: [CGFloat] { get set } var color: [CGFloat] { get set } @@ -102,8 +102,6 @@ extension Stroke { indexBuffer: indexBuffer, indexBufferOffset: 0 ) - self.vertexBuffer = nil - self.indexBuffer = nil } } diff --git a/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift b/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift index 8252756..db944e0 100644 --- a/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift +++ b/Memola/Canvas/Elements/Geometries/Stroke/Strokes/PenStroke.swift @@ -167,7 +167,5 @@ final class PenStroke: Stroke, @unchecked Sendable { indexBuffer: erasedIndexBuffer, indexBufferOffset: 0 ) - self.erasedIndexBuffer = nil - self.erasedVertexBuffer = nil } } diff --git a/Memola/Canvas/Elements/Photo/Photo.swift b/Memola/Canvas/Elements/Photo/Photo.swift index c153d74..45c720e 100644 --- a/Memola/Canvas/Elements/Photo/Photo.swift +++ b/Memola/Canvas/Elements/Photo/Photo.swift @@ -8,7 +8,7 @@ import MetalKit import Foundation -final class Photo: @unchecked Sendable, Equatable, Comparable { +final class Photo: @unchecked Sendable, Equatable { var id: UUID = UUID() var size: CGSize var origin: CGPoint @@ -93,6 +93,10 @@ extension Photo { static func < (lhs: Photo, rhs: Photo) -> Bool { lhs.createdAt < rhs.createdAt } + + static func ^= (lhs: Photo, rhs: Photo) -> Bool { + lhs == rhs + } } extension Photo { diff --git a/Memola/Canvas/RenderPasses/CacheRenderPass.swift b/Memola/Canvas/RenderPasses/CacheRenderPass.swift index 7c5f152..9086361 100644 --- a/Memola/Canvas/RenderPasses/CacheRenderPass.swift +++ b/Memola/Canvas/RenderPasses/CacheRenderPass.swift @@ -34,53 +34,16 @@ class CacheRenderPass: RenderPass { cacheTexture = Textures.createCacheTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat) } - func draw(on canvas: Canvas, with renderer: Renderer) { - guard let descriptor, let strokeRenderPass, let eraserRenderPass, let photoRenderPass else { return } + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + guard let descriptor else { return } - copyTexture(on: canvas, with: renderer) - - guard let graphicPipelineState else { return } - descriptor.colorAttachments[0].texture = cacheTexture - descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) - descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - descriptor.colorAttachments[0].storeAction = .store - - let graphicContext = canvas.graphicContext - if let element = graphicContext.currentElement { - switch element { - case .stroke(let anyStroke): - let stroke = anyStroke.value - switch stroke.style { - case .eraser: - eraserRenderPass.stroke = stroke - eraserRenderPass.descriptor = descriptor - eraserRenderPass.draw(on: canvas, with: renderer) - case .marker: - canvas.setGraphicRenderType(.inProgress) - strokeRenderPass.stroke = stroke - strokeRenderPass.graphicDescriptor = descriptor - strokeRenderPass.graphicPipelineState = graphicPipelineState - strokeRenderPass.draw(on: canvas, with: renderer) - } - case .photo(let photo): - photoRenderPass.photo = photo - photoRenderPass.descriptor = descriptor - photoRenderPass.draw(on: canvas, with: renderer) - } - clearsTexture = false - } - } - - private func copyTexture(on canvas: Canvas, with renderer: Renderer) { + // MARK: - Copying texture guard let graphicTexture, let cacheTexture else { return } guard let cachePipelineState else { return } - guard let copyCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { + guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return } - guard let computeEncoder = copyCommandBuffer.makeComputeCommandEncoder() else { - return - } - computeEncoder.label = label + computeEncoder.label = "Cache Compute Encoder" computeEncoder.setComputePipelineState(cachePipelineState) computeEncoder.setTexture(graphicTexture, index: 0) @@ -93,6 +56,34 @@ class CacheRenderPass: RenderPass { ) computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) computeEncoder.endEncoding() - copyCommandBuffer.commit() + + // MARK: - Drawing + guard let graphicPipelineState else { return } + descriptor.colorAttachments[0].texture = cacheTexture + descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) + descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load + descriptor.colorAttachments[0].storeAction = .store + + let graphicContext = canvas.graphicContext + if let element = graphicContext.currentElement { + let elementGroup = ElementGroup(element) + switch elementGroup.type { + case .stroke: + canvas.setGraphicRenderType(.inProgress) + strokeRenderPass?.elementGroup = elementGroup + strokeRenderPass?.graphicDescriptor = descriptor + strokeRenderPass?.graphicPipelineState = graphicPipelineState + strokeRenderPass?.draw(into: commandBuffer, on: canvas, with: renderer) + case .eraser: + eraserRenderPass?.elementGroup = elementGroup + eraserRenderPass?.descriptor = descriptor + eraserRenderPass?.draw(into: commandBuffer, on: canvas, with: renderer) + case .photo: + photoRenderPass?.elementGroup = elementGroup + photoRenderPass?.descriptor = descriptor + photoRenderPass?.draw(into: commandBuffer, on: canvas, with: renderer) + } + clearsTexture = false + } } } diff --git a/Memola/Canvas/RenderPasses/EraserRenderPass.swift b/Memola/Canvas/RenderPasses/EraserRenderPass.swift index 34d89cb..226b541 100644 --- a/Memola/Canvas/RenderPasses/EraserRenderPass.swift +++ b/Memola/Canvas/RenderPasses/EraserRenderPass.swift @@ -16,7 +16,7 @@ class EraserRenderPass: RenderPass { var eraserPipelineState: MTLRenderPipelineState? var quadPipelineState: MTLComputePipelineState? - var stroke: (any Stroke)? + var elementGroup: ElementGroup? weak var graphicTexture: MTLTexture? init(renderer: Renderer) { @@ -26,49 +26,24 @@ class EraserRenderPass: RenderPass { func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { } - func draw(on canvas: Canvas, with renderer: Renderer) { + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + guard let elementGroup else { return } guard let descriptor else { return } - generateVertexBuffer(on: canvas, with: renderer) - - guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } - commandBuffer.label = "Eraser Command Buffer" - - guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } - renderEncoder.label = label - - guard let eraserPipelineState else { return } - renderEncoder.setRenderPipelineState(eraserPipelineState) - - canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) - if let stroke = stroke as? PenStroke { - stroke.erase(device: renderer.device, renderEncoder: renderEncoder) - } else { - stroke?.draw(device: renderer.device, renderEncoder: renderEncoder) + // MARK: - Generating vertices + guard !elementGroup.isEmpty, let quadPipelineState else { return } + let eraserStrokes = elementGroup.elements.compactMap { element -> EraserStroke? in + guard case .stroke(let anyStroke) = element else { return nil } + return anyStroke.value as? EraserStroke } + let quads = eraserStrokes.flatMap { $0.quads } + guard !quads.isEmpty else { return } + guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return } - renderEncoder.endEncoding() - commandBuffer.commit() - } + computeEncoder.label = "Quad Compute Encoder" - private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) { - guard let stroke else { return } - let quadCount: Int - var quads: [Quad] - if let stroke = stroke as? PenStroke { - quads = stroke.getAllErasedQuads() - quadCount = quads.endIndex - } else { - quadCount = stroke.quads.endIndex - quads = stroke.quads - } - guard !quads.isEmpty, let quadPipelineState else { return } - guard let quadCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } - guard let computeEncoder = quadCommandBuffer.makeComputeCommandEncoder() else { return } - - computeEncoder.label = "Quad Render Pass" - - let quadBuffer = renderer.device.makeBuffer(bytes: &quads, length: MemoryLayout.stride * quadCount, options: []) + let quadCount = quads.endIndex + let quadBuffer = renderer.device.makeBuffer(bytes: quads, length: MemoryLayout.stride * quadCount, options: []) let indexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * quadCount * 6, options: [.cpuCacheModeWriteCombined]) let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * quadCount * 4, options: [.cpuCacheModeWriteCombined]) @@ -77,20 +52,30 @@ class EraserRenderPass: RenderPass { computeEncoder.setBuffer(indexBuffer, offset: 0, index: 1) computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2) - if let stroke = stroke as? PenStroke { - stroke.erasedIndexBuffer = indexBuffer - stroke.erasedVertexBuffer = vertexBuffer - stroke.erasedQuadCount = quadCount - } else { - stroke.indexBuffer = indexBuffer - stroke.vertexBuffer = vertexBuffer - } - let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1) let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1) computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup) computeEncoder.endEncoding() - quadCommandBuffer.commit() - quadCommandBuffer.waitUntilCompleted() + + // MARK: - Rendering eraser + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } + renderEncoder.label = "Stroke Render Encoder" + + guard let eraserPipelineState else { return } + renderEncoder.setRenderPipelineState(eraserPipelineState) + + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + + if let indexBuffer { + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + renderEncoder.drawIndexedPrimitives( + type: .triangle, + indexCount: quads.endIndex * 6, + indexType: .uint32, + indexBuffer: indexBuffer, + indexBufferOffset: 0 + ) + } + renderEncoder.endEncoding() } } diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index 544ab96..0056320 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -33,94 +33,71 @@ class GraphicRenderPass: RenderPass { clearsTexture = true } - func draw(on canvas: Canvas, with renderer: Renderer) { - guard let strokeRenderPass, let eraserRenderPass, let photoRenderPass, let photoBackgroundRenderPass else { return } - guard let descriptor else { return } - - guard let graphicPipelineState else { return } - guard let graphicTexture else { return } - - descriptor.colorAttachments[0].texture = graphicTexture - descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) - descriptor.colorAttachments[0].storeAction = .store + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + descriptor?.colorAttachments[0].texture = graphicTexture + descriptor?.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) + descriptor?.colorAttachments[0].storeAction = .store let graphicContext = canvas.graphicContext if renderer.redrawsGraphicRender { canvas.setGraphicRenderType(.finished) + var elementGroup: ElementGroup? + let start = Date.now.timeIntervalSince1970 * 1000 for _element in graphicContext.tree.search(box: canvas.bounds.box) { if graphicContext.previousElement == _element || graphicContext.currentElement == _element { continue } - switch _element { - case .stroke(let _stroke): - let stroke = _stroke.value - guard stroke.isVisible(in: canvas.bounds) else { continue } - descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - switch stroke.style { - case .eraser: - eraserRenderPass.stroke = stroke - eraserRenderPass.descriptor = descriptor - eraserRenderPass.draw(on: canvas, with: renderer) - case .marker: - canvas.setGraphicRenderType(.finished) - strokeRenderPass.stroke = stroke - strokeRenderPass.graphicDescriptor = descriptor - strokeRenderPass.graphicPipelineState = graphicPipelineState - strokeRenderPass.draw(on: canvas, with: renderer) + if elementGroup == nil { + let _elementGroup = ElementGroup(_element) + elementGroup = _elementGroup + } else { + guard let _elementGroup = elementGroup else { return } + if _elementGroup.isSameElement(_element) { + _elementGroup.add(_element) + } else { + if let elementGroup { + draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer) + } + let _elementGroup = ElementGroup(_element) + elementGroup = _elementGroup } - clearsTexture = false - case .photo(let photo): - descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - photoRenderPass.photo = photo - photoRenderPass.descriptor = descriptor - photoRenderPass.draw(on: canvas, with: renderer) - - photoBackgroundRenderPass.photo = photo - photoBackgroundRenderPass.clearsTexture = clearsTexture - photoBackgroundRenderPass.draw(on: canvas, with: renderer) - - clearsTexture = false } } + if let elementGroup { + draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer) + } + let end = Date.now.timeIntervalSince1970 * 1000 + NSLog("[Memola] - duration: \(end - start)") renderer.redrawsGraphicRender = false } - if let element = graphicContext.previousElement { - descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - switch element { - case .stroke(let anyStroke): - let stroke = anyStroke.value - switch stroke.style { - case .eraser: - eraserRenderPass.stroke = stroke - eraserRenderPass.descriptor = descriptor - eraserRenderPass.draw(on: canvas, with: renderer) - case .marker: - canvas.setGraphicRenderType(.newlyFinished) - strokeRenderPass.stroke = stroke - strokeRenderPass.graphicDescriptor = descriptor - strokeRenderPass.graphicPipelineState = graphicPipelineState - strokeRenderPass.draw(on: canvas, with: renderer) - } - case .photo(let photo): - photoRenderPass.photo = photo - photoRenderPass.descriptor = descriptor - photoRenderPass.draw(on: canvas, with: renderer) - - photoBackgroundRenderPass.photo = photo - photoBackgroundRenderPass.clearsTexture = clearsTexture - photoBackgroundRenderPass.draw(on: canvas, with: renderer) - } - clearsTexture = false + let elementGroup = ElementGroup(element) + draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer) graphicContext.previousElement = nil } + } - let eraserStrokes = graphicContext.eraserStrokes - for eraserStroke in eraserStrokes { - if eraserStroke.finishesSaving { - graphicContext.eraserStrokes.remove(eraserStroke) - continue - } + private func draw(for elementGroup: ElementGroup, into commandBuffer: MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + switch elementGroup.type { + case .stroke: + descriptor?.colorAttachments[0].loadAction = clearsTexture ? .clear : .load + strokeRenderPass?.elementGroup = elementGroup + strokeRenderPass?.graphicDescriptor = descriptor + strokeRenderPass?.graphicPipelineState = graphicPipelineState + strokeRenderPass?.draw(into: commandBuffer, on: canvas, with: renderer) + clearsTexture = false + case .eraser: + descriptor?.colorAttachments[0].loadAction = clearsTexture ? .clear : .load + eraserRenderPass?.elementGroup = elementGroup + eraserRenderPass?.descriptor = descriptor + eraserRenderPass?.draw(into: commandBuffer, on: canvas, with: renderer) + clearsTexture = false + case .photo: + descriptor?.colorAttachments[0].loadAction = clearsTexture ? .clear : .load + photoRenderPass?.elementGroup = elementGroup + photoRenderPass?.descriptor = descriptor + photoRenderPass?.draw(into: commandBuffer, on: canvas, with: renderer) + clearsTexture = false } } } diff --git a/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift index 33a5a1b..9a1e231 100644 --- a/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift +++ b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift @@ -30,6 +30,10 @@ class PhotoBackgroundRenderPass: RenderPass { photoBackgroundTexture = Textures.createPhotoBackgroundTexture(from: renderer, size: size, pixelFormat: renderer.pixelFormat) } + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + + } + func draw(on canvas: Canvas, with renderer: Renderer) { guard let descriptor else { return } diff --git a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift index 30c03d4..1127990 100644 --- a/Memola/Canvas/RenderPasses/PhotoRenderPass.swift +++ b/Memola/Canvas/RenderPasses/PhotoRenderPass.swift @@ -16,7 +16,7 @@ class PhotoRenderPass: RenderPass { var photoPipelineState: MTLRenderPipelineState? weak var graphicTexture: MTLTexture? - var photo: Photo? + var elementGroup: ElementGroup? init(renderer: Renderer) { photoPipelineState = PipelineStates.createPhotoPipelineState(from: renderer) @@ -24,11 +24,16 @@ class PhotoRenderPass: RenderPass { func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { } - func draw(on canvas: Canvas, with renderer: Renderer) { + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + guard let elementGroup else { return } guard let descriptor else { return } - guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } - commandBuffer.label = "Photo Command Buffer" + guard !elementGroup.isEmpty else { return } + + let photos = elementGroup.elements.compactMap { element -> Photo? in + guard case .photo(let photo) = element else { return nil } + return photo + } guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } renderEncoder.label = label @@ -37,10 +42,11 @@ class PhotoRenderPass: RenderPass { renderEncoder.setRenderPipelineState(photoPipelineState) canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) - photo?.draw(device: renderer.device, renderEncoder: renderEncoder) + + for photo in photos { + photo.draw(device: renderer.device, renderEncoder: renderEncoder) + } renderEncoder.endEncoding() - commandBuffer.commit() - commandBuffer.waitUntilCompleted() } } diff --git a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift index f365459..90061aa 100644 --- a/Memola/Canvas/RenderPasses/StrokeRenderPass.swift +++ b/Memola/Canvas/RenderPasses/StrokeRenderPass.swift @@ -18,7 +18,7 @@ class StrokeRenderPass: RenderPass { var quadPipelineState: MTLComputePipelineState? weak var graphicPipelineState: MTLRenderPipelineState? - var stroke: (any Stroke)? + var elementGroup: ElementGroup? var strokeTexture: MTLTexture? weak var eraserRenderPass: EraserRenderPass? @@ -33,52 +33,27 @@ class StrokeRenderPass: RenderPass { guard size != .zero else { return } strokeTexture = Textures.createStrokeTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat) } - - func draw(on canvas: Canvas, with renderer: Renderer) { + + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { + guard let elementGroup else { return } guard let descriptor else { return } - generateVertexBuffer(on: canvas, with: renderer) - - guard let strokeTexture else { return } - descriptor.colorAttachments[0].texture = strokeTexture - descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) - descriptor.colorAttachments[0].loadAction = .clear - descriptor.colorAttachments[0].storeAction = .store - - guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } - commandBuffer.label = "Stroke Command Buffer" - - guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } - renderEncoder.label = label - - guard let strokePipelineState else { return } - renderEncoder.setRenderPipelineState(strokePipelineState) - - canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) - stroke?.draw(device: renderer.device, renderEncoder: renderEncoder) - renderEncoder.endEncoding() - commandBuffer.commit() - - if let eraserRenderPass, let stroke = stroke as? PenStroke, !stroke.isEmptyErasedQuads { - descriptor.colorAttachments[0].loadAction = .load - eraserRenderPass.stroke = stroke - eraserRenderPass.descriptor = descriptor - eraserRenderPass.draw(on: canvas, with: renderer) + // MARK: - Generating vertices + guard !elementGroup.isEmpty, let quadPipelineState else { return } + let penStrokes = elementGroup.elements.compactMap { element -> PenStroke? in + guard case .stroke(let anyStroke) = element else { return nil } + return anyStroke.value as? PenStroke } + let penStroke = penStrokes.first + let quads = penStrokes.flatMap { $0.quads } + let erasedQuads = Set(penStrokes.flatMap { $0.eraserStrokes }).flatMap { $0.quads } + guard !quads.isEmpty else { return } + guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return } - drawStrokeTexture(on: canvas, with: renderer) - } + computeEncoder.label = "Quad Compute Encoder" - private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) { - guard let stroke, !stroke.isEmpty, let quadPipelineState else { return } - guard let quadCommandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } - guard let computeEncoder = quadCommandBuffer.makeComputeCommandEncoder() else { return } - - computeEncoder.label = "Quad Render Pass" - - let quadCount = stroke.quads.endIndex - var quads = stroke.quads - let quadBuffer = renderer.device.makeBuffer(bytes: &quads, length: MemoryLayout.stride * quadCount, options: []) + let quadCount = quads.endIndex + let quadBuffer = renderer.device.makeBuffer(bytes: quads, length: MemoryLayout.stride * quadCount, options: []) let indexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * quadCount * 6, options: [.cpuCacheModeWriteCombined]) let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * quadCount * 4, options: [.cpuCacheModeWriteCombined]) @@ -87,33 +62,94 @@ class StrokeRenderPass: RenderPass { computeEncoder.setBuffer(indexBuffer, offset: 0, index: 1) computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2) - stroke.indexBuffer = indexBuffer - stroke.vertexBuffer = vertexBuffer - let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1) let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1) computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup) computeEncoder.endEncoding() - quadCommandBuffer.commit() - quadCommandBuffer.waitUntilCompleted() - } - private func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) { - guard let stroke else { return } + // MARK: - Rendering stroke + guard let strokeTexture else { return } + descriptor.colorAttachments[0].texture = strokeTexture + descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) + descriptor.colorAttachments[0].loadAction = .clear + descriptor.colorAttachments[0].storeAction = .store + + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } + renderEncoder.label = "Stroke Render Encoder" + + guard let strokePipelineState else { return } + renderEncoder.setRenderPipelineState(strokePipelineState) + + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + + if let penStyle = penStroke?.penStyle, let indexBuffer { + if penStyle.textureName != nil { + let texture = penStyle.loadTexture(on: renderer.device) + renderEncoder.setFragmentTexture(texture, index: 0) + } + renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + renderEncoder.drawIndexedPrimitives( + type: .triangle, + indexCount: quads.endIndex * 6, + indexType: .uint32, + indexBuffer: indexBuffer, + indexBufferOffset: 0 + ) + } + renderEncoder.endEncoding() + + // MARK: Erasing path + if let eraserPipelineState = eraserRenderPass?.eraserPipelineState, !erasedQuads.isEmpty { + guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return } + + computeEncoder.label = "Erased Quad Compute Encoder" + + let erasedQuadCount = erasedQuads.endIndex + let erasedQuadBuffer = renderer.device.makeBuffer(bytes: erasedQuads, length: MemoryLayout.stride * erasedQuadCount, options: []) + let erasedIndexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * erasedQuadCount * 6, options: [.cpuCacheModeWriteCombined]) + let erasedVertexBuffer = renderer.device.makeBuffer(length: MemoryLayout.stride * erasedQuadCount * 4, options: [.cpuCacheModeWriteCombined]) + + computeEncoder.setComputePipelineState(quadPipelineState) + computeEncoder.setBuffer(erasedQuadBuffer, offset: 0, index: 0) + computeEncoder.setBuffer(erasedIndexBuffer, offset: 0, index: 1) + computeEncoder.setBuffer(erasedVertexBuffer, offset: 0, index: 2) + + let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1) + let numThreadgroups = MTLSize(width: erasedQuadCount + 1, height: 1, depth: 1) + computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup) + computeEncoder.endEncoding() + + descriptor.colorAttachments[0].loadAction = .load + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } + renderEncoder.label = "Stroke Eraser Render Encoder" + + renderEncoder.setRenderPipelineState(eraserPipelineState) + + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + if let erasedIndexBuffer { + renderEncoder.setVertexBuffer(erasedVertexBuffer, offset: 0, index: 0) + renderEncoder.drawIndexedPrimitives( + type: .triangle, + indexCount: erasedQuadCount * 6, + indexType: .uint32, + indexBuffer: erasedIndexBuffer, + indexBufferOffset: 0 + ) + } + renderEncoder.endEncoding() + } + + // MARK: Drawing on graphic texture guard let graphicDescriptor, let graphicPipelineState else { return } - - guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } - commandBuffer.label = "Graphic Command Buffer" guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: graphicDescriptor) else { return } - renderEncoder.label = "Graphic Render Pass" + renderEncoder.label = "Stroke Graphic Render Encoder" renderEncoder.setRenderPipelineState(graphicPipelineState) renderEncoder.setFragmentTexture(strokeTexture, index: 0) - var uniforms = GraphicUniforms(color: stroke.color) + var uniforms = GraphicUniforms(color: elementGroup.getPenColor() ?? []) let uniformsBuffer = renderer.device.makeBuffer(bytes: &uniforms, length: MemoryLayout.size) renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11) canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder) renderEncoder.endEncoding() - commandBuffer.commit() } } diff --git a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift index 0fda41b..c4246c7 100644 --- a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift +++ b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift @@ -29,18 +29,14 @@ class ViewPortRenderPass: RenderPass { func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { } - func draw(on canvas: Canvas, with renderer: Renderer) { + func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) { guard let descriptor else { return } - guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { - return - } - commandBuffer.label = "View Port Command Buffer" guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } - renderEncoder.label = label + renderEncoder.label = "View Port Render Encoder" guard let gridPipelineState else { return } renderEncoder.setRenderPipelineState(gridPipelineState) @@ -64,7 +60,7 @@ class ViewPortRenderPass: RenderPass { } renderEncoder.setRenderPipelineState(viewPortPipelineState) - + renderEncoder.setFragmentTexture(photoBackgroundTexture, index: 0) canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder)