mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-17 21:27:09 +02:00
Merge pull request #48 from dscyrescotti/feature/render-optimization
Optimize renderer frame rate
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; };
|
EC35655A2BF060D900A4E0BF /* Quad.metal in Sources */ = {isa = PBXBuildFile; fileRef = EC3565592BF060D900A4E0BF /* Quad.metal */; };
|
||||||
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; };
|
EC35655C2BF0712A00A4E0BF /* Float++.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC35655B2BF0712A00A4E0BF /* Float++.swift */; };
|
||||||
EC37FB122C1B2DD90008D976 /* ToolSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC37FB112C1B2DD90008D976 /* ToolSelection.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 */; };
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC4538882BEBCAE000A86FEC /* Quad.swift */; };
|
||||||
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
|
EC5050072BF65CED00B4D86E /* PenDropDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */; };
|
||||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC50500C2BF6674400B4D86E /* OnDragViewModifier.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = "<group>"; };
|
||||||
@@ -713,6 +715,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
ECD12A892C19EFB000B96E12 /* Element.swift */,
|
ECD12A892C19EFB000B96E12 /* Element.swift */,
|
||||||
|
EC42F7842C25267000E86E96 /* ElementGroup.swift */,
|
||||||
);
|
);
|
||||||
path = Core;
|
path = Core;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -906,6 +909,7 @@
|
|||||||
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
ECA738F42BE612A000A4542E /* Array++.swift in Sources */,
|
||||||
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
EC4538892BEBCAE000A86FEC /* Quad.swift in Sources */,
|
||||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */,
|
||||||
|
EC42F7852C25267000E86E96 /* ElementGroup.swift in Sources */,
|
||||||
ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */,
|
ECD12A8F2C1AEBA400B96E12 /* Photo.swift in Sources */,
|
||||||
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */,
|
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */,
|
||||||
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
ECA7388F2BE600DA00A4542E /* Grid.metal in Sources */,
|
||||||
|
|||||||
@@ -12,5 +12,5 @@ protocol RenderPass {
|
|||||||
var label: String { get }
|
var label: String { get }
|
||||||
var descriptor: MTLRenderPassDescriptor? { get set }
|
var descriptor: MTLRenderPassDescriptor? { get set }
|
||||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ extension GraphicContext {
|
|||||||
// MARK: - Photo
|
// MARK: - Photo
|
||||||
extension GraphicContext {
|
extension GraphicContext {
|
||||||
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) -> Photo {
|
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) -> Photo {
|
||||||
let size = photoItem.dimension
|
let size = photoItem.getDimension()
|
||||||
let origin = point
|
let origin = point
|
||||||
let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
|
let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
|
||||||
let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark)
|
let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark)
|
||||||
|
|||||||
@@ -72,13 +72,17 @@ final class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func draw(in view: MTKView, on canvas: Canvas) {
|
func draw(in view: MTKView, on canvas: Canvas) {
|
||||||
|
guard let commandBuffer = commandQueue.makeCommandBuffer() else {
|
||||||
|
NSLog("[Memola] - Unable to create command buffer")
|
||||||
|
return
|
||||||
|
}
|
||||||
if !updatesViewPort {
|
if !updatesViewPort {
|
||||||
strokeRenderPass.eraserRenderPass = eraserRenderPass
|
strokeRenderPass.eraserRenderPass = eraserRenderPass
|
||||||
graphicRenderPass.photoRenderPass = photoRenderPass
|
graphicRenderPass.photoRenderPass = photoRenderPass
|
||||||
graphicRenderPass.strokeRenderPass = strokeRenderPass
|
graphicRenderPass.strokeRenderPass = strokeRenderPass
|
||||||
graphicRenderPass.eraserRenderPass = eraserRenderPass
|
graphicRenderPass.eraserRenderPass = eraserRenderPass
|
||||||
graphicRenderPass.photoBackgroundRenderPass = photoBackgroundRenderPass
|
graphicRenderPass.photoBackgroundRenderPass = photoBackgroundRenderPass
|
||||||
graphicRenderPass.draw(on: canvas, with: self)
|
graphicRenderPass.draw(into: commandBuffer, on: canvas, with: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
|
cacheRenderPass.clearsTexture = graphicRenderPass.clearsTexture
|
||||||
@@ -87,11 +91,11 @@ final class Renderer {
|
|||||||
cacheRenderPass.eraserRenderPass = eraserRenderPass
|
cacheRenderPass.eraserRenderPass = eraserRenderPass
|
||||||
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
|
cacheRenderPass.graphicTexture = graphicRenderPass.graphicTexture
|
||||||
cacheRenderPass.graphicPipelineState = graphicRenderPass.graphicPipelineState
|
cacheRenderPass.graphicPipelineState = graphicRenderPass.graphicPipelineState
|
||||||
cacheRenderPass.draw(on: canvas, with: self)
|
cacheRenderPass.draw(into: commandBuffer, on: canvas, with: self)
|
||||||
|
|
||||||
viewPortRenderPass.descriptor = view.currentRenderPassDescriptor
|
viewPortRenderPass.descriptor = view.currentRenderPassDescriptor
|
||||||
viewPortRenderPass.photoBackgroundTexture = photoBackgroundRenderPass.photoBackgroundTexture
|
viewPortRenderPass.photoBackgroundTexture = photoBackgroundRenderPass.photoBackgroundTexture
|
||||||
viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture
|
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 {
|
static func < (lhs: Element, rhs: Element) -> Bool {
|
||||||
switch (lhs, rhs) {
|
switch (lhs, rhs) {
|
||||||
case let (.stroke(leftStroke), .stroke(rightStroke)):
|
case let (.stroke(leftStroke), .stroke(rightStroke)):
|
||||||
@@ -53,4 +65,15 @@ enum Element: Equatable, Comparable {
|
|||||||
stroke.value.createdAt < photo.createdAt
|
stroke.value.createdAt < photo.createdAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func ^= (lhs: Element, rhs: Element) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case let (.stroke(leftStroke), .stroke(rightStroke)):
|
||||||
|
leftStroke ^= rightStroke
|
||||||
|
case (.photo, .photo):
|
||||||
|
true
|
||||||
|
default:
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
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 {
|
static func < (lhs: AnyStroke, rhs: AnyStroke) -> Bool {
|
||||||
lhs.value.createdAt < rhs.value.createdAt
|
lhs.value.createdAt < rhs.value.createdAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import MetalKit
|
import MetalKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol Stroke: AnyObject, Drawable, Hashable, Equatable, Comparable {
|
protocol Stroke: AnyObject, Drawable, Hashable, Equatable {
|
||||||
var id: UUID { get set }
|
var id: UUID { get set }
|
||||||
var bounds: [CGFloat] { get set }
|
var bounds: [CGFloat] { get set }
|
||||||
var color: [CGFloat] { get set }
|
var color: [CGFloat] { get set }
|
||||||
@@ -102,8 +102,6 @@ extension Stroke {
|
|||||||
indexBuffer: indexBuffer,
|
indexBuffer: indexBuffer,
|
||||||
indexBufferOffset: 0
|
indexBufferOffset: 0
|
||||||
)
|
)
|
||||||
self.vertexBuffer = nil
|
|
||||||
self.indexBuffer = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,5 @@ final class PenStroke: Stroke, @unchecked Sendable {
|
|||||||
indexBuffer: erasedIndexBuffer,
|
indexBuffer: erasedIndexBuffer,
|
||||||
indexBufferOffset: 0
|
indexBufferOffset: 0
|
||||||
)
|
)
|
||||||
self.erasedIndexBuffer = nil
|
|
||||||
self.erasedVertexBuffer = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import MetalKit
|
import MetalKit
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
final class Photo: @unchecked Sendable, Equatable, Comparable {
|
final class Photo: @unchecked Sendable, Equatable {
|
||||||
var id: UUID = UUID()
|
var id: UUID = UUID()
|
||||||
var size: CGSize
|
var size: CGSize
|
||||||
var origin: CGPoint
|
var origin: CGPoint
|
||||||
@@ -77,7 +77,6 @@ extension Photo: Drawable {
|
|||||||
renderEncoder.setFragmentTexture(texture, index: 0)
|
renderEncoder.setFragmentTexture(texture, index: 0)
|
||||||
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
renderEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
|
||||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
|
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: vertices.count)
|
||||||
vertexBuffer = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +92,10 @@ extension Photo {
|
|||||||
static func < (lhs: Photo, rhs: Photo) -> Bool {
|
static func < (lhs: Photo, rhs: Photo) -> Bool {
|
||||||
lhs.createdAt < rhs.createdAt
|
lhs.createdAt < rhs.createdAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func ^= (lhs: Photo, rhs: Photo) -> Bool {
|
||||||
|
lhs == rhs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Photo {
|
extension Photo {
|
||||||
|
|||||||
@@ -34,53 +34,16 @@ class CacheRenderPass: RenderPass {
|
|||||||
cacheTexture = Textures.createCacheTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
cacheTexture = Textures.createCacheTexture(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 descriptor, let strokeRenderPass, let eraserRenderPass, let photoRenderPass else { return }
|
guard let descriptor else { return }
|
||||||
|
|
||||||
copyTexture(on: canvas, with: renderer)
|
// MARK: - Copying texture
|
||||||
|
|
||||||
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) {
|
|
||||||
guard let graphicTexture, let cacheTexture else { return }
|
guard let graphicTexture, let cacheTexture else { return }
|
||||||
guard let cachePipelineState else { return }
|
guard let cachePipelineState else { return }
|
||||||
guard let copyCommandBuffer = renderer.commandQueue.makeCommandBuffer() else {
|
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let computeEncoder = copyCommandBuffer.makeComputeCommandEncoder() else {
|
computeEncoder.label = "Cache Compute Encoder"
|
||||||
return
|
|
||||||
}
|
|
||||||
computeEncoder.label = label
|
|
||||||
|
|
||||||
computeEncoder.setComputePipelineState(cachePipelineState)
|
computeEncoder.setComputePipelineState(cachePipelineState)
|
||||||
computeEncoder.setTexture(graphicTexture, index: 0)
|
computeEncoder.setTexture(graphicTexture, index: 0)
|
||||||
@@ -93,6 +56,34 @@ class CacheRenderPass: RenderPass {
|
|||||||
)
|
)
|
||||||
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
|
computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
|
||||||
computeEncoder.endEncoding()
|
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 eraserPipelineState: MTLRenderPipelineState?
|
||||||
var quadPipelineState: MTLComputePipelineState?
|
var quadPipelineState: MTLComputePipelineState?
|
||||||
|
|
||||||
var stroke: (any Stroke)?
|
var elementGroup: ElementGroup?
|
||||||
weak var graphicTexture: MTLTexture?
|
weak var graphicTexture: MTLTexture?
|
||||||
|
|
||||||
init(renderer: Renderer) {
|
init(renderer: Renderer) {
|
||||||
@@ -26,49 +26,24 @@ class EraserRenderPass: RenderPass {
|
|||||||
|
|
||||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
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 descriptor else { return }
|
||||||
|
|
||||||
generateVertexBuffer(on: canvas, with: renderer)
|
// MARK: - Generating vertices
|
||||||
|
guard !elementGroup.isEmpty, let quadPipelineState else { return }
|
||||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
let eraserStrokes = elementGroup.elements.compactMap { element -> EraserStroke? in
|
||||||
commandBuffer.label = "Eraser Command Buffer"
|
guard case .stroke(let anyStroke) = element else { return nil }
|
||||||
|
return anyStroke.value as? EraserStroke
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
let quads = eraserStrokes.flatMap { $0.quads }
|
||||||
|
guard !quads.isEmpty else { return }
|
||||||
|
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else { return }
|
||||||
|
|
||||||
renderEncoder.endEncoding()
|
computeEncoder.label = "Quad Compute Encoder"
|
||||||
commandBuffer.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateVertexBuffer(on canvas: Canvas, with renderer: Renderer) {
|
let quadCount = quads.endIndex
|
||||||
guard let stroke else { return }
|
let quadBuffer = renderer.device.makeBuffer(bytes: quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
|
||||||
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 indexBuffer = renderer.device.makeBuffer(length: MemoryLayout<UInt>.stride * quadCount * 6, options: [.cpuCacheModeWriteCombined])
|
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])
|
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(indexBuffer, offset: 0, index: 1)
|
||||||
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2)
|
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 threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1)
|
||||||
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
|
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
|
||||||
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
|
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
|
||||||
computeEncoder.endEncoding()
|
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,75 @@ class GraphicRenderPass: RenderPass {
|
|||||||
clearsTexture = true
|
clearsTexture = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func draw(on canvas: Canvas, with renderer: Renderer) {
|
func draw(into commandBuffer: any MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) {
|
||||||
guard let strokeRenderPass, let eraserRenderPass, let photoRenderPass, let photoBackgroundRenderPass else { return }
|
descriptor?.colorAttachments[0].texture = graphicTexture
|
||||||
guard let descriptor else { return }
|
descriptor?.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||||
|
descriptor?.colorAttachments[0].storeAction = .store
|
||||||
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
|
|
||||||
|
|
||||||
let graphicContext = canvas.graphicContext
|
let graphicContext = canvas.graphicContext
|
||||||
if renderer.redrawsGraphicRender {
|
if renderer.redrawsGraphicRender {
|
||||||
canvas.setGraphicRenderType(.finished)
|
canvas.setGraphicRenderType(.finished)
|
||||||
|
var elementGroup: ElementGroup?
|
||||||
|
let start = Date.now.timeIntervalSince1970 * 1000
|
||||||
for _element in graphicContext.tree.search(box: canvas.bounds.box) {
|
for _element in graphicContext.tree.search(box: canvas.bounds.box) {
|
||||||
if graphicContext.previousElement == _element || graphicContext.currentElement == _element {
|
if graphicContext.previousElement == _element || graphicContext.currentElement == _element {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch _element {
|
if elementGroup == nil {
|
||||||
case .stroke(let _stroke):
|
let _elementGroup = ElementGroup(_element)
|
||||||
let stroke = _stroke.value
|
elementGroup = _elementGroup
|
||||||
guard stroke.isVisible(in: canvas.bounds) else { continue }
|
} else {
|
||||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
guard let _elementGroup = elementGroup else { return }
|
||||||
switch stroke.style {
|
if _elementGroup.isSameElement(_element) {
|
||||||
case .eraser:
|
_elementGroup.add(_element)
|
||||||
eraserRenderPass.stroke = stroke
|
} else {
|
||||||
eraserRenderPass.descriptor = descriptor
|
if let elementGroup {
|
||||||
eraserRenderPass.draw(on: canvas, with: renderer)
|
draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer)
|
||||||
case .marker:
|
}
|
||||||
canvas.setGraphicRenderType(.finished)
|
let _elementGroup = ElementGroup(_element)
|
||||||
strokeRenderPass.stroke = stroke
|
elementGroup = _elementGroup
|
||||||
strokeRenderPass.graphicDescriptor = descriptor
|
|
||||||
strokeRenderPass.graphicPipelineState = graphicPipelineState
|
|
||||||
strokeRenderPass.draw(on: canvas, with: renderer)
|
|
||||||
}
|
}
|
||||||
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
|
renderer.redrawsGraphicRender = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if let element = graphicContext.previousElement {
|
if let element = graphicContext.previousElement {
|
||||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
let elementGroup = ElementGroup(element)
|
||||||
switch element {
|
draw(for: elementGroup, into: commandBuffer, on: canvas, with: renderer)
|
||||||
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
|
|
||||||
graphicContext.previousElement = nil
|
graphicContext.previousElement = nil
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let eraserStrokes = graphicContext.eraserStrokes
|
private func draw(for elementGroup: ElementGroup, into commandBuffer: MTLCommandBuffer, on canvas: Canvas, with renderer: Renderer) {
|
||||||
for eraserStroke in eraserStrokes {
|
switch elementGroup.type {
|
||||||
if eraserStroke.finishesSaving {
|
case .stroke:
|
||||||
graphicContext.eraserStrokes.remove(eraserStroke)
|
descriptor?.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
continue
|
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)
|
||||||
|
|
||||||
|
photoBackgroundRenderPass?.elementGroup = elementGroup
|
||||||
|
photoBackgroundRenderPass?.clearsTexture = clearsTexture
|
||||||
|
photoBackgroundRenderPass?.draw(into: commandBuffer, on: canvas, with: renderer)
|
||||||
|
clearsTexture = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class PhotoBackgroundRenderPass: RenderPass {
|
|||||||
var photoBackgroundTexture: MTLTexture?
|
var photoBackgroundTexture: MTLTexture?
|
||||||
|
|
||||||
var photo: Photo?
|
var photo: Photo?
|
||||||
|
var elementGroup: ElementGroup?
|
||||||
|
|
||||||
var clearsTexture: Bool = true
|
var clearsTexture: Bool = true
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@ class PhotoBackgroundRenderPass: RenderPass {
|
|||||||
photoBackgroundTexture = Textures.createPhotoBackgroundTexture(from: renderer, size: size, pixelFormat: renderer.pixelFormat)
|
photoBackgroundTexture = Textures.createPhotoBackgroundTexture(from: renderer, size: size, pixelFormat: renderer.pixelFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 descriptor else { return }
|
||||||
|
|
||||||
descriptor.colorAttachments[0].texture = photoBackgroundTexture
|
descriptor.colorAttachments[0].texture = photoBackgroundTexture
|
||||||
@@ -38,20 +40,25 @@ class PhotoBackgroundRenderPass: RenderPass {
|
|||||||
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load
|
||||||
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
||||||
|
|
||||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
guard !elementGroup.isEmpty else { return }
|
||||||
commandBuffer.label = "Photo Background Command Buffer"
|
|
||||||
|
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 }
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
|
||||||
renderEncoder.label = label
|
renderEncoder.label = "Photo Background Render Encoder"
|
||||||
|
|
||||||
guard let photoBackgroundPipelineState else { return }
|
guard let photoBackgroundPipelineState else { return }
|
||||||
renderEncoder.setRenderPipelineState(photoBackgroundPipelineState)
|
renderEncoder.setRenderPipelineState(photoBackgroundPipelineState)
|
||||||
|
|
||||||
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
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()
|
renderEncoder.endEncoding()
|
||||||
commandBuffer.commit()
|
|
||||||
commandBuffer.waitUntilCompleted()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class PhotoRenderPass: RenderPass {
|
|||||||
var photoPipelineState: MTLRenderPipelineState?
|
var photoPipelineState: MTLRenderPipelineState?
|
||||||
weak var graphicTexture: MTLTexture?
|
weak var graphicTexture: MTLTexture?
|
||||||
|
|
||||||
var photo: Photo?
|
var elementGroup: ElementGroup?
|
||||||
|
|
||||||
init(renderer: Renderer) {
|
init(renderer: Renderer) {
|
||||||
photoPipelineState = PipelineStates.createPhotoPipelineState(from: renderer)
|
photoPipelineState = PipelineStates.createPhotoPipelineState(from: renderer)
|
||||||
@@ -24,23 +24,29 @@ class PhotoRenderPass: RenderPass {
|
|||||||
|
|
||||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
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 descriptor else { return }
|
||||||
|
|
||||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return }
|
guard !elementGroup.isEmpty else { return }
|
||||||
commandBuffer.label = "Photo Command Buffer"
|
|
||||||
|
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 }
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return }
|
||||||
renderEncoder.label = label
|
renderEncoder.label = "Photo Render Encoder"
|
||||||
|
|
||||||
guard let photoPipelineState else { return }
|
guard let photoPipelineState else { return }
|
||||||
renderEncoder.setRenderPipelineState(photoPipelineState)
|
renderEncoder.setRenderPipelineState(photoPipelineState)
|
||||||
|
|
||||||
canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder)
|
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()
|
renderEncoder.endEncoding()
|
||||||
commandBuffer.commit()
|
|
||||||
commandBuffer.waitUntilCompleted()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class StrokeRenderPass: RenderPass {
|
|||||||
var quadPipelineState: MTLComputePipelineState?
|
var quadPipelineState: MTLComputePipelineState?
|
||||||
weak var graphicPipelineState: MTLRenderPipelineState?
|
weak var graphicPipelineState: MTLRenderPipelineState?
|
||||||
|
|
||||||
var stroke: (any Stroke)?
|
var elementGroup: ElementGroup?
|
||||||
var strokeTexture: MTLTexture?
|
var strokeTexture: MTLTexture?
|
||||||
|
|
||||||
weak var eraserRenderPass: EraserRenderPass?
|
weak var eraserRenderPass: EraserRenderPass?
|
||||||
@@ -34,51 +34,26 @@ class StrokeRenderPass: RenderPass {
|
|||||||
strokeTexture = Textures.createStrokeTexture(from: renderer, size: size, pixelFormat: view.colorPixelFormat)
|
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 }
|
guard let descriptor else { return }
|
||||||
|
|
||||||
generateVertexBuffer(on: canvas, with: renderer)
|
// MARK: - Generating vertices
|
||||||
|
guard !elementGroup.isEmpty, let quadPipelineState else { return }
|
||||||
guard let strokeTexture else { return }
|
let penStrokes = elementGroup.elements.compactMap { element -> PenStroke? in
|
||||||
descriptor.colorAttachments[0].texture = strokeTexture
|
guard case .stroke(let anyStroke) = element else { return nil }
|
||||||
descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0)
|
return anyStroke.value as? PenStroke
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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) {
|
let quadCount = quads.endIndex
|
||||||
guard let stroke, !stroke.isEmpty, let quadPipelineState else { return }
|
let quadBuffer = renderer.device.makeBuffer(bytes: quads, length: MemoryLayout<Quad>.stride * quadCount, options: [])
|
||||||
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 indexBuffer = renderer.device.makeBuffer(length: MemoryLayout<UInt>.stride * quadCount * 6, options: [.cpuCacheModeWriteCombined])
|
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])
|
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(indexBuffer, offset: 0, index: 1)
|
||||||
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2)
|
computeEncoder.setBuffer(vertexBuffer, offset: 0, index: 2)
|
||||||
|
|
||||||
stroke.indexBuffer = indexBuffer
|
|
||||||
stroke.vertexBuffer = vertexBuffer
|
|
||||||
|
|
||||||
let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1)
|
let threadsPerGroup = MTLSize(width: 1, height: 1, depth: 1)
|
||||||
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
|
let numThreadgroups = MTLSize(width: quadCount + 1, height: 1, depth: 1)
|
||||||
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
|
computeEncoder.dispatchThreadgroups(numThreadgroups, threadsPerThreadgroup: threadsPerGroup)
|
||||||
computeEncoder.endEncoding()
|
computeEncoder.endEncoding()
|
||||||
quadCommandBuffer.commit()
|
|
||||||
quadCommandBuffer.waitUntilCompleted()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func drawStrokeTexture(on canvas: Canvas, with renderer: Renderer) {
|
// MARK: - Rendering stroke
|
||||||
guard let stroke else { return }
|
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 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 }
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: graphicDescriptor) else { return }
|
||||||
renderEncoder.label = "Graphic Render Pass"
|
renderEncoder.label = "Stroke Graphic Render Encoder"
|
||||||
renderEncoder.setRenderPipelineState(graphicPipelineState)
|
renderEncoder.setRenderPipelineState(graphicPipelineState)
|
||||||
|
|
||||||
renderEncoder.setFragmentTexture(strokeTexture, index: 0)
|
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)
|
let uniformsBuffer = renderer.device.makeBuffer(bytes: &uniforms, length: MemoryLayout<Uniforms>.size)
|
||||||
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
renderEncoder.setVertexBuffer(uniformsBuffer, offset: 0, index: 11)
|
||||||
canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder)
|
canvas.renderGraphic(device: renderer.device, renderEncoder: renderEncoder)
|
||||||
renderEncoder.endEncoding()
|
renderEncoder.endEncoding()
|
||||||
commandBuffer.commit()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,18 +29,14 @@ class ViewPortRenderPass: RenderPass {
|
|||||||
|
|
||||||
func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { }
|
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 {
|
guard let descriptor else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
commandBuffer.label = "View Port Command Buffer"
|
|
||||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
|
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderEncoder.label = label
|
renderEncoder.label = "View Port Render Encoder"
|
||||||
|
|
||||||
guard let gridPipelineState else { return }
|
guard let gridPipelineState else { return }
|
||||||
renderEncoder.setRenderPipelineState(gridPipelineState)
|
renderEncoder.setRenderPipelineState(gridPipelineState)
|
||||||
|
|||||||
@@ -114,33 +114,35 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) {
|
func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) {
|
||||||
guard let resizedImage = resizePhoto(of: image) else { return }
|
guard let (resizedImage, dimension) = resizePhoto(of: image) else { return }
|
||||||
let photoItem = bookmarkPhoto(of: resizedImage, with: canvasID)
|
let photoItem = bookmarkPhoto(of: resizedImage, in: dimension, with: canvasID)
|
||||||
withAnimation {
|
withAnimation {
|
||||||
selectedPhotoItem = photoItem
|
selectedPhotoItem = photoItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func resizePhoto(of image: UIImage) -> UIImage? {
|
private func resizePhoto(of image: UIImage) -> (UIImage, CGSize)? {
|
||||||
let targetSize = CGSize(width: 768, height: 768)
|
let targetSize = CGSize(width: 512, height: 512)
|
||||||
let size = image.size
|
let size = image.size
|
||||||
let widthRatio = targetSize.width / size.width
|
let widthRatio = targetSize.width / size.width
|
||||||
let heightRatio = targetSize.height / size.height
|
let heightRatio = targetSize.height / size.height
|
||||||
let newSize = CGSize(
|
let dimension = CGSize(
|
||||||
width: size.width * min(widthRatio, heightRatio),
|
width: size.width * min(widthRatio, heightRatio),
|
||||||
height: size.height * min(widthRatio, heightRatio)
|
height: size.height * min(widthRatio, heightRatio)
|
||||||
)
|
)
|
||||||
let rect = CGRect(origin: .zero, size: newSize)
|
let rect = CGRect(origin: .zero, size: targetSize)
|
||||||
|
|
||||||
UIGraphicsBeginImageContextWithOptions(newSize, true, 1.0)
|
UIGraphicsBeginImageContextWithOptions(targetSize, true, 1.0)
|
||||||
image.draw(in: rect)
|
image.draw(in: rect)
|
||||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
|
|
||||||
return newImage
|
guard let newImage else { return nil }
|
||||||
|
|
||||||
|
return (newImage, dimension)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bookmarkPhoto(of image: UIImage, with canvasID: NSManagedObjectID) -> PhotoItem? {
|
private func bookmarkPhoto(of image: UIImage, in dimension: CGSize, with canvasID: NSManagedObjectID) -> PhotoItem? {
|
||||||
guard let data = image.jpegData(compressionQuality: 1) else { return nil }
|
guard let data = image.jpegData(compressionQuality: 1) else { return nil }
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||||
@@ -167,7 +169,7 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
var photoBookmark: PhotoItem?
|
var photoBookmark: PhotoItem?
|
||||||
do {
|
do {
|
||||||
let bookmark = try file.bookmarkData(options: .minimalBookmark)
|
let bookmark = try file.bookmarkData(options: .minimalBookmark)
|
||||||
photoBookmark = PhotoItem(id: file, image: image, bookmark: bookmark)
|
photoBookmark = PhotoItem(id: file, image: image, dimension: dimension, bookmark: bookmark)
|
||||||
} catch {
|
} catch {
|
||||||
NSLog("[Memola] - \(error.localizedDescription)")
|
NSLog("[Memola] - \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DrawingView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateDrawableSize(with size: CGSize) {
|
func updateDrawableSize(with size: CGSize) {
|
||||||
renderView.drawableSize = size.multiply(by: 2.5)
|
renderView.drawableSize = size.multiply(by: 2.6)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import Foundation
|
|||||||
struct PhotoItem: Identifiable, Equatable {
|
struct PhotoItem: Identifiable, Equatable {
|
||||||
var id: URL
|
var id: URL
|
||||||
let image: UIImage
|
let image: UIImage
|
||||||
|
let dimension: CGSize
|
||||||
let bookmark: Data
|
let bookmark: Data
|
||||||
|
|
||||||
var dimension: CGSize {
|
func getDimension() -> CGSize {
|
||||||
let size = image.size
|
let maxSize = max(dimension.width, dimension.height)
|
||||||
let maxSize = max(size.width, size.height)
|
let width = dimension.width * 100 / maxSize
|
||||||
let width = size.width * 128 / maxSize
|
let height = dimension.height * 100 / maxSize
|
||||||
let height = size.height * 128 / maxSize
|
|
||||||
return CGSize(width: width, height: height)
|
return CGSize(width: width, height: height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user