chore: refactor render pipeline

This commit is contained in:
dscyrescotti
2024-06-23 16:06:42 +07:00
parent 90c87cd4a4
commit 6c8d0ea9b6
16 changed files with 325 additions and 244 deletions

View File

@@ -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 */,

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View 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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -167,7 +167,5 @@ final class PenStroke: Stroke, @unchecked Sendable {
indexBuffer: erasedIndexBuffer,
indexBufferOffset: 0
)
self.erasedIndexBuffer = nil
self.erasedVertexBuffer = nil
}
}

View File

@@ -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 {

View File

@@ -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
}
}
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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 }

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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)