From 721f9133a5a0dd56cd6ca924c9a870d40acf899a Mon Sep 17 00:00:00 2001 From: Josh Bernfeld Date: Mon, 2 Apr 2018 02:11:23 -0700 Subject: [PATCH 1/5] [Pipeline] Added remove(target:) support, fixed exc_bad_access crashes and framebuffers not being properly locked, improved PictureInput, improved RenderView, etc. --- README.md | 6 +- .../FilterShowcase/FilterOperations.swift | 4 +- .../FilterShowcase.xcodeproj/project.pbxproj | 6 +- .../FilterShowcaseSwift/AppDelegate.swift | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../FilterDisplayViewController.swift | 2 +- .../FilterListViewController.swift | 0 .../FilterShowcaseSwift/Info.plist | 0 .../project.pbxproj | 6 +- .../SimpleImageFilter/AppDelegate.swift | 0 .../Base.lproj/LaunchScreen.storyboard | 0 .../Base.lproj/Main.storyboard | 0 .../SimpleImageFilter/Info.plist | 0 .../SimpleImageFilter/ViewController.swift | 17 ++- .../project.pbxproj | 6 +- .../Base.lproj/Main.storyboard | 22 ++-- framework/Source/BasicOperation.swift | 43 +++--- framework/Source/Framebuffer.swift | 27 ++-- framework/Source/FramebufferCache.swift | 11 +- framework/Source/ImageOrientation.swift | 4 +- framework/Source/OperationGroup.swift | 4 +- .../Source/Operations/AmatorkaFilter.swift | 7 +- .../Operations/AverageColorExtractor.swift | 2 +- .../AverageLuminanceExtractor.swift | 2 +- framework/Source/Operations/Crop.swift | 4 +- framework/Source/Operations/Histogram.swift | 2 +- .../Source/Operations/LanczosResampling.swift | 2 +- .../Source/Operations/MissEtikateFilter.swift | 7 +- framework/Source/Operations/MotionBlur.swift | 2 +- framework/Source/Operations/Sharpen.swift | 2 +- .../Source/Operations/SoftElegance.swift | 9 +- .../Operations/TransformOperation.swift | 6 +- framework/Source/Pipeline.swift | 46 +++++-- .../Source/TextureSamplingOperation.swift | 2 +- framework/Source/TwoStageOperation.swift | 2 +- framework/Source/iOS/PictureInput.swift | 123 +++++++++++------- framework/Source/iOS/PictureOutput.swift | 8 +- framework/Source/iOS/RenderView.swift | 94 ++++++++----- 39 files changed, 308 insertions(+), 170 deletions(-) mode change 100644 => 100755 examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj mode change 100644 => 100755 examples/iOS/FilterShowcase/FilterShowcaseSwift/AppDelegate.swift mode change 100644 => 100755 examples/iOS/FilterShowcase/FilterShowcaseSwift/Base.lproj/LaunchScreen.storyboard mode change 100644 => 100755 examples/iOS/FilterShowcase/FilterShowcaseSwift/Base.lproj/Main.storyboard mode change 100644 => 100755 examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterDisplayViewController.swift mode change 100644 => 100755 examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterListViewController.swift mode change 100644 => 100755 examples/iOS/FilterShowcase/FilterShowcaseSwift/Info.plist mode change 100644 => 100755 examples/iOS/SimpleImageFilter/SimpleImageFilter.xcodeproj/project.pbxproj mode change 100644 => 100755 examples/iOS/SimpleImageFilter/SimpleImageFilter/AppDelegate.swift mode change 100644 => 100755 examples/iOS/SimpleImageFilter/SimpleImageFilter/Base.lproj/LaunchScreen.storyboard mode change 100644 => 100755 examples/iOS/SimpleImageFilter/SimpleImageFilter/Base.lproj/Main.storyboard mode change 100644 => 100755 examples/iOS/SimpleImageFilter/SimpleImageFilter/Info.plist mode change 100644 => 100755 examples/iOS/SimpleImageFilter/SimpleImageFilter/ViewController.swift mode change 100644 => 100755 framework/Source/ImageOrientation.swift mode change 100644 => 100755 framework/Source/OperationGroup.swift mode change 100644 => 100755 framework/Source/Operations/AverageLuminanceExtractor.swift mode change 100644 => 100755 framework/Source/Operations/Crop.swift mode change 100644 => 100755 framework/Source/Operations/Sharpen.swift mode change 100644 => 100755 framework/Source/TextureSamplingOperation.swift mode change 100644 => 100755 framework/Source/iOS/PictureOutput.swift diff --git a/README.md b/README.md index 89c2d95d..0798adda 100755 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ There are a few different ways to approach filtering an image. The easiest are t ```swift let testImage = UIImage(named:"WID-small.jpg")! let toonFilter = SmoothToonFilter() -let filteredImage = testImage.filterWithOperation(toonFilter) +let filteredImage = try! testImage.filterWithOperation(toonFilter) ``` for a more complex pipeline: @@ -161,7 +161,7 @@ for a more complex pipeline: let testImage = UIImage(named:"WID-small.jpg")! let toonFilter = SmoothToonFilter() let luminanceFilter = Luminance() -let filteredImage = testImage.filterWithPipeline{input, output in +let filteredImage = try! testImage.filterWithPipeline{input, output in input --> toonFilter --> luminanceFilter --> output } ``` @@ -173,7 +173,7 @@ Both of these convenience methods wrap several operations. To feed a picture int ```swift let toonFilter = SmoothToonFilter() let testImage = UIImage(named:"WID-small.jpg")! -let pictureInput = PictureInput(image:testImage) +let pictureInput = try! PictureInput(image:testImage) let pictureOutput = PictureOutput() pictureOutput.imageAvailableCallback = {image in // Do something with image diff --git a/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift b/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift index 234cdfd7..3440e17f 100755 --- a/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift +++ b/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift @@ -172,7 +172,7 @@ let filterOperations: Array = [ sliderUpdateCallback: nil, filterOperationType:.custom(filterSetupFunction:{(camera, filter, outputView) in let castFilter = filter as! Luminance - let maskImage = PictureInput(imageName:"Mask.png") + let maskImage = try! PictureInput(imageName:"Mask.png") castFilter.drawUnmodifiedImageOutsideOfMask = false castFilter.mask = maskImage maskImage.processImage() @@ -739,7 +739,7 @@ let filterOperations: Array = [ let blendFilter = AlphaBlend() blendFilter.mix = 1.0 - let inputImage = PictureInput(imageName:blendImageName) + let inputImage = try! PictureInput(imageName:blendImageName) inputImage --> blendFilter camera --> castFilter --> blendFilter --> outputView diff --git a/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj b/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 5358e00c..8f8405f8 --- a/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj +++ b/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj @@ -157,7 +157,7 @@ isa = PBXGroup; children = ( BC9E364D1E525A3200B8604F /* GPUImage.framework */, - BC9E364F1E525A3200B8604F /* GPUImage.xctest */, + BC9E364F1E525A3200B8604F /* GPUImageTests_macOS.xctest */, BC9E36511E525A3200B8604F /* GPUImage.framework */, BC9E36531E525A3200B8604F /* GPUImageTests_iOS.xctest */, ); @@ -246,10 +246,10 @@ remoteRef = BC9E364C1E525A3200B8604F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - BC9E364F1E525A3200B8604F /* GPUImage.xctest */ = { + BC9E364F1E525A3200B8604F /* GPUImageTests_macOS.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; - path = GPUImage.xctest; + path = GPUImageTests_macOS.xctest; remoteRef = BC9E364E1E525A3200B8604F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; diff --git a/examples/iOS/FilterShowcase/FilterShowcaseSwift/AppDelegate.swift b/examples/iOS/FilterShowcase/FilterShowcaseSwift/AppDelegate.swift old mode 100644 new mode 100755 diff --git a/examples/iOS/FilterShowcase/FilterShowcaseSwift/Base.lproj/LaunchScreen.storyboard b/examples/iOS/FilterShowcase/FilterShowcaseSwift/Base.lproj/LaunchScreen.storyboard old mode 100644 new mode 100755 diff --git a/examples/iOS/FilterShowcase/FilterShowcaseSwift/Base.lproj/Main.storyboard b/examples/iOS/FilterShowcase/FilterShowcaseSwift/Base.lproj/Main.storyboard old mode 100644 new mode 100755 diff --git a/examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterDisplayViewController.swift b/examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterDisplayViewController.swift old mode 100644 new mode 100755 index b98b61bb..00323443 --- a/examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterDisplayViewController.swift +++ b/examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterDisplayViewController.swift @@ -45,7 +45,7 @@ class FilterDisplayViewController: UIViewController, UISplitViewControllerDelega currentFilterConfiguration.filter.addTarget(view) case .blend: videoCamera.addTarget(currentFilterConfiguration.filter) - self.blendImage = PictureInput(imageName:blendImageName) + self.blendImage = try? PictureInput(imageName:blendImageName) self.blendImage?.addTarget(currentFilterConfiguration.filter) self.blendImage?.processImage() currentFilterConfiguration.filter.addTarget(view) diff --git a/examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterListViewController.swift b/examples/iOS/FilterShowcase/FilterShowcaseSwift/FilterListViewController.swift old mode 100644 new mode 100755 diff --git a/examples/iOS/FilterShowcase/FilterShowcaseSwift/Info.plist b/examples/iOS/FilterShowcase/FilterShowcaseSwift/Info.plist old mode 100644 new mode 100755 diff --git a/examples/iOS/SimpleImageFilter/SimpleImageFilter.xcodeproj/project.pbxproj b/examples/iOS/SimpleImageFilter/SimpleImageFilter.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 6fb5dd86..2f0afb57 --- a/examples/iOS/SimpleImageFilter/SimpleImageFilter.xcodeproj/project.pbxproj +++ b/examples/iOS/SimpleImageFilter/SimpleImageFilter.xcodeproj/project.pbxproj @@ -94,7 +94,7 @@ isa = PBXGroup; children = ( BC9E36601E525B5B00B8604F /* GPUImage.framework */, - BC9E36621E525B5B00B8604F /* GPUImage.xctest */, + BC9E36621E525B5B00B8604F /* GPUImageTests_macOS.xctest */, BC9E36641E525B5B00B8604F /* GPUImage.framework */, BC9E36661E525B5B00B8604F /* GPUImageTests_iOS.xctest */, ); @@ -219,10 +219,10 @@ remoteRef = BC9E365F1E525B5B00B8604F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - BC9E36621E525B5B00B8604F /* GPUImage.xctest */ = { + BC9E36621E525B5B00B8604F /* GPUImageTests_macOS.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; - path = GPUImage.xctest; + path = GPUImageTests_macOS.xctest; remoteRef = BC9E36611E525B5B00B8604F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; diff --git a/examples/iOS/SimpleImageFilter/SimpleImageFilter/AppDelegate.swift b/examples/iOS/SimpleImageFilter/SimpleImageFilter/AppDelegate.swift old mode 100644 new mode 100755 diff --git a/examples/iOS/SimpleImageFilter/SimpleImageFilter/Base.lproj/LaunchScreen.storyboard b/examples/iOS/SimpleImageFilter/SimpleImageFilter/Base.lproj/LaunchScreen.storyboard old mode 100644 new mode 100755 diff --git a/examples/iOS/SimpleImageFilter/SimpleImageFilter/Base.lproj/Main.storyboard b/examples/iOS/SimpleImageFilter/SimpleImageFilter/Base.lproj/Main.storyboard old mode 100644 new mode 100755 diff --git a/examples/iOS/SimpleImageFilter/SimpleImageFilter/Info.plist b/examples/iOS/SimpleImageFilter/SimpleImageFilter/Info.plist old mode 100644 new mode 100755 diff --git a/examples/iOS/SimpleImageFilter/SimpleImageFilter/ViewController.swift b/examples/iOS/SimpleImageFilter/SimpleImageFilter/ViewController.swift old mode 100644 new mode 100755 index 7980b1dc..a2c52890 --- a/examples/iOS/SimpleImageFilter/SimpleImageFilter/ViewController.swift +++ b/examples/iOS/SimpleImageFilter/SimpleImageFilter/ViewController.swift @@ -14,7 +14,14 @@ class ViewController: UIViewController { // Filtering image for saving let testImage = UIImage(named:"WID-small.jpg")! let toonFilter = SmoothToonFilter() - let filteredImage = testImage.filterWithOperation(toonFilter) + + let filteredImage:UIImage + do { + filteredImage = try testImage.filterWithOperation(toonFilter) + } catch { + print("Couldn't filter image with error: \(error)") + return + } let pngImage = UIImagePNGRepresentation(filteredImage)! do { @@ -25,8 +32,14 @@ class ViewController: UIViewController { print("Couldn't write to file with error: \(error)") } + // Filtering image for display - picture = PictureInput(image:UIImage(named:"WID-small.jpg")!) + do { + picture = try PictureInput(image:UIImage(named:"WID-small.jpg")!) + } catch { + print("Couldn't create PictureInput with error: \(error)") + return + } filter = SaturationAdjustment() picture --> filter --> renderView picture.processImage() diff --git a/examples/iOS/SimpleMovieFilter/SimpleMovieFilter.xcodeproj/project.pbxproj b/examples/iOS/SimpleMovieFilter/SimpleMovieFilter.xcodeproj/project.pbxproj index d570a4a3..a3b959f0 100644 --- a/examples/iOS/SimpleMovieFilter/SimpleMovieFilter.xcodeproj/project.pbxproj +++ b/examples/iOS/SimpleMovieFilter/SimpleMovieFilter.xcodeproj/project.pbxproj @@ -92,7 +92,7 @@ isa = PBXGroup; children = ( BC9E36731E525BC000B8604F /* GPUImage.framework */, - BC9E36751E525BC000B8604F /* GPUImage.xctest */, + BC9E36751E525BC000B8604F /* GPUImageTests_macOS.xctest */, BC9E36771E525BC000B8604F /* GPUImage.framework */, BC9E36791E525BC000B8604F /* GPUImageTests_iOS.xctest */, ); @@ -209,10 +209,10 @@ remoteRef = BC9E36721E525BC000B8604F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; - BC9E36751E525BC000B8604F /* GPUImage.xctest */ = { + BC9E36751E525BC000B8604F /* GPUImageTests_macOS.xctest */ = { isa = PBXReferenceProxy; fileType = wrapper.cfbundle; - path = GPUImage.xctest; + path = GPUImageTests_macOS.xctest; remoteRef = BC9E36741E525BC000B8604F /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; diff --git a/examples/iOS/SimpleMovieFilter/SimpleMovieFilter/Base.lproj/Main.storyboard b/examples/iOS/SimpleMovieFilter/SimpleMovieFilter/Base.lproj/Main.storyboard index 67c04ee8..53ade93e 100644 --- a/examples/iOS/SimpleMovieFilter/SimpleMovieFilter/Base.lproj/Main.storyboard +++ b/examples/iOS/SimpleMovieFilter/SimpleMovieFilter/Base.lproj/Main.storyboard @@ -1,8 +1,12 @@ - - + + + + + - + + @@ -14,16 +18,18 @@ - + - - - + + + - + + + diff --git a/framework/Source/BasicOperation.swift b/framework/Source/BasicOperation.swift index 60ad430d..511dbcc9 100755 --- a/framework/Source/BasicOperation.swift +++ b/framework/Source/BasicOperation.swift @@ -2,12 +2,12 @@ import Foundation public func defaultVertexShaderForInputs(_ inputCount:UInt) -> String { switch inputCount { - case 1: return OneInputVertexShader - case 2: return TwoInputVertexShader - case 3: return ThreeInputVertexShader - case 4: return FourInputVertexShader - case 5: return FiveInputVertexShader - default: return OneInputVertexShader + case 1: return OneInputVertexShader + case 2: return TwoInputVertexShader + case 3: return ThreeInputVertexShader + case 4: return FourInputVertexShader + case 5: return FiveInputVertexShader + default: return OneInputVertexShader } } @@ -35,14 +35,14 @@ open class BasicOperation: ImageProcessingOperation { } public var activatePassthroughOnNextFrame:Bool = false public var uniformSettings = ShaderUniformSettings() - + // MARK: - // MARK: Internal - + public let targets = TargetContainer() public let sources = SourceContainer() var shader:ShaderProgram - var inputFramebuffers = [UInt:Framebuffer]() + public var inputFramebuffers = [UInt:Framebuffer]() var renderFramebuffer:Framebuffer! var outputFramebuffer:Framebuffer { get { return renderFramebuffer } } let usesAspectRatio:Bool @@ -51,7 +51,7 @@ open class BasicOperation: ImageProcessingOperation { // MARK: - // MARK: Initialization and teardown - + public init(shader:ShaderProgram, numberOfInputs:UInt = 1) { self.maximumInputs = numberOfInputs self.shader = shader @@ -64,7 +64,7 @@ open class BasicOperation: ImageProcessingOperation { self.shader = compiledShader usesAspectRatio = shader.uniformIndex("aspectRatio") != nil } - + public init(vertexShaderFile:URL? = nil, fragmentShaderFile:URL, numberOfInputs:UInt = 1, operationName:String = #file) throws { let compiledShader:ShaderProgram if let vertexShaderFile = vertexShaderFile { @@ -78,7 +78,7 @@ open class BasicOperation: ImageProcessingOperation { } deinit { - debugPrint("Deallocating operation: \(self)") + //debugPrint("Deallocating operation: \(self)") } // MARK: - @@ -89,7 +89,7 @@ open class BasicOperation: ImageProcessingOperation { previousFramebuffer.unlock() } inputFramebuffers[fromSourceIndex] = framebuffer - + guard (!activatePassthroughOnNextFrame) else { // Use this to allow a bootstrap of cyclical processing, like with a low pass filter activatePassthroughOnNextFrame = false updateTargetsWithFramebuffer(framebuffer) @@ -103,7 +103,7 @@ open class BasicOperation: ImageProcessingOperation { } } - func renderFrame() { + open func renderFrame() { renderFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation:.portrait, size:sizeOfInitialStageBasedOnFramebuffer(inputFramebuffers[0]!), stencil:mask != nil) let textureProperties = initialTextureProperties() @@ -123,7 +123,7 @@ open class BasicOperation: ImageProcessingOperation { } } - func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { + open func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { renderQuadWithShader(shader, uniformSettings:uniformSettings, vertexBufferObject:sharedImageProcessingContext.standardImageVBO, inputTextures:textureProperties) releaseIncomingFramebuffers() } @@ -175,7 +175,7 @@ open class BasicOperation: ImageProcessingOperation { return inputTextureProperties } - func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { + open func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { if usesAspectRatio { let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait) uniformSettings["aspectRatio"] = inputFramebuffer.aspectRatioForRotation(outputRotation) @@ -183,11 +183,10 @@ open class BasicOperation: ImageProcessingOperation { } public func transmitPreviousImage(to target:ImageConsumer, atIndex:UInt) { - sharedImageProcessingContext.runOperationAsynchronously{ - guard let renderFramebuffer = self.renderFramebuffer, (!renderFramebuffer.timingStyle.isTransient()) else { return } - - renderFramebuffer.lock() - target.newFramebufferAvailable(renderFramebuffer, fromSourceIndex:atIndex) - } + //guard let renderFramebuffer = self.renderFramebuffer, (!renderFramebuffer.timingStyle.isTransient()) else { return } + + //renderFramebuffer.lock() + //target.newFramebufferAvailable(renderFramebuffer, fromSourceIndex:atIndex) } } + diff --git a/framework/Source/Framebuffer.swift b/framework/Source/Framebuffer.swift index 4af692a1..d0ecf354 100755 --- a/framework/Source/Framebuffer.swift +++ b/framework/Source/Framebuffer.swift @@ -15,6 +15,7 @@ import Glibc #endif import Foundation +import AVFoundation // TODO: Add a good lookup table to this to allow for detailed error messages struct FramebufferCreationError:Error { @@ -32,7 +33,7 @@ public enum FramebufferTimingStyle { } } - var timestamp:Timestamp? { + public var timestamp:Timestamp? { get { switch self { case .stillImage: return nil @@ -45,7 +46,7 @@ public enum FramebufferTimingStyle { public class Framebuffer { public var timingStyle:FramebufferTimingStyle = .stillImage public var orientation:ImageOrientation - + public let texture:GLuint let framebuffer:GLuint? let stencilBuffer:GLuint? @@ -57,7 +58,7 @@ public class Framebuffer { let hash:Int64 let textureOverride:Bool - weak var context:OpenGLContext? + unowned var context:OpenGLContext public init(context:OpenGLContext, orientation:ImageOrientation, size:GLSize, textureOnly:Bool = false, minFilter:Int32 = GL_LINEAR, magFilter:Int32 = GL_LINEAR, wrapS:Int32 = GL_CLAMP_TO_EDGE, wrapT:Int32 = GL_CLAMP_TO_EDGE, internalFormat:Int32 = GL_RGBA, format:Int32 = GL_BGRA, type:Int32 = GL_UNSIGNED_BYTE, stencil:Bool = false, overriddenTexture:GLuint? = nil) throws { self.context = context @@ -96,22 +97,28 @@ public class Framebuffer { deinit { if (!textureOverride) { var mutableTexture = texture - glDeleteTextures(1, &mutableTexture) - debugPrint("Delete texture at size: \(size)") + context.runOperationAsynchronously { + glDeleteTextures(1, &mutableTexture) + } + //debugPrint("Delete texture at size: \(size)") } if let framebuffer = framebuffer { var mutableFramebuffer = framebuffer - glDeleteFramebuffers(1, &mutableFramebuffer) + context.runOperationAsynchronously { + glDeleteFramebuffers(1, &mutableFramebuffer) + } } if let stencilBuffer = stencilBuffer { var mutableStencil = stencilBuffer - glDeleteRenderbuffers(1, &mutableStencil) + context.runOperationAsynchronously { + glDeleteRenderbuffers(1, &mutableStencil) + } } } - func sizeForTargetOrientation(_ targetOrientation:ImageOrientation) -> GLSize { + public func sizeForTargetOrientation(_ targetOrientation:ImageOrientation) -> GLSize { if self.orientation.rotationNeededForOrientation(targetOrientation).flipsDimensions() { return GLSize(width:size.height, height:size.width) } else { @@ -119,7 +126,7 @@ public class Framebuffer { } } - func aspectRatioForRotation(_ rotation:Rotation) -> Float { + public func aspectRatioForRotation(_ rotation:Rotation) -> Float { if rotation.flipsDimensions() { return Float(size.width) / Float(size.height) } else { @@ -144,7 +151,7 @@ public class Framebuffer { } public func texturePropertiesForOutputRotation(_ rotation:Rotation) -> InputTextureProperties { - return InputTextureProperties(textureVBO:context!.textureVBO(for:rotation), texture:texture) + return InputTextureProperties(textureVBO:context.textureVBO(for:rotation), texture:texture) } public func texturePropertiesForTargetOrientation(_ targetOrientation:ImageOrientation) -> InputTextureProperties { diff --git a/framework/Source/FramebufferCache.swift b/framework/Source/FramebufferCache.swift index f62575c7..d4153f4d 100755 --- a/framework/Source/FramebufferCache.swift +++ b/framework/Source/FramebufferCache.swift @@ -25,13 +25,18 @@ public class FramebufferCache { public func requestFramebufferWithProperties(orientation:ImageOrientation, size:GLSize, textureOnly:Bool = false, minFilter:Int32 = GL_LINEAR, magFilter:Int32 = GL_LINEAR, wrapS:Int32 = GL_CLAMP_TO_EDGE, wrapT:Int32 = GL_CLAMP_TO_EDGE, internalFormat:Int32 = GL_RGBA, format:Int32 = GL_BGRA, type:Int32 = GL_UNSIGNED_BYTE, stencil:Bool = false) -> Framebuffer { let hash = hashForFramebufferWithProperties(orientation:orientation, size:size, textureOnly:textureOnly, minFilter:minFilter, magFilter:magFilter, wrapS:wrapS, wrapT:wrapT, internalFormat:internalFormat, format:format, type:type, stencil:stencil) let framebuffer:Framebuffer + + if(framebufferCache.count > 20) { + print("WARNING: Runaway framebuffer cache with size: \(framebufferCache.count)") + } + if ((framebufferCache[hash]?.count ?? -1) > 0) { -// print("Restoring previous framebuffer") + //print("Restoring previous framebuffer") framebuffer = framebufferCache[hash]!.removeLast() framebuffer.orientation = orientation } else { do { - debugPrint("Generating new framebuffer at size: \(size)") + //debugPrint("Generating new framebuffer at size: \(size)") framebuffer = try Framebuffer(context:context, orientation:orientation, size:size, textureOnly:textureOnly, minFilter:minFilter, magFilter:magFilter, wrapS:wrapS, wrapT:wrapT, internalFormat:internalFormat, format:format, type:type, stencil:stencil) framebuffer.cache = self @@ -47,7 +52,7 @@ public class FramebufferCache { } func returnToCache(_ framebuffer:Framebuffer) { -// print("Returning to cache: \(framebuffer)") + //print("Returning to cache: \(framebuffer)") context.runOperationSynchronously{ if (self.framebufferCache[framebuffer.hash] != nil) { self.framebufferCache[framebuffer.hash]!.append(framebuffer) diff --git a/framework/Source/ImageOrientation.swift b/framework/Source/ImageOrientation.swift old mode 100644 new mode 100755 index 59013707..7371c0d1 --- a/framework/Source/ImageOrientation.swift +++ b/framework/Source/ImageOrientation.swift @@ -4,7 +4,7 @@ public enum ImageOrientation { case landscapeLeft case landscapeRight - func rotationNeededForOrientation(_ targetOrientation:ImageOrientation) -> Rotation { + public func rotationNeededForOrientation(_ targetOrientation:ImageOrientation) -> Rotation { switch (self, targetOrientation) { case (.portrait, .portrait), (.portraitUpsideDown, .portraitUpsideDown), (.landscapeLeft, .landscapeLeft), (.landscapeRight, .landscapeRight): return .noRotation case (.portrait, .portraitUpsideDown): return .rotate180 @@ -33,7 +33,7 @@ public enum Rotation { case rotateClockwiseAndFlipVertically case rotateClockwiseAndFlipHorizontally - func flipsDimensions() -> Bool { + public func flipsDimensions() -> Bool { switch self { case .noRotation, .rotate180, .flipHorizontally, .flipVertically: return false case .rotateCounterclockwise, .rotateClockwise, .rotateClockwiseAndFlipVertically, .rotateClockwiseAndFlipHorizontally: return true diff --git a/framework/Source/OperationGroup.swift b/framework/Source/OperationGroup.swift old mode 100644 new mode 100755 index 8e6f5675..634c7219 --- a/framework/Source/OperationGroup.swift +++ b/framework/Source/OperationGroup.swift @@ -1,6 +1,6 @@ open class OperationGroup: ImageProcessingOperation { - let inputImageRelay = ImageRelay() - let outputImageRelay = ImageRelay() + public let inputImageRelay = ImageRelay() + public let outputImageRelay = ImageRelay() public var sources:SourceContainer { get { return inputImageRelay.sources } } public var targets:TargetContainer { get { return outputImageRelay.targets } } diff --git a/framework/Source/Operations/AmatorkaFilter.swift b/framework/Source/Operations/AmatorkaFilter.swift index fc569eaf..2a6d507e 100755 --- a/framework/Source/Operations/AmatorkaFilter.swift +++ b/framework/Source/Operations/AmatorkaFilter.swift @@ -11,7 +11,12 @@ public class AmatorkaFilter: LookupFilter { public override init() { super.init() - ({lookupImage = PictureInput(imageName:"lookup_amatorka.png")})() + do { + try ({lookupImage = try PictureInput(imageName:"lookup_amatorka.png")})() + } + catch { + print("ERROR: Unable to create PictureInput \(error)") + } ({intensity = 1.0})() } } diff --git a/framework/Source/Operations/AverageColorExtractor.swift b/framework/Source/Operations/AverageColorExtractor.swift index 1e4911ab..ccf86608 100755 --- a/framework/Source/Operations/AverageColorExtractor.swift +++ b/framework/Source/Operations/AverageColorExtractor.swift @@ -22,7 +22,7 @@ public class AverageColorExtractor: BasicOperation { super.init(vertexShader:AverageColorVertexShader, fragmentShader:AverageColorFragmentShader) } - override func renderFrame() { + override open func renderFrame() { averageColorBySequentialReduction(inputFramebuffer:inputFramebuffers[0]!, shader:shader, extractAverageOperation:extractAverageColorFromFramebuffer) releaseIncomingFramebuffers() } diff --git a/framework/Source/Operations/AverageLuminanceExtractor.swift b/framework/Source/Operations/AverageLuminanceExtractor.swift old mode 100644 new mode 100755 index 57e22336..a7d4915e --- a/framework/Source/Operations/AverageLuminanceExtractor.swift +++ b/framework/Source/Operations/AverageLuminanceExtractor.swift @@ -19,7 +19,7 @@ public class AverageLuminanceExtractor: BasicOperation { super.init(vertexShader:AverageColorVertexShader, fragmentShader:AverageLuminanceFragmentShader) } - override func renderFrame() { + override open func renderFrame() { // Reduce to luminance before passing into the downsampling // TODO: Combine this with the first stage of the downsampling by doing reduction here let luminancePassShader = crashOnShaderCompileFailure("AverageLuminance"){try sharedImageProcessingContext.programForVertexShader(defaultVertexShaderForInputs(1), fragmentShader:LuminanceFragmentShader)} diff --git a/framework/Source/Operations/Crop.swift b/framework/Source/Operations/Crop.swift old mode 100644 new mode 100755 index eb452c53..f0113da8 --- a/framework/Source/Operations/Crop.swift +++ b/framework/Source/Operations/Crop.swift @@ -1,7 +1,7 @@ // TODO: Have this adjust in real time to changing crop sizes // TODO: Verify at all orientations -public class Crop: BasicOperation { +open class Crop: BasicOperation { public var cropSizeInPixels: Size? public var locationOfCropInPixels: Position? @@ -9,7 +9,7 @@ public class Crop: BasicOperation { super.init(fragmentShader:PassthroughFragmentShader, numberOfInputs:1) } - override func renderFrame() { + override open func renderFrame() { let inputFramebuffer:Framebuffer = inputFramebuffers[0]! let inputSize = inputFramebuffer.sizeForTargetOrientation(.portrait) diff --git a/framework/Source/Operations/Histogram.swift b/framework/Source/Operations/Histogram.swift index 95274542..6fa49515 100755 --- a/framework/Source/Operations/Histogram.swift +++ b/framework/Source/Operations/Histogram.swift @@ -50,7 +50,7 @@ public class Histogram: BasicOperation { } } - override func renderFrame() { + override open func renderFrame() { let inputSize = sizeOfInitialStageBasedOnFramebuffer(inputFramebuffers[0]!) let inputByteSize = Int(inputSize.width * inputSize.height * 4) let data = UnsafeMutablePointer.allocate(capacity:inputByteSize) diff --git a/framework/Source/Operations/LanczosResampling.swift b/framework/Source/Operations/LanczosResampling.swift index fb6c3af2..a13dd45e 100644 --- a/framework/Source/Operations/LanczosResampling.swift +++ b/framework/Source/Operations/LanczosResampling.swift @@ -3,7 +3,7 @@ public class LanczosResampling: BasicOperation { super.init(vertexShader:LanczosResamplingVertexShader, fragmentShader:LanczosResamplingFragmentShader) } - override func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { + override public func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait) // Shrink the vertical component of the first stage diff --git a/framework/Source/Operations/MissEtikateFilter.swift b/framework/Source/Operations/MissEtikateFilter.swift index 16d60168..49f38efd 100755 --- a/framework/Source/Operations/MissEtikateFilter.swift +++ b/framework/Source/Operations/MissEtikateFilter.swift @@ -10,7 +10,12 @@ public class MissEtikateFilter: LookupFilter { public override init() { super.init() - ({lookupImage = PictureInput(imageName:"lookup_miss_etikate.png")})() + do { + try ({lookupImage = try PictureInput(imageName:"lookup_miss_etikate.png")})() + } + catch { + print("ERROR: Unable to create PictureInput \(error)") + } } } #endif diff --git a/framework/Source/Operations/MotionBlur.swift b/framework/Source/Operations/MotionBlur.swift index 6c76fe29..8b3f0607 100755 --- a/framework/Source/Operations/MotionBlur.swift +++ b/framework/Source/Operations/MotionBlur.swift @@ -12,7 +12,7 @@ public class MotionBlur: BasicOperation { super.init(vertexShader:MotionBlurVertexShader, fragmentShader:MotionBlurFragmentShader, numberOfInputs:1) } - override func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { + override open func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait) let texelSize = inputFramebuffer.texelSize(for:outputRotation) diff --git a/framework/Source/Operations/Sharpen.swift b/framework/Source/Operations/Sharpen.swift old mode 100644 new mode 100755 index 3ba518dc..770d579d --- a/framework/Source/Operations/Sharpen.swift +++ b/framework/Source/Operations/Sharpen.swift @@ -8,7 +8,7 @@ public class Sharpen: BasicOperation { ({sharpness = 0.0})() } - override func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { + override open func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait) let texelSize = overriddenTexelSize ?? inputFramebuffer.texelSize(for:outputRotation) uniformSettings["texelWidth"] = texelSize.width diff --git a/framework/Source/Operations/SoftElegance.swift b/framework/Source/Operations/SoftElegance.swift index 50e85ef3..8cec3af0 100755 --- a/framework/Source/Operations/SoftElegance.swift +++ b/framework/Source/Operations/SoftElegance.swift @@ -9,8 +9,13 @@ public class SoftElegance: OperationGroup { super.init() self.configureGroup{input, output in - self.lookup1.lookupImage = PictureInput(imageName:"lookup_soft_elegance_1.png") - self.lookup2.lookupImage = PictureInput(imageName:"lookup_soft_elegance_2.png") + do { + self.lookup1.lookupImage = try PictureInput(imageName:"lookup_soft_elegance_1.png") + self.lookup2.lookupImage = try PictureInput(imageName:"lookup_soft_elegance_2.png") + } + catch { + print("ERROR: Unable to create PictureInput \(error)") + } self.gaussianBlur.blurRadiusInPixels = 10.0 self.alphaBlend.mix = 0.14 diff --git a/framework/Source/Operations/TransformOperation.swift b/framework/Source/Operations/TransformOperation.swift index 6b87a377..dcd700c7 100644 --- a/framework/Source/Operations/TransformOperation.swift +++ b/framework/Source/Operations/TransformOperation.swift @@ -12,7 +12,7 @@ #endif #endif -public class TransformOperation: BasicOperation { +open class TransformOperation: BasicOperation { public var transform:Matrix4x4 = Matrix4x4.identity { didSet { uniformSettings["transformMatrix"] = transform } } var normalizedImageVertices:[GLfloat]! @@ -22,12 +22,12 @@ public class TransformOperation: BasicOperation { ({transform = Matrix4x4.identity})() } - override func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { + override open func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { renderQuadWithShader(shader, uniformSettings:uniformSettings, vertices:normalizedImageVertices, inputTextures:textureProperties) releaseIncomingFramebuffers() } - override func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { + override open func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait) let aspectRatio = inputFramebuffer.aspectRatioForRotation(outputRotation) let orthoMatrix = orthographicMatrix(-1.0, right:1.0, bottom:-1.0 * aspectRatio, top:1.0 * aspectRatio, near:-1.0, far:1.0) diff --git a/framework/Source/Pipeline.swift b/framework/Source/Pipeline.swift index 65611ea7..911317e1 100755 --- a/framework/Source/Pipeline.swift +++ b/framework/Source/Pipeline.swift @@ -35,10 +35,14 @@ public extension ImageSource { if let targetIndex = atTargetIndex { target.setSource(self, atIndex:targetIndex) targets.append(target, indexAtTarget:targetIndex) - transmitPreviousImage(to:target, atIndex:targetIndex) + sharedImageProcessingContext.runOperationAsynchronously { + self.transmitPreviousImage(to:target, atIndex:targetIndex) + } } else if let indexAtTarget = target.addSource(self) { targets.append(target, indexAtTarget:indexAtTarget) - transmitPreviousImage(to:target, atIndex:indexAtTarget) + sharedImageProcessingContext.runOperationAsynchronously { + self.transmitPreviousImage(to:target, atIndex:indexAtTarget) + } } else { debugPrint("Warning: tried to add target beyond target's input capacity") } @@ -51,17 +55,31 @@ public extension ImageSource { targets.removeAll() } + public func remove(_ target:ImageConsumer) { + for (testTarget, index) in targets { + if(target === testTarget) { + target.removeSourceAtIndex(index) + targets.remove(target) + } + } + } + public func updateTargetsWithFramebuffer(_ framebuffer:Framebuffer) { - if targets.count == 0 { // Deal with the case where no targets are attached by immediately returning framebuffer to cache + var foundTargets = [(ImageConsumer, UInt)]() + for target in targets { + foundTargets.append(target) + } + + if foundTargets.count == 0 { // Deal with the case where no targets are attached by immediately returning framebuffer to cache framebuffer.lock() framebuffer.unlock() } else { // Lock first for each output, to guarantee proper ordering on multi-output operations - for _ in targets { + for _ in foundTargets { framebuffer.lock() } } - for (target, index) in targets { + for (target, index) in foundTargets { target.newFramebufferAvailable(framebuffer, fromSourceIndex:index) } } @@ -91,8 +109,10 @@ class WeakImageConsumer { } public class TargetContainer:Sequence { - var targets = [WeakImageConsumer]() - var count:Int { get {return targets.count}} + private var targets = [WeakImageConsumer]() + + private var count:Int { get { return targets.count } } + #if !os(Linux) let dispatchQueue = DispatchQueue(label:"com.sunsetlakesoftware.GPUImage.targetContainerQueue", attributes: []) #endif @@ -158,10 +178,20 @@ public class TargetContainer:Sequence { } #endif } + + public func remove(_ target:ImageConsumer) { + #if os(Linux) + self.targets = self.targets.filter { $0.value !== target } + #else + dispatchQueue.async{ + self.targets = self.targets.filter { $0.value !== target } + } + #endif + } } public class SourceContainer { - var sources:[UInt:ImageSource] = [:] + public var sources:[UInt:ImageSource] = [:] public init() { } diff --git a/framework/Source/TextureSamplingOperation.swift b/framework/Source/TextureSamplingOperation.swift old mode 100644 new mode 100755 index 19026fd0..60fc3451 --- a/framework/Source/TextureSamplingOperation.swift +++ b/framework/Source/TextureSamplingOperation.swift @@ -5,7 +5,7 @@ open class TextureSamplingOperation: BasicOperation { super.init(vertexShader:vertexShader, fragmentShader:fragmentShader, numberOfInputs:numberOfInputs) } - override func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { + override open func configureFramebufferSpecificUniforms(_ inputFramebuffer:Framebuffer) { let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait) let texelSize = overriddenTexelSize ?? inputFramebuffer.texelSize(for:outputRotation) uniformSettings["texelWidth"] = texelSize.width diff --git a/framework/Source/TwoStageOperation.swift b/framework/Source/TwoStageOperation.swift index 1afd416c..7f65850d 100644 --- a/framework/Source/TwoStageOperation.swift +++ b/framework/Source/TwoStageOperation.swift @@ -5,7 +5,7 @@ open class TwoStageOperation: BasicOperation { var downsamplingFactor:Float? - override func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { + override open func internalRenderFunction(_ inputFramebuffer:Framebuffer, textureProperties:[InputTextureProperties]) { let outputRotation = overriddenOutputRotation ?? inputFramebuffer.orientation.rotationNeededForOrientation(.portrait) // Downsample diff --git a/framework/Source/iOS/PictureInput.swift b/framework/Source/iOS/PictureInput.swift index 3c6aedf9..0b148d9c 100755 --- a/framework/Source/iOS/PictureInput.swift +++ b/framework/Source/iOS/PictureInput.swift @@ -1,19 +1,40 @@ import OpenGLES import UIKit +public enum PictureInputError: Error, CustomStringConvertible { + case zeroSizedImageError + case dataProviderNilError + case noSuchImageError(imageName: String) + + public var errorDescription: String { + switch self { + case .zeroSizedImageError: + return "Tried to pass in a zero-sized image" + case .dataProviderNilError: + return "Unable to retrieve image dataProvider" + case .noSuchImageError(let imageName): + return "No such image named: \(imageName) in your application bundle" + } + } + + public var description: String { + return "<\(type(of: self)): errorDescription = \(self.errorDescription)>" + } +} + public class PictureInput: ImageSource { public let targets = TargetContainer() - var imageFramebuffer:Framebuffer! + var imageFramebuffer:Framebuffer? + public var framebufferUserInfo:[AnyHashable:Any]? var hasProcessedImage:Bool = false - public init(image:CGImage, smoothlyScaleOutput:Bool = false, orientation:ImageOrientation = .portrait) { - // TODO: Dispatch this whole thing asynchronously to move image loading off main thread + public init(image:CGImage, smoothlyScaleOutput:Bool = false, orientation:ImageOrientation = .portrait) throws { let widthOfImage = GLint(image.width) let heightOfImage = GLint(image.height) // If passed an empty image reference, CGContextDrawImage will fail in future versions of the SDK. - guard((widthOfImage > 0) && (heightOfImage > 0)) else { fatalError("Tried to pass in a zero-sized image") } - + guard((widthOfImage > 0) && (heightOfImage > 0)) else { throw PictureInputError.zeroSizedImageError } + var widthToUseForTexture = widthOfImage var heightToUseForTexture = heightOfImage var shouldRedrawUsingCoreGraphics = false @@ -42,8 +63,8 @@ public class PictureInput: ImageSource { if (!shouldRedrawUsingCoreGraphics) { /* Check that the memory layout is compatible with GL, as we cannot use glPixelStore to - * tell GL about the memory layout with GLES. - */ + * tell GL about the memory layout with GLES. + */ if ((image.bytesPerRow != image.width * 4) || (image.bitsPerPixel != 32) || (image.bitsPerComponent != 8)) { shouldRedrawUsingCoreGraphics = true @@ -58,12 +79,12 @@ public class PictureInput: ImageSource { if (bitmapInfo.contains(.byteOrder32Little)) { /* Little endian, for alpha-first we can use this bitmap directly in GL */ if ((alphaInfo != CGImageAlphaInfo.premultipliedFirst) && (alphaInfo != CGImageAlphaInfo.first) && (alphaInfo != CGImageAlphaInfo.noneSkipFirst)) { - shouldRedrawUsingCoreGraphics = true + shouldRedrawUsingCoreGraphics = true } } else if ((bitmapInfo.contains(CGBitmapInfo())) || (bitmapInfo.contains(.byteOrder32Big))) { /* Big endian, for alpha-last we can use this bitmap directly in GL */ if ((alphaInfo != CGImageAlphaInfo.premultipliedLast) && (alphaInfo != CGImageAlphaInfo.last) && (alphaInfo != CGImageAlphaInfo.noneSkipLast)) { - shouldRedrawUsingCoreGraphics = true + shouldRedrawUsingCoreGraphics = true } else { /* Can access directly using GL_RGBA pixel format */ format = GL_RGBA @@ -73,32 +94,30 @@ public class PictureInput: ImageSource { } } - // CFAbsoluteTime elapsedTime, startTime = CFAbsoluteTimeGetCurrent(); - - if (shouldRedrawUsingCoreGraphics) { - // For resized or incompatible image: redraw - imageData = UnsafeMutablePointer.allocate(capacity:Int(widthToUseForTexture * heightToUseForTexture) * 4) - - let genericRGBColorspace = CGColorSpaceCreateDeviceRGB() + try sharedImageProcessingContext.runOperationSynchronously{ + // CFAbsoluteTime elapsedTime, startTime = CFAbsoluteTimeGetCurrent(); - let imageContext = CGContext(data: imageData, width: Int(widthToUseForTexture), height: Int(heightToUseForTexture), bitsPerComponent: 8, bytesPerRow: Int(widthToUseForTexture) * 4, space: genericRGBColorspace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) - // CGContextSetBlendMode(imageContext, kCGBlendModeCopy); // From Technical Q&A QA1708: http://developer.apple.com/library/ios/#qa/qa1708/_index.html - imageContext?.draw(image, in:CGRect(x:0.0, y:0.0, width:CGFloat(widthToUseForTexture), height:CGFloat(heightToUseForTexture))) - } else { - // Access the raw image bytes directly - dataFromImageDataProvider = image.dataProvider?.data - imageData = UnsafeMutablePointer(mutating:CFDataGetBytePtr(dataFromImageDataProvider)) - } - - sharedImageProcessingContext.runOperationSynchronously{ - do { - // TODO: Alter orientation based on metadata from photo - self.imageFramebuffer = try Framebuffer(context:sharedImageProcessingContext, orientation:orientation, size:GLSize(width:widthToUseForTexture, height:heightToUseForTexture), textureOnly:true) - } catch { - fatalError("ERROR: Unable to initialize framebuffer of size (\(widthToUseForTexture), \(heightToUseForTexture)) with error: \(error)") + if (shouldRedrawUsingCoreGraphics) { + // For resized or incompatible image: redraw + imageData = UnsafeMutablePointer.allocate(capacity:Int(widthToUseForTexture * heightToUseForTexture) * 4) + + let genericRGBColorspace = CGColorSpaceCreateDeviceRGB() + + let imageContext = CGContext(data: imageData, width: Int(widthToUseForTexture), height: Int(heightToUseForTexture), bitsPerComponent: 8, bytesPerRow: Int(widthToUseForTexture) * 4, space: genericRGBColorspace, bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue) + // CGContextSetBlendMode(imageContext, kCGBlendModeCopy); // From Technical Q&A QA1708: http://developer.apple.com/library/ios/#qa/qa1708/_index.html + imageContext?.draw(image, in:CGRect(x:0.0, y:0.0, width:CGFloat(widthToUseForTexture), height:CGFloat(heightToUseForTexture))) + } else { + // Access the raw image bytes directly + guard let data = image.dataProvider?.data else { throw PictureInputError.dataProviderNilError } + dataFromImageDataProvider = data + imageData = UnsafeMutablePointer(mutating:CFDataGetBytePtr(dataFromImageDataProvider)) } - glBindTexture(GLenum(GL_TEXTURE_2D), self.imageFramebuffer.texture) + // TODO: Alter orientation based on metadata from photo + self.imageFramebuffer = try Framebuffer(context:sharedImageProcessingContext, orientation:orientation, size:GLSize(width:widthToUseForTexture, height:heightToUseForTexture), textureOnly:true) + self.imageFramebuffer!.lock() + + glBindTexture(GLenum(GL_TEXTURE_2D), self.imageFramebuffer!.texture) if (smoothlyScaleOutput) { glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR_MIPMAP_LINEAR) } @@ -114,35 +133,47 @@ public class PictureInput: ImageSource { if (shouldRedrawUsingCoreGraphics) { imageData.deallocate(capacity:Int(widthToUseForTexture * heightToUseForTexture) * 4) } + } - public convenience init(image:UIImage, smoothlyScaleOutput:Bool = false, orientation:ImageOrientation = .portrait) { - self.init(image:image.cgImage!, smoothlyScaleOutput:smoothlyScaleOutput, orientation:orientation) + public convenience init(image:UIImage, smoothlyScaleOutput:Bool = false, orientation:ImageOrientation = .portrait) throws { + try self.init(image:image.cgImage!, smoothlyScaleOutput:smoothlyScaleOutput, orientation:orientation) } - - public convenience init(imageName:String, smoothlyScaleOutput:Bool = false, orientation:ImageOrientation = .portrait) { - guard let image = UIImage(named:imageName) else { fatalError("No such image named: \(imageName) in your application bundle") } - self.init(image:image.cgImage!, smoothlyScaleOutput:smoothlyScaleOutput, orientation:orientation) + + public convenience init(imageName:String, smoothlyScaleOutput:Bool = false, orientation:ImageOrientation = .portrait) throws { + guard let image = UIImage(named:imageName) else { throw PictureInputError.noSuchImageError(imageName: imageName) } + try self.init(image:image.cgImage!, smoothlyScaleOutput:smoothlyScaleOutput, orientation:orientation) } - + + deinit { + //debugPrint("Deallocating operation: \(self)") + + self.imageFramebuffer?.unlock() + } + public func processImage(synchronously:Bool = false) { if synchronously { sharedImageProcessingContext.runOperationSynchronously{ - self.updateTargetsWithFramebuffer(self.imageFramebuffer) - self.hasProcessedImage = true + if let framebuffer = self.imageFramebuffer { + self.updateTargetsWithFramebuffer(framebuffer) + self.hasProcessedImage = true + } } } else { sharedImageProcessingContext.runOperationAsynchronously{ - self.updateTargetsWithFramebuffer(self.imageFramebuffer) - self.hasProcessedImage = true + if let framebuffer = self.imageFramebuffer { + self.updateTargetsWithFramebuffer(framebuffer) + self.hasProcessedImage = true + } } } } public func transmitPreviousImage(to target:ImageConsumer, atIndex:UInt) { - if hasProcessedImage { - imageFramebuffer.lock() - target.newFramebufferAvailable(imageFramebuffer, fromSourceIndex:atIndex) + if let framebuffer = self.imageFramebuffer, + hasProcessedImage { + framebuffer.lock() + target.newFramebufferAvailable(framebuffer, fromSourceIndex:atIndex) } } } diff --git a/framework/Source/iOS/PictureOutput.swift b/framework/Source/iOS/PictureOutput.swift old mode 100644 new mode 100755 index 6e434bf6..1db2110b --- a/framework/Source/iOS/PictureOutput.swift +++ b/framework/Source/iOS/PictureOutput.swift @@ -114,14 +114,14 @@ public extension ImageSource { } public extension UIImage { - public func filterWithOperation(_ operation:T) -> UIImage { - return filterWithPipeline{input, output in + public func filterWithOperation(_ operation:T) throws -> UIImage { + return try filterWithPipeline{input, output in input --> operation --> output } } - public func filterWithPipeline(_ pipeline:(PictureInput, PictureOutput) -> ()) -> UIImage { - let picture = PictureInput(image:self) + public func filterWithPipeline(_ pipeline:(PictureInput, PictureOutput) -> ()) throws -> UIImage { + let picture = try PictureInput(image:self) var outputImage:UIImage? let pictureOutput = PictureOutput() pictureOutput.onlyCaptureNextFrame = true diff --git a/framework/Source/iOS/RenderView.swift b/framework/Source/iOS/RenderView.swift index 3bc4f382..f44be099 100755 --- a/framework/Source/iOS/RenderView.swift +++ b/framework/Source/iOS/RenderView.swift @@ -1,7 +1,6 @@ import UIKit // TODO: Add support for transparency -// TODO: Deal with view resizing public class RenderView:UIView, ImageConsumer { public var backgroundRenderColor = Color.black public var fillMode = FillMode.preserveAspectRatio @@ -17,81 +16,111 @@ public class RenderView:UIView, ImageConsumer { private lazy var displayShader:ShaderProgram = { return sharedImageProcessingContext.passthroughShader }() - - // TODO: Need to set viewport to appropriate size, resize viewport on view reshape + + private var internalLayer: CAEAGLLayer! required public init?(coder:NSCoder) { super.init(coder:coder) self.commonInit() } - + public override init(frame:CGRect) { super.init(frame:frame) self.commonInit() } - + override public class var layerClass:Swift.AnyClass { get { return CAEAGLLayer.self } } + override public var bounds: CGRect { + didSet { + // Check if the size changed + if(oldValue.size != self.bounds.size) { + // Destroy the displayFramebuffer so we render at the correct size for the next frame + sharedImageProcessingContext.runOperationAsynchronously{ + self.destroyDisplayFramebuffer() + } + } + } + } + func commonInit() { self.contentScaleFactor = UIScreen.main.scale let eaglLayer = self.layer as! CAEAGLLayer eaglLayer.isOpaque = true - eaglLayer.drawableProperties = [NSNumber(value:false): kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8: kEAGLDrawablePropertyColorFormat] + eaglLayer.drawableProperties = [kEAGLDrawablePropertyRetainedBacking: NSNumber(value:false), kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8] + + self.internalLayer = eaglLayer } deinit { - destroyDisplayFramebuffer() + sharedImageProcessingContext.runOperationSynchronously{ + destroyDisplayFramebuffer() + } } - func createDisplayFramebuffer() { + func createDisplayFramebuffer() -> Bool { var newDisplayFramebuffer:GLuint = 0 glGenFramebuffers(1, &newDisplayFramebuffer) displayFramebuffer = newDisplayFramebuffer glBindFramebuffer(GLenum(GL_FRAMEBUFFER), displayFramebuffer!) - + var newDisplayRenderbuffer:GLuint = 0 glGenRenderbuffers(1, &newDisplayRenderbuffer) displayRenderbuffer = newDisplayRenderbuffer glBindRenderbuffer(GLenum(GL_RENDERBUFFER), displayRenderbuffer!) - - sharedImageProcessingContext.context.renderbufferStorage(Int(GL_RENDERBUFFER), from:self.layer as! CAEAGLLayer) - + + // Without the flush you will occasionally get a warning from UIKit and when that happens the RenderView just stays black. + // "CoreAnimation: [EAGLContext renderbufferStorage:fromDrawable:] was called from a non-main thread in an implicit transaction! + // Note that this may be unsafe without an explicit CATransaction or a call to [CATransaction flush]." + // I tried a transaction and that doesn't work and this is probably why --> http://danielkbx.com/post/108060601989/catransaction-flush + // Using flush is important because it guarantees the view is layed out at the correct size before it is drawn to since this is being done on a background thread. + // Its possible the size of the view was changed right before we got here and would result in us drawing to the view at the old size + // and then the view size would change to the new size at the next layout pass and distort our already drawn image. + // Since we do not call this function often we do not need to worry about the performance impact of calling flush. + CATransaction.flush() + sharedImageProcessingContext.context.renderbufferStorage(Int(GL_RENDERBUFFER), from:self.internalLayer) + var backingWidth:GLint = 0 var backingHeight:GLint = 0 glGetRenderbufferParameteriv(GLenum(GL_RENDERBUFFER), GLenum(GL_RENDERBUFFER_WIDTH), &backingWidth) glGetRenderbufferParameteriv(GLenum(GL_RENDERBUFFER), GLenum(GL_RENDERBUFFER_HEIGHT), &backingHeight) backingSize = GLSize(width:backingWidth, height:backingHeight) - guard ((backingWidth > 0) && (backingHeight > 0)) else { - fatalError("View had a zero size") + guard (backingWidth > 0 && backingHeight > 0) else { + print("WARNING: View had a zero size") + + if(self.internalLayer.bounds.width > 0 && self.internalLayer.bounds.height > 0) { + print("WARNING: View size \(self.internalLayer.bounds) may be too large ") + } + return false } - + glFramebufferRenderbuffer(GLenum(GL_FRAMEBUFFER), GLenum(GL_COLOR_ATTACHMENT0), GLenum(GL_RENDERBUFFER), displayRenderbuffer!) let status = glCheckFramebufferStatus(GLenum(GL_FRAMEBUFFER)) if (status != GLenum(GL_FRAMEBUFFER_COMPLETE)) { - fatalError("Display framebuffer creation failed with error: \(FramebufferCreationError(errorCode:status))") + print("WARNING: Display framebuffer creation failed with error: \(FramebufferCreationError(errorCode:status))") + return false } + + return true } func destroyDisplayFramebuffer() { - sharedImageProcessingContext.runOperationSynchronously{ - if let displayFramebuffer = self.displayFramebuffer { - var temporaryFramebuffer = displayFramebuffer - glDeleteFramebuffers(1, &temporaryFramebuffer) - self.displayFramebuffer = nil - } - - if let displayRenderbuffer = self.displayRenderbuffer { - var temporaryRenderbuffer = displayRenderbuffer - glDeleteRenderbuffers(1, &temporaryRenderbuffer) - self.displayRenderbuffer = nil - } + if let displayFramebuffer = self.displayFramebuffer { + var temporaryFramebuffer = displayFramebuffer + glDeleteFramebuffers(1, &temporaryFramebuffer) + self.displayFramebuffer = nil + } + if let displayRenderbuffer = self.displayRenderbuffer { + var temporaryRenderbuffer = displayRenderbuffer + glDeleteRenderbuffers(1, &temporaryRenderbuffer) + self.displayRenderbuffer = nil } } @@ -101,13 +130,15 @@ public class RenderView:UIView, ImageConsumer { } public func newFramebufferAvailable(_ framebuffer:Framebuffer, fromSourceIndex:UInt) { - if (displayFramebuffer == nil) { - self.createDisplayFramebuffer() + if (self.displayFramebuffer == nil && !self.createDisplayFramebuffer()) { + // Bail if we couldn't successfully create the displayFramebuffer + framebuffer.unlock() + return } self.activateDisplayFramebuffer() clearFramebufferWithColor(backgroundRenderColor) - + let scaledVertices = fillMode.transformVertices(verticallyInvertedImageVertices, fromInputSize:framebuffer.sizeForTargetOrientation(self.orientation), toFitSize:backingSize) renderQuadWithShader(self.displayShader, vertices:scaledVertices, inputTextures:[framebuffer.texturePropertiesForTargetOrientation(self.orientation)]) framebuffer.unlock() @@ -116,3 +147,4 @@ public class RenderView:UIView, ImageConsumer { sharedImageProcessingContext.presentBufferForDisplay() } } + From 636a9b5a88f75900237cda82bc12ce3368265bc0 Mon Sep 17 00:00:00 2001 From: Josh Bernfeld Date: Sat, 23 Feb 2019 23:22:32 -0800 Subject: [PATCH 2/5] Fix PictureOutput warning "CGImageCreate: invalid image alphaInfo: kCGImageAlphaNone. It should be kCGImageAlphaNoneSkipLast" --- framework/Source/iOS/PictureOutput.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 framework/Source/iOS/PictureOutput.swift diff --git a/framework/Source/iOS/PictureOutput.swift b/framework/Source/iOS/PictureOutput.swift old mode 100755 new mode 100644 index da67e62c..a115f487 --- a/framework/Source/iOS/PictureOutput.swift +++ b/framework/Source/iOS/PictureOutput.swift @@ -53,7 +53,7 @@ public class PictureOutput: ImageConsumer { renderFramebuffer.unlock() guard let dataProvider = CGDataProvider(dataInfo:nil, data:data, size:imageByteSize, releaseData: dataProviderReleaseCallback) else {fatalError("Could not allocate a CGDataProvider")} let defaultRGBColorSpace = CGColorSpaceCreateDeviceRGB() - return CGImage(width:Int(framebuffer.size.width), height:Int(framebuffer.size.height), bitsPerComponent:8, bitsPerPixel:32, bytesPerRow:4 * Int(framebuffer.size.width), space:defaultRGBColorSpace, bitmapInfo:CGBitmapInfo() /*| CGImageAlphaInfo.Last*/, provider:dataProvider, decode:nil, shouldInterpolate:false, intent:.defaultIntent)! + return CGImage(width:Int(framebuffer.size.width), height:Int(framebuffer.size.height), bitsPerComponent:8, bitsPerPixel:32, bytesPerRow:4 * Int(framebuffer.size.width), space:defaultRGBColorSpace, bitmapInfo:CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue), provider:dataProvider, decode:nil, shouldInterpolate:false, intent:.defaultIntent)! } public func newFramebufferAvailable(_ framebuffer:Framebuffer, fromSourceIndex:UInt) { From 75e001f4afc7fd4e416169fbc61bcfd856f15772 Mon Sep 17 00:00:00 2001 From: Josh Bernfeld Date: Fri, 5 Apr 2019 19:42:18 -0700 Subject: [PATCH 3/5] Fix transmitPreviousImage() crash --- framework/Source/Pipeline.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/Source/Pipeline.swift b/framework/Source/Pipeline.swift index 911317e1..74bdb8e6 100755 --- a/framework/Source/Pipeline.swift +++ b/framework/Source/Pipeline.swift @@ -232,7 +232,9 @@ public class ImageRelay: ImageProcessingOperation { } public func transmitPreviousImage(to target:ImageConsumer, atIndex:UInt) { - sources.sources[0]?.transmitPreviousImage(to:self, atIndex:0) + if(sources.sources.count > 0) { + sources.sources[0]?.transmitPreviousImage(to:self, atIndex:0) + } } public func newFramebufferAvailable(_ framebuffer:Framebuffer, fromSourceIndex:UInt) { From 140aa43653dacd08ab8e2bd359da6c545e065b8e Mon Sep 17 00:00:00 2001 From: Josh Bernfeld Date: Sat, 6 Apr 2019 14:16:17 -0700 Subject: [PATCH 4/5] Fix transmitPreviousImage() crash --- framework/Source/Pipeline.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/framework/Source/Pipeline.swift b/framework/Source/Pipeline.swift index 74bdb8e6..78751c74 100755 --- a/framework/Source/Pipeline.swift +++ b/framework/Source/Pipeline.swift @@ -232,8 +232,14 @@ public class ImageRelay: ImageProcessingOperation { } public func transmitPreviousImage(to target:ImageConsumer, atIndex:UInt) { - if(sources.sources.count > 0) { - sources.sources[0]?.transmitPreviousImage(to:self, atIndex:0) + DispatchQueue.main.async { + // Sources is not thread safe so we should only access it from the main thread + // Note: addTarget() modifies the sources from the main thread + if let source = self.sources.sources[0] { + sharedImageProcessingContext.runOperationAsynchronously { + source.transmitPreviousImage(to:self, atIndex:0) + } + } } } From 443e4eedd1d1266170183445fa682aed05dc209b Mon Sep 17 00:00:00 2001 From: Josh Bernfeld Date: Mon, 23 Sep 2019 13:46:19 -0700 Subject: [PATCH 5/5] Add lookup table for detailed framebuffer creation error messages Fix exc_bad_access crashes caused by accessing main thread for framebuffer creation in ImageGenerator and TextureInput init() --- .../FilterShowcase/FilterOperations.swift | 6 ++--- .../FilterShowcase.xcodeproj/project.pbxproj | 6 +++-- framework/Source/Framebuffer.swift | 27 +++++++++++++++++-- framework/Source/ImageGenerator.swift | 11 +++++--- framework/Source/TextureInput.swift | 12 +++++---- 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift b/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift index 3440e17f..405e66e4 100755 --- a/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift +++ b/examples/Mac/FilterShowcase/FilterShowcase/FilterOperations.swift @@ -297,7 +297,7 @@ let filterOperations: Array = [ titleName:"Histogram", sliderConfiguration:.enabled(minimumValue:4.0, maximumValue:32.0, initialValue:16.0), sliderUpdateCallback: {(filter, sliderValue) in - filter.downsamplingFactor = UInt(round(sliderValue)) + filter.downsamplingFactor = UInt(roundf(sliderValue)) }, filterOperationType:.custom(filterSetupFunction: {(camera, filter, outputView) in let castFilter = filter as! Histogram @@ -684,7 +684,7 @@ let filterOperations: Array = [ titleName:"Posterize", sliderConfiguration:.enabled(minimumValue:1.0, maximumValue:20.0, initialValue:10.0), sliderUpdateCallback: {(filter, sliderValue) in - filter.colorLevels = round(sliderValue) + filter.colorLevels = roundf(sliderValue) }, filterOperationType:.singleInput ), @@ -753,7 +753,7 @@ let filterOperations: Array = [ titleName:"Kuwahara", sliderConfiguration:.enabled(minimumValue:3.0, maximumValue:9.0, initialValue:3.0), sliderUpdateCallback: {(filter, sliderValue) in - filter.radius = Int(round(sliderValue)) + filter.radius = Int(roundf(sliderValue)) }, filterOperationType:.singleInput ), diff --git a/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj b/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj index aaa4b714..b0f2fec3 100755 --- a/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj +++ b/examples/iOS/FilterShowcase/FilterShowcase.xcodeproj/project.pbxproj @@ -209,6 +209,7 @@ TargetAttributes = { BC0037B6195CA11B00B9D651 = { CreatedOnToolsVersion = 6.0; + DevelopmentTeam = FQ7WDPMYNA; LastSwiftMigration = 0940; ProvisioningStyle = Automatic; }; @@ -219,6 +220,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -442,7 +444,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = FQ7WDPMYNA; INFOPLIST_FILE = FilterShowcaseSwift/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.sunsetlakesoftware.${PRODUCT_NAME:rfc1034identifier}"; @@ -460,7 +462,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = FQ7WDPMYNA; INFOPLIST_FILE = FilterShowcaseSwift/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.sunsetlakesoftware.${PRODUCT_NAME:rfc1034identifier}"; diff --git a/framework/Source/Framebuffer.swift b/framework/Source/Framebuffer.swift index b0a7452e..cb7d5b1f 100755 --- a/framework/Source/Framebuffer.swift +++ b/framework/Source/Framebuffer.swift @@ -17,9 +17,32 @@ import Glibc import Foundation import AVFoundation -// TODO: Add a good lookup table to this to allow for detailed error messages -struct FramebufferCreationError:Error { +struct FramebufferCreationError:Error, CustomStringConvertible { + var description:String { + return "FramebufferCreationError(errorCode: \(self.errorCode), errorCodeDescription: \(self.errorCodeDescription))" + } + let errorCode:GLenum + + var errorCodeDescription:String { + // Source --> https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glCheckFramebufferStatus.xhtml + switch self.errorCode { + case GLenum(GL_FRAMEBUFFER_COMPLETE): + return "GL_FRAMEBUFFER_UNDEFINED" + case GLenum(GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT): + return "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT" + case GLenum(GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT): + return "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" + case GLenum(GL_FRAMEBUFFER_UNSUPPORTED): + return "GL_FRAMEBUFFER_UNSUPPORTED" + case GLenum(GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE): + return "GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE" + case GLenum(GL_INVALID_ENUM): + return "GL_INVALID_ENUM" + default: + return "UNKNOWN" + } + } } public enum FramebufferTimingStyle { diff --git a/framework/Source/ImageGenerator.swift b/framework/Source/ImageGenerator.swift index ea79f99b..932a2ba3 100644 --- a/framework/Source/ImageGenerator.swift +++ b/framework/Source/ImageGenerator.swift @@ -6,10 +6,13 @@ public class ImageGenerator: ImageSource { public init(size:Size) { self.size = size - do { - imageFramebuffer = try Framebuffer(context:sharedImageProcessingContext, orientation:.portrait, size:GLSize(size)) - } catch { - fatalError("Could not construct framebuffer of size: \(size), error:\(error)") + + sharedImageProcessingContext.runOperationSynchronously{ + do { + imageFramebuffer = try Framebuffer(context:sharedImageProcessingContext, orientation:.portrait, size:GLSize(size)) + } catch { + fatalError("Could not construct framebuffer of size: \(size), error:\(error)") + } } } diff --git a/framework/Source/TextureInput.swift b/framework/Source/TextureInput.swift index b2a782bf..81990e44 100644 --- a/framework/Source/TextureInput.swift +++ b/framework/Source/TextureInput.swift @@ -15,13 +15,15 @@ public class TextureInput: ImageSource { public let targets = TargetContainer() - let textureFramebuffer:Framebuffer + var textureFramebuffer:Framebuffer! public init(texture:GLuint, size:Size, orientation:ImageOrientation = .portrait) { - do { - textureFramebuffer = try Framebuffer(context:sharedImageProcessingContext, orientation:orientation, size:GLSize(size), textureOnly:true, overriddenTexture:texture) - } catch { - fatalError("Could not create framebuffer for custom input texture.") + sharedImageProcessingContext.runOperationSynchronously{ + do { + textureFramebuffer = try Framebuffer(context:sharedImageProcessingContext, orientation:orientation, size:GLSize(size), textureOnly:true, overriddenTexture:texture) + } catch { + fatalError("Could not create framebuffer for custom input texture.") + } } }