@@ -1,7 +1,3 @@
1−// TODO: Render metaballs on layer/view instead of image
2−// TODO: Remove references to view size from shader
3−// TODO: Make shader ignore metaballs that are outside a certain area of tolerance (eg ±100 from borders)
5// TODO: Make graphics have edges only according to graph (instead of being a complete graph)
1// TODO: Make graphics have edges only according to graph (instead of being a complete graph)
7// TODO: Interpolate metaball colors
3// TODO: Interpolate metaball colors
@@ -11,44 +7,113 @@
11import Metal
7import Metal
12import UIKit
8import UIKit
14−class MetalMetaballRenderer: MetaballRenderer {
12+class MetalMetaballRenderer {
16 typealias TargetView = UIImageView
14 typealias TargetView = UIImageView
16+ let targetView: TargetView
18− static let context = MTLContext()
18+ let context = MTLContext()
19+ var activeComputeContext: MTLComputeContext
20+ var idleComputeContext: MTLComputeContext
22+ let dataSource: MetaballDataSource
24+ let semaphore = dispatch_semaphore_create(2)
32+ var state: RendererState = .Idle {
35+ if newValue == .Running {
42+ required init(dataSource: MetaballDataSource, frame: CGRect) {
43+ self.dataSource = dataSource
45+ targetView = TargetView(frame: frame)
47+ let width = Int(frame.width)
48+ let height = Int(frame.height)
49+ let textureDescriptor =
50+ MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
52+ let texture1 = context.device.newTextureWithDescriptor(textureDescriptor)
53+ activeComputeContext = MTLComputeContext(size: targetView.size, texture: texture1)
54+ let texture2 = context.device.newTextureWithDescriptor(textureDescriptor)
55+ idleComputeContext = MTLComputeContext(size: targetView.size, texture: texture2)
58+ func updateTargetView() {
60+ let timeout = dispatch_time(DISPATCH_TIME_NOW, 1000000000)
61+ dispatch_semaphore_wait(semaphore, timeout)
63+ print("Update \(i++)!")
67+ dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.rawValue), 0)) { () -> Void in
69+ let computeContext = self.activeComputeContext
70+ swap(&self.activeComputeContext, &self.idleComputeContext)
72+ // Render graphics to metal texture
73+ self.renderToContext(computeContext)
20− static func updateTargetView(imageView: UIImageView, dataSource: MetaballDataSource) {
75+ if self.state != .Running {
21− let metaballs = dataSource.metaballs
76+ self.state = .Idle
78+ self.updateTargetView()
81+ // Transform metal texture into image
82+ let uiimage = self.createImageFromContext(computeContext)
84+ dispatch_semaphore_signal(self.semaphore)
86+ dispatch_async(dispatch_get_main_queue(), { () -> Void in
87+ // Send image to view
88+ self.targetView.image = uiimage
93+ func renderToContext(computeContext: MTLComputeContext) {
94+ let width = Int(targetView.width)
95+ let height = Int(targetView.height)
23 let kernelFunction: MTLFunction?
97 let kernelFunction: MTLFunction?
24 let pipeline: MTLComputePipelineState
98 let pipeline: MTLComputePipelineState
25− var internalTexture: MTLTexture! = nil
100+ // Get long-lived objects
28 kernelFunction = context.library.newFunctionWithName("drawMetaballs")
101 kernelFunction = context.library.newFunctionWithName("drawMetaballs")
29 pipeline = try context.device.newComputePipelineStateWithFunction(kernelFunction!)
102 pipeline = try context.device.newComputePipelineStateWithFunction(kernelFunction!)
31− let width = Int(imageView.width)
104+ // Configure info for computing
32− let height = Int(imageView.height)
34− let textureDescriptor =
35− MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
37− internalTexture = context.device.newTextureWithDescriptor(textureDescriptor)
38 let threadGroupCounts = MTLSizeMake(8, 8, 1)
105 let threadGroupCounts = MTLSizeMake(8, 8, 1)
39 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
106 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
40 height / threadGroupCounts.height, 1)
107 height / threadGroupCounts.height, 1)
42− //
109+ // Send commands to metal and render
43− let metaballInfoTexture = metaballTexture(metaballs)
46 let commandBuffer = context.commandQueue.commandBuffer()
110 let commandBuffer = context.commandQueue.commandBuffer()
48 let commandEncoder = commandBuffer.computeCommandEncoder()
112 let commandEncoder = commandBuffer.computeCommandEncoder()
49 commandEncoder.setComputePipelineState(pipeline)
113 commandEncoder.setComputePipelineState(pipeline)
50− commandEncoder.setTexture(internalTexture, atIndex: 0)
114+ commandEncoder.setTexture(computeContext.texture, atIndex: 0)
51− commandEncoder.setTexture(metaballInfoTexture, atIndex: 1)
115+ let metaballInfoBuffer = metaballBuffer()
116+ commandEncoder.setBuffer(metaballInfoBuffer, offset: 0, atIndex: 0)
52 commandEncoder.dispatchThreadgroups(threadGroups,
117 commandEncoder.dispatchThreadgroups(threadGroups,
53 threadsPerThreadgroup: threadGroupCounts)
118 threadsPerThreadgroup: threadGroupCounts)
54 commandEncoder.endEncoding()
119 commandEncoder.endEncoding()
@@ -59,30 +124,32 @@ class MetalMetaballRenderer: MetaballRenderer {
59 } catch _ {
124 } catch _ {
60 print("Error!")
125 print("Error!")
63− imageView.image = image(texture: internalTexture)
66− static func image(texture texture: MTLTexture) -> UIImage {
129+ func createImageFromContext(computeContext: MTLComputeContext) -> UIImage {
130+ let texture = computeContext.texture
67 let imageSize = CGSizeMake(CGFloat(texture.width),
133 let imageSize = CGSizeMake(CGFloat(texture.width),
68 CGFloat(texture.height))
134 CGFloat(texture.height))
70 let width = Int(imageSize.width)
135 let width = Int(imageSize.width)
71 let height = Int(imageSize.height)
136 let height = Int(imageSize.height)
73 let imageByteCount = width * height * 4
137 let imageByteCount = width * height * 4
74− let imageBytes = malloc(imageByteCount)
75 let bytesPerRow = width * 4
138 let bytesPerRow = width * 4
76 let region = MTLRegionMake2D(0, 0, width, height)
139 let region = MTLRegionMake2D(0, 0, width, height)
77− texture.getBytes(imageBytes, bytesPerRow: bytesPerRow,
78− fromRegion: region, mipmapLevel: 0)
80− let provider = CGDataProviderCreateWithData(nil, imageBytes,
141+ // Allocate the buffer if needed
142+ if computeContext.imageBuffer == nil {
81− imageByteCount) {
143+ computeContext.imageBuffer = malloc(imageByteCount)
82− (_, data, _) -> Void in
83− free(UnsafeMutablePointer<Void>(data))
146+ // Transfer texture info into image buffer
147+ texture.getBytes(computeContext.imageBuffer, bytesPerRow: bytesPerRow,
148+ fromRegion: region, mipmapLevel: 0)
150+ // Get info for UIImage
151+ let provider = CGDataProviderCreateWithData(nil, computeContext.imageBuffer,
152+ imageByteCount, nil)
86 let bitsPerComponent = 8
153 let bitsPerComponent = 8
87 let bitsperPixel = 32
154 let bitsperPixel = 32
88 let colorSpaceRef = CGColorSpaceCreateDeviceRGB()
155 let colorSpaceRef = CGColorSpaceCreateDeviceRGB()
@@ -90,37 +157,39 @@ class MetalMetaballRenderer: MetaballRenderer {
90 CGImageAlphaInfo.PremultipliedLast.rawValue |
157 CGImageAlphaInfo.PremultipliedLast.rawValue |
91 CGBitmapInfo.ByteOrder32Big.rawValue)
158 CGBitmapInfo.ByteOrder32Big.rawValue)
92 let renderingIntent = CGColorRenderingIntent.RenderingIntentDefault
159 let renderingIntent = CGColorRenderingIntent.RenderingIntentDefault
161+ // Create UIImage from image buffer
93 let imageRef = CGImageCreate(width, height,
162 let imageRef = CGImageCreate(width, height,
94 bitsPerComponent, bitsperPixel, bytesPerRow,
163 bitsPerComponent, bitsperPixel, bytesPerRow,
95 colorSpaceRef, bitmapInfo, provider,
164 colorSpaceRef, bitmapInfo, provider,
96 nil, false, renderingIntent)
165 nil, false, renderingIntent)
97 let image = UIImage(CGImage: imageRef!, scale: 0.0,
166 let image = UIImage(CGImage: imageRef!, scale: 0.0,
98− orientation: UIImageOrientation.DownMirrored)
167+ orientation: UIImageOrientation.Up)
100 return image
169 return image
103− static func metaballTexture(metaballs: [Metaball]) -> MTLTexture {
172+ func metaballBuffer() -> MTLBuffer {
173+ // Create Float array for buffer
174+ // Exclude metaballs far from the view's bounds
175+ let border: CGFloat = 100
176+ let metaballs = self.dataSource.metaballs
104− let floats: [Float] = metaballs.reduce([Float(metaballs.count)]) { (var array, metaball) -> [Float] in
177+ var floats: [Float] = metaballs.reduce([Float(0)]) { (var array, metaball) -> [Float] in
105− array.append(Float(metaball.midX))
178+ let x = CGFloat(metaball.midX)
106− array.append(Float(metaball.midY))
179+ let y = CGFloat(metaball.midY)
180+ if -border < x ≤ targetView.width + border &&
181+ -border < y ≤ targetView.height + border {
182+ array.append(Float(x))
183+ array.append(Float(y))
107 return array
185 return array
187+ // Add array size to start of array so metal knows how far to go
109− let size = floats.count
188+ floats[0] = Float(floats.count) / 2
111− let textureDescriptor =
190+ // Create buffer
112− MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(
191+ let buffer = context.device.newBufferWithBytes(floats, length: floats.count * sizeof(Float), options: .StorageModeShared)
113− .R32Float, width: size, height: 1, mipmapped: false)
115− let texture = context.device.newTextureWithDescriptor(textureDescriptor)
117− let region = MTLRegionMake2D(0, 0, size, 1)
119− texture.replaceRegion(region, mipmapLevel: 0, withBytes: floats,
120− bytesPerRow: sizeof(Float) * size)
122− return texture
193+ return buffer