@@ -1,7 +1,3 @@
1−// TODO: Refactor update into 2 calls: metalRendering() and textureToImage()
2−// TODO: Use async dispatch to free up main queue
3−// TODO: Use two textures/images to alternate the 2 calls (like OpenGL double buffers)
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,45 +7,92 @@
11import Metal
7import Metal
12import UIKit
8import UIKit
14−class MetalMetaballRenderer: MetaballRenderer {
10+var i = 0
16− typealias TargetView = UIImageView
12+class MetalMetaballRenderer {
18− var supportsDynamicRendering: Bool { get {
14+ typealias TargetView = UIImageView
23− let targetView = TargetView()
16+ let targetView: TargetView
24− var previousFrame = CGRect.zero
26 let context = MTLContext()
18 let context = MTLContext()
27− var renderingTexture: MTLTexture!
19+ var activeComputeContext: MTLComputeContext
28− var imageBuffer: UnsafeMutablePointer<Void>!
20+ var idleComputeContext: MTLComputeContext
30 let dataSource: MetaballDataSource
22 let dataSource: MetaballDataSource
24+ let semaphore = dispatch_semaphore_create(2)
32+ var state: RendererState = .Idle {
35+ if newValue == .Running {
32− required init(dataSource: MetaballDataSource) {
42+ required init(dataSource: MetaballDataSource, frame: CGRect) {
33 self.dataSource = dataSource
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)
36 func updateTargetView() {
58 func updateTargetView() {
37− let metaballs = dataSource.metaballs
39− if previousFrame != targetView.frame {
60+ let timeout = dispatch_time(DISPATCH_TIME_NOW, 1000000000)
40− previousFrame = targetView.frame
61+ dispatch_semaphore_wait(semaphore, timeout)
44− let width = Int(targetView.width)
63+ print("Update \(i++)!")
45− let height = Int(targetView.height)
64+
47− if renderingTexture == nil {
67+ dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INTERACTIVE.rawValue), 0)) { () -> Void in
48− let textureDescriptor =
49− MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
51− renderingTexture = context.device.newTextureWithDescriptor(textureDescriptor)
69+ let computeContext = self.activeComputeContext
70+ swap(&self.activeComputeContext, &self.idleComputeContext)
72+ // Render graphics to metal texture
73+ self.renderToContext(computeContext)
75+ if self.state != .Running {
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)
54 let kernelFunction: MTLFunction?
97 let kernelFunction: MTLFunction?
55 let pipeline: MTLComputePipelineState
98 let pipeline: MTLComputePipelineState
@@ -63,15 +106,13 @@ class MetalMetaballRenderer: MetaballRenderer {
63 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
106 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
64 height / threadGroupCounts.height, 1)
107 height / threadGroupCounts.height, 1)
66− // Create up-to-date metaball buffer
67− let metaballInfoBuffer = metaballBuffer(metaballs)
69 // Send commands to metal and render
109 // Send commands to metal and render
70 let commandBuffer = context.commandQueue.commandBuffer()
110 let commandBuffer = context.commandQueue.commandBuffer()
72 let commandEncoder = commandBuffer.computeCommandEncoder()
112 let commandEncoder = commandBuffer.computeCommandEncoder()
73 commandEncoder.setComputePipelineState(pipeline)
113 commandEncoder.setComputePipelineState(pipeline)
74− commandEncoder.setTexture(renderingTexture, atIndex: 0)
114+ commandEncoder.setTexture(computeContext.texture, atIndex: 0)
115+ let metaballInfoBuffer = metaballBuffer()
75 commandEncoder.setBuffer(metaballInfoBuffer, offset: 0, atIndex: 0)
116 commandEncoder.setBuffer(metaballInfoBuffer, offset: 0, atIndex: 0)
76 commandEncoder.dispatchThreadgroups(threadGroups,
117 commandEncoder.dispatchThreadgroups(threadGroups,
77 threadsPerThreadgroup: threadGroupCounts)
118 threadsPerThreadgroup: threadGroupCounts)
@@ -83,12 +124,11 @@ class MetalMetaballRenderer: MetaballRenderer {
83 } catch _ {
124 } catch _ {
84 print("Error!")
125 print("Error!")
87− // Transform new metal texture into image
88− targetView.image = image(texture: renderingTexture)
91− func image(texture texture: MTLTexture) -> UIImage {
129+ func createImageFromContext(computeContext: MTLComputeContext) -> UIImage {
130+ let texture = computeContext.texture
92 // Get image info
132 // Get image info
93 let imageSize = CGSizeMake(CGFloat(texture.width),
133 let imageSize = CGSizeMake(CGFloat(texture.width),
94 CGFloat(texture.height))
134 CGFloat(texture.height))
@@ -99,16 +139,16 @@ class MetalMetaballRenderer: MetaballRenderer {
99 let region = MTLRegionMake2D(0, 0, width, height)
139 let region = MTLRegionMake2D(0, 0, width, height)
101 // Allocate the buffer if needed
141 // Allocate the buffer if needed
102− if imageBuffer == nil {
142+ if computeContext.imageBuffer == nil {
103− imageBuffer = malloc(imageByteCount)
143+ computeContext.imageBuffer = malloc(imageByteCount)
106 // Transfer texture info into image buffer
146 // Transfer texture info into image buffer
107− texture.getBytes(imageBuffer, bytesPerRow: bytesPerRow,
147+ texture.getBytes(computeContext.imageBuffer, bytesPerRow: bytesPerRow,
108 fromRegion: region, mipmapLevel: 0)
148 fromRegion: region, mipmapLevel: 0)
110 // Get info for UIImage
150 // Get info for UIImage
111− let provider = CGDataProviderCreateWithData(nil, imageBuffer,
151+ let provider = CGDataProviderCreateWithData(nil, computeContext.imageBuffer,
112 imageByteCount, nil)
152 imageByteCount, nil)
113 let bitsPerComponent = 8
153 let bitsPerComponent = 8
114 let bitsperPixel = 32
154 let bitsperPixel = 32
@@ -129,17 +169,18 @@ class MetalMetaballRenderer: MetaballRenderer {
129 return image
169 return image
132− func metaballBuffer(metaballs: [Metaball]) -> MTLBuffer {
172+ func metaballBuffer() -> MTLBuffer {
133 // Create Float array for buffer
173 // Create Float array for buffer
134 // Exclude metaballs far from the view's bounds
174 // Exclude metaballs far from the view's bounds
135 let border: CGFloat = 100
175 let border: CGFloat = 100
176+ let metaballs = self.dataSource.metaballs
136 var floats: [Float] = metaballs.reduce([Float(0)]) { (var array, metaball) -> [Float] in
177 var floats: [Float] = metaballs.reduce([Float(0)]) { (var array, metaball) -> [Float] in
137 let x = CGFloat(metaball.midX)
178 let x = CGFloat(metaball.midX)
138 let y = CGFloat(metaball.midY)
179 let y = CGFloat(metaball.midY)
139 if -border < x ≤ targetView.width + border &&
180 if -border < x ≤ targetView.width + border &&
140− -border < y ≤ targetView.height + border {
181+ -border < y ≤ targetView.height + border {
141− array.append(Float(x))
182+ array.append(Float(x))
142− array.append(Float(y))
183+ array.append(Float(y))
144 return array
185 return array
@@ -148,18 +189,7 @@ class MetalMetaballRenderer: MetaballRenderer {
149 // Create buffer
190 // Create buffer
150 let buffer = context.device.newBufferWithBytes(floats, length: floats.count * sizeof(Float), options: .StorageModeShared)
191 let buffer = context.device.newBufferWithBytes(floats, length: floats.count * sizeof(Float), options: .StorageModeShared)
152 return buffer
193 return buffer
155− func updateFrame() {
156− if imageBuffer != nil {