diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 4eb8613..b1ff510 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 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 */; }; + EC5D40812C21CE270067F090 /* PhotoBackgroundRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */; }; EC5E83902BFDB69C00261D9C /* MovingAverage.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */; }; EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; @@ -127,6 +128,7 @@ EC5050062BF65CED00B4D86E /* PenDropDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDropDelegate.swift; sourceTree = ""; }; EC50500C2BF6674400B4D86E /* OnDragViewModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDragViewModifier.swift; sourceTree = ""; }; EC50500E2BF670EA00B4D86E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoBackgroundRenderPass.swift; sourceTree = ""; }; EC5E838F2BFDB69C00261D9C /* MovingAverage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovingAverage.swift; sourceTree = ""; }; EC7F6BE82BE5E6E300A34A7B /* Memola.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Memola.app; sourceTree = BUILT_PRODUCTS_DIR; }; EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; @@ -624,6 +626,7 @@ ECA738DF2BE610B900A4542E /* EraserRenderPass.swift */, ECA738E12BE610D000A4542E /* GraphicRenderPass.swift */, ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */, + EC5D40802C21CE270067F090 /* PhotoBackgroundRenderPass.swift */, ); path = RenderPasses; sourceTree = ""; @@ -843,6 +846,7 @@ files = ( ECA738B02BE60D0B00A4542E /* CanvasViewController.swift in Sources */, ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */, + EC5D40812C21CE270067F090 /* PhotoBackgroundRenderPass.swift in Sources */, ECA738E42BE6110800A4542E /* Drawable.swift in Sources */, ECA738AD2BE60CC600A4542E /* DrawingView.swift in Sources */, EC1B783D2BFA0AC9005A34E2 /* Toolbar.swift in Sources */, diff --git a/Memola/Canvas/Core/Renderer.swift b/Memola/Canvas/Core/Renderer.swift index 85c77dc..6086dc7 100644 --- a/Memola/Canvas/Core/Renderer.swift +++ b/Memola/Canvas/Core/Renderer.swift @@ -37,6 +37,9 @@ final class Renderer { lazy var viewPortRenderPass: ViewPortRenderPass = { ViewPortRenderPass(renderer: self) }() + lazy var photoBackgroundRenderPass: PhotoBackgroundRenderPass = { + PhotoBackgroundRenderPass(renderer: self) + }() init(canvasView: MTKView) { guard let device = MTLCreateSystemDefaultDevice() else { @@ -59,6 +62,7 @@ final class Renderer { func resize(on view: MTKView, to size: CGSize) { if !updatesViewPort { + photoBackgroundRenderPass.resize(on: view, to: size, with: self) strokeRenderPass.resize(on: view, to: size, with: self) graphicRenderPass.resize(on: view, to: size, with: self) cacheRenderPass.resize(on: view, to: size, with: self) @@ -73,6 +77,7 @@ final class Renderer { graphicRenderPass.photoRenderPass = photoRenderPass graphicRenderPass.strokeRenderPass = strokeRenderPass graphicRenderPass.eraserRenderPass = eraserRenderPass + graphicRenderPass.photoBackgroundRenderPass = photoBackgroundRenderPass graphicRenderPass.draw(on: canvas, with: self) } @@ -85,6 +90,7 @@ final class Renderer { cacheRenderPass.draw(on: canvas, with: self) viewPortRenderPass.descriptor = view.currentRenderPassDescriptor + viewPortRenderPass.photoBackgroundTexture = photoBackgroundRenderPass.photoBackgroundTexture viewPortRenderPass.cacheTexture = cacheRenderPass.cacheTexture viewPortRenderPass.draw(on: canvas, with: self) } diff --git a/Memola/Canvas/Core/Textures.swift b/Memola/Canvas/Core/Textures.swift index 24a9268..838ab4a 100644 --- a/Memola/Canvas/Core/Textures.swift +++ b/Memola/Canvas/Core/Textures.swift @@ -116,4 +116,27 @@ class Textures { texture.label = "Stroke Texture" return texture } + + static func createPhotoBackgroundTexture( + from renderer: Renderer, + size: CGSize, + pixelFormat: MTLPixelFormat? = nil + ) -> MTLTexture? { + let width = Int(size.width) + let height = Int(size.height) + guard width > 0, height > 0 else { return nil } + let descriptor = MTLTextureDescriptor.texture2DDescriptor( + pixelFormat: pixelFormat ?? renderer.pixelFormat, + width: width, + height: height, + mipmapped: false + ) + descriptor.storageMode = .shared + descriptor.usage = [.shaderRead, .renderTarget, .shaderWrite] + guard let texture = renderer.device.makeTexture(descriptor: descriptor) else { + return nil + } + texture.label = "Photo Background Texture" + return texture + } } diff --git a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift index ee3a2d8..544ab96 100644 --- a/Memola/Canvas/RenderPasses/GraphicRenderPass.swift +++ b/Memola/Canvas/RenderPasses/GraphicRenderPass.swift @@ -18,6 +18,7 @@ class GraphicRenderPass: RenderPass { weak var photoRenderPass: PhotoRenderPass? weak var strokeRenderPass: StrokeRenderPass? weak var eraserRenderPass: EraserRenderPass? + weak var photoBackgroundRenderPass: PhotoBackgroundRenderPass? var clearsTexture: Bool = true @@ -33,7 +34,7 @@ class GraphicRenderPass: RenderPass { } func draw(on canvas: Canvas, with renderer: Renderer) { - guard let strokeRenderPass, let eraserRenderPass, let photoRenderPass else { return } + guard let strokeRenderPass, let eraserRenderPass, let photoRenderPass, let photoBackgroundRenderPass else { return } guard let descriptor else { return } guard let graphicPipelineState else { return } @@ -55,7 +56,6 @@ class GraphicRenderPass: RenderPass { let stroke = _stroke.value guard stroke.isVisible(in: canvas.bounds) else { continue } descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - clearsTexture = false switch stroke.style { case .eraser: eraserRenderPass.stroke = stroke @@ -68,12 +68,18 @@ class GraphicRenderPass: RenderPass { strokeRenderPass.graphicPipelineState = graphicPipelineState strokeRenderPass.draw(on: canvas, with: renderer) } + clearsTexture = false case .photo(let photo): descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - clearsTexture = false 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 } } renderer.redrawsGraphicRender = false @@ -81,7 +87,6 @@ class GraphicRenderPass: RenderPass { if let element = graphicContext.previousElement { descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load - clearsTexture = false switch element { case .stroke(let anyStroke): let stroke = anyStroke.value @@ -101,7 +106,12 @@ class GraphicRenderPass: RenderPass { 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 } diff --git a/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift new file mode 100644 index 0000000..c3427c9 --- /dev/null +++ b/Memola/Canvas/RenderPasses/PhotoBackgroundRenderPass.swift @@ -0,0 +1,56 @@ +// +// PhotoBackgroundRenderPass.swift +// Memola +// +// Created by Dscyre Scotti on 6/18/24. +// + +import MetalKit +import Foundation + +class PhotoBackgroundRenderPass: RenderPass { + var label: String = "Photo Background Render Pass" + + var descriptor: MTLRenderPassDescriptor? + + var photoBackgroundPipelineState: MTLRenderPipelineState? + + var photoBackgroundTexture: MTLTexture? + + var photo: Photo? + + var clearsTexture: Bool = true + + init(renderer: Renderer) { + descriptor = MTLRenderPassDescriptor() + photoBackgroundPipelineState = PipelineStates.createPhotoPipelineState(from: renderer) + } + + func resize(on view: MTKView, to size: CGSize, with renderer: Renderer) { + photoBackgroundTexture = Textures.createPhotoBackgroundTexture(from: renderer, size: size, pixelFormat: renderer.pixelFormat) + } + + func draw(on canvas: Canvas, with renderer: Renderer) { + guard let descriptor else { return } + + descriptor.colorAttachments[0].texture = photoBackgroundTexture + descriptor.colorAttachments[0].storeAction = .store + descriptor.colorAttachments[0].loadAction = clearsTexture ? .clear : .load + descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 0) + + guard let commandBuffer = renderer.commandQueue.makeCommandBuffer() else { return } + commandBuffer.label = "Photo Background Command Buffer" + + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } + renderEncoder.label = label + + guard let photoBackgroundPipelineState else { return } + renderEncoder.setRenderPipelineState(photoBackgroundPipelineState) + + canvas.setUniformsBuffer(device: renderer.device, renderEncoder: renderEncoder) + photo?.draw(device: renderer.device, renderEncoder: renderEncoder) + + renderEncoder.endEncoding() + commandBuffer.commit() + } +} diff --git a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift index 8589bd2..8f56ae9 100644 --- a/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift +++ b/Memola/Canvas/RenderPasses/ViewPortRenderPass.swift @@ -17,6 +17,7 @@ class ViewPortRenderPass: RenderPass { var viewPortUpdatePipelineState: MTLRenderPipelineState? weak var cacheTexture: MTLTexture? + weak var photoBackgroundTexture: MTLTexture? weak var view: MTKView? @@ -51,6 +52,10 @@ class ViewPortRenderPass: RenderPass { } renderEncoder.setRenderPipelineState(viewPortUpdatePipelineState) + + renderEncoder.setFragmentTexture(photoBackgroundTexture, index: 0) + canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder) + renderEncoder.setFragmentTexture(cacheTexture, index: 0) canvas.renderViewPortUpdate(device: renderer.device, renderEncoder: renderEncoder) } else { @@ -59,6 +64,10 @@ class ViewPortRenderPass: RenderPass { } renderEncoder.setRenderPipelineState(viewPortPipelineState) + + renderEncoder.setFragmentTexture(photoBackgroundTexture, index: 0) + canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder) + renderEncoder.setFragmentTexture(cacheTexture, index: 0) canvas.renderViewPort(device: renderer.device, renderEncoder: renderEncoder) }