@@ -1,7 +1,12 @@
1−// TODO: Render metaballs on layer/view instead of image
1+// TODO: Optimize resources (don't allocate textures and images every frame)
22// TODO: Remove references to view size from shader
33// TODO: Make shader ignore metaballs that are outside a certain area of tolerance (eg ±100 from borders)
44
5+// TODO: Metaballs from texture to buffer
6+// TODO: Use async dispatch to free up main queue
7+// TODO: Refactor update into 2 calls: metalRendering() and textureToImage()
8+// TODO: Use two textures/images to alternate the 2 calls (like OpenGL double buffers)
9+
510// TODO: Make graphics have edges only according to graph (instead of being a complete graph)
611
712// TODO: Interpolate metaball colors
@@ -15,39 +20,58 @@ class MetalMetaballRenderer: MetaballRenderer {
1520
1621 typealias TargetView = UIImageView
1722
18− static let context = MTLContext()
23+ let targetView = TargetView()
24+ var previousFrame = CGRect.zero
25+
26+ let context = MTLContext()
27+ var renderingTexture: MTLTexture!
28+ var imageBuffer: UnsafeMutablePointer<Void>!
29+
30+ let dataSource: MetaballDataSource
31+
32+ required init(dataSource: MetaballDataSource) {
33+ self.dataSource = dataSource
34+ }
1935
20− static func updateTargetView(imageView: UIImageView, dataSource: MetaballDataSource) {
36+ func updateTargetView() {
2137 let metaballs = dataSource.metaballs
2238
39+ if previousFrame != targetView.frame {
40+ previousFrame = targetView.frame
41+ updateFrame()
42+ }
43+
44+ let width = Int(targetView.width)
45+ let height = Int(targetView.height)
46+
47+ if renderingTexture == nil {
48+ let textureDescriptor =
49+ MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
50+
51+ renderingTexture = context.device.newTextureWithDescriptor(textureDescriptor)
52+ }
53+
2354 let kernelFunction: MTLFunction?
2455 let pipeline: MTLComputePipelineState
25− var internalTexture: MTLTexture! = nil
26−
2756 do {
57+ // Get long-lived objects
2858 kernelFunction = context.library.newFunctionWithName("drawMetaballs")
2959 pipeline = try context.device.newComputePipelineStateWithFunction(kernelFunction!)
3060
31− let width = Int(imageView.width)
32− let height = Int(imageView.height)
33−
34− let textureDescriptor =
35− MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(.BGRA8Unorm, width: width, height: height, mipmapped: false)
36−
37− internalTexture = context.device.newTextureWithDescriptor(textureDescriptor)
61+ // Configure info for computing
3862 let threadGroupCounts = MTLSizeMake(8, 8, 1)
3963 let threadGroups = MTLSizeMake(width / threadGroupCounts.width,
4064 height / threadGroupCounts.height, 1)
4165
42− //
66+ // Create up-to-date metaball buffer
4367 let metaballInfoTexture = metaballTexture(metaballs)
4468
45− //
69+ // Send commands to metal and render
4670 let commandBuffer = context.commandQueue.commandBuffer()
4771
4872 let commandEncoder = commandBuffer.computeCommandEncoder()
4973 commandEncoder.setComputePipelineState(pipeline)
50− commandEncoder.setTexture(internalTexture, atIndex: 0)
74+ commandEncoder.setTexture(renderingTexture, atIndex: 0)
5175 commandEncoder.setTexture(metaballInfoTexture, atIndex: 1)
5276 commandEncoder.dispatchThreadgroups(threadGroups,
5377 threadsPerThreadgroup: threadGroupCounts)
@@ -60,29 +84,32 @@ class MetalMetaballRenderer: MetaballRenderer {
6084 print("Error!")
6185 }
6286
63− imageView.image = image(texture: internalTexture)
87+ // Transform new metal texture into image
88+ targetView.image = image(texture: renderingTexture)
6489 }
6590
66− static func image(texture texture: MTLTexture) -> UIImage {
91+ func image(texture texture: MTLTexture) -> UIImage {
92+ // Get image info
6793 let imageSize = CGSizeMake(CGFloat(texture.width),
6894 CGFloat(texture.height))
69−
7095 let width = Int(imageSize.width)
7196 let height = Int(imageSize.height)
72−
7397 let imageByteCount = width * height * 4
74− let imageBytes = malloc(imageByteCount)
7598 let bytesPerRow = width * 4
7699 let region = MTLRegionMake2D(0, 0, width, height)
77− texture.getBytes(imageBytes, bytesPerRow: bytesPerRow,
78− fromRegion: region, mipmapLevel: 0)
79100
80− let provider = CGDataProviderCreateWithData(nil, imageBytes,
81− imageByteCount) {
82− (_, data, _) -> Void in
83− free(UnsafeMutablePointer<Void>(data))
101+ // Allocate the buffer if needed
102+ if imageBuffer == nil {
103+ imageBuffer = malloc(imageByteCount)
84104 }
85105
106+ // Transfer texture info into image buffer
107+ texture.getBytes(imageBuffer, bytesPerRow: bytesPerRow,
108+ fromRegion: region, mipmapLevel: 0)
109+
110+ // Get info for UIImage
111+ let provider = CGDataProviderCreateWithData(nil, imageBuffer,
112+ imageByteCount, nil)
86113 let bitsPerComponent = 8
87114 let bitsperPixel = 32
88115 let colorSpaceRef = CGColorSpaceCreateDeviceRGB()
@@ -90,6 +117,8 @@ class MetalMetaballRenderer: MetaballRenderer {
90117 CGImageAlphaInfo.PremultipliedLast.rawValue |
91118 CGBitmapInfo.ByteOrder32Big.rawValue)
92119 let renderingIntent = CGColorRenderingIntent.RenderingIntentDefault
120+
121+ // Create UIImage from image buffer
93122 let imageRef = CGImageCreate(width, height,
94123 bitsPerComponent, bitsperPixel, bytesPerRow,
95124 colorSpaceRef, bitmapInfo, provider,
@@ -100,27 +129,37 @@ class MetalMetaballRenderer: MetaballRenderer {
100129 return image
101130 }
102131
103− static func metaballTexture(metaballs: [Metaball]) -> MTLTexture {
132+ func metaballTexture(metaballs: [Metaball]) -> MTLTexture {
133+ // Create Float array for buffer
104134 let floats: [Float] = metaballs.reduce([Float(metaballs.count)]) { (var array, metaball) -> [Float] in
105135 array.append(Float(metaball.midX))
106136 array.append(Float(metaball.midY))
107137 return array
108138 }
109− let size = floats.count
110139
140+ // Create texture
141+ let size = floats.count
111142 let textureDescriptor =
112143 MTLTextureDescriptor.texture2DDescriptorWithPixelFormat(
113144 .R32Float, width: size, height: 1, mipmapped: false)
114−
115145 let texture = context.device.newTextureWithDescriptor(textureDescriptor)
116146
147+ // Copy array info into texture
117148 let region = MTLRegionMake2D(0, 0, size, 1)
118−
119149 texture.replaceRegion(region, mipmapLevel: 0, withBytes: floats,
120150 bytesPerRow: sizeof(Float) * size)
121151
122152 return texture
123153 }
124−
125−}
126154
155+ func updateFrame() {
156+ if imageBuffer != nil {
157+ free(imageBuffer)
158+ }
159+ imageBuffer = nil
160+ }
161+
162+ deinit {
163+ free(imageBuffer)
164+ }
165+}