mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-25 10:51:41 +01:00
chore: refactor render pipeline
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
EC35655B2BF0712A00A4E0BF /* Float++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Float++.swift"; sourceTree = "<group>"; };
|
||||
EC37FB112C1B2DD90008D976 /* ToolSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolSelection.swift; sourceTree = "<group>"; };
|
||||
EC42F7842C25267000E86E96 /* ElementGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementGroup.swift; sourceTree = "<group>"; };
|
||||
EC4538882BEBCAE000A86FEC /* Quad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quad.swift; sourceTree = "<group>"; };
|
||||
EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = "<group>"; };
|
||||
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = "<group>"; };
|
||||
@@ -713,6 +715,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECD12A892C19EFB000B96E12 /* Element.swift */,
|
||||
EC42F7842C25267000E86E96 /* ElementGroup.swift */,
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
Memola/Canvas/Elements/Core/ElementGroup.swift
Normal file
51
Memola/Canvas/Elements/Core/ElementGroup.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -167,7 +167,5 @@ final class PenStroke: Stroke, @unchecked Sendable {
|
||||
indexBuffer: erasedIndexBuffer,
|
||||
indexBufferOffset: 0
|
||||
)
|
||||
self.erasedIndexBuffer = nil
|
||||
self.erasedVertexBuffer = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Quad>.stride * quadCount, options: [])
|
||||
let quadCount = quads.endIndex
|
||||
let quadBuffer = renderer.device.makeBuffer(bytes: quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
|
||||
let indexBuffer = renderer.device.makeBuffer(length: MemoryLayout<UInt>.stride * quadCount * 6, options: [.cpuCacheModeWriteCombined])
|
||||
let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout<QuadVertex>.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Quad>.stride * quadCount, options: [])
|
||||
let quadCount = quads.endIndex
|
||||
let quadBuffer = renderer.device.makeBuffer(bytes: quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
|
||||
let indexBuffer = renderer.device.makeBuffer(length: MemoryLayout<UInt>.stride * quadCount * 6, options: [.cpuCacheModeWriteCombined])
|
||||
let vertexBuffer = renderer.device.makeBuffer(length: MemoryLayout<QuadVertex>.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<Quad>.stride * erasedQuadCount, options: [])
|
||||
let erasedIndexBuffer = renderer.device.makeBuffer(length: MemoryLayout<UInt>.stride * erasedQuadCount * 6, options: [.cpuCacheModeWriteCombined])
|
||||
let erasedVertexBuffer = renderer.device.makeBuffer(length: MemoryLayout<QuadVertex>.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<Uniforms>.size)
|
||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||
canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder)
|
||||
renderEncoder.endEncoding()
|
||||
commandBuffer.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user