From 8b4f2d8cc23179c868f4a684e6cb6a4209837427 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 5 Aug 2017 11:10:06 -0700 Subject: [PATCH] [MetaballsKit] Do not compute a texture, just provide buffers for balls and parameter data --- MetaballsKit/Metaballs.swift | 198 +++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 93 deletions(-) diff --git a/MetaballsKit/Metaballs.swift b/MetaballsKit/Metaballs.swift index 395e8b4..c45f638 100644 --- a/MetaballsKit/Metaballs.swift +++ b/MetaballsKit/Metaballs.swift @@ -14,13 +14,13 @@ public enum MetaballsError: Error { } public struct Ball { - let radius: CGFloat - var position = CGPoint() - var velocity = CGVector() + let radius: Float + var position = Point() + var velocity = Vector() internal var bounds: CGRect { - let diameter = radius * 2 - return CGRect(x: position.x - radius, y: position.y - radius, width: diameter, height: diameter) + let diameter = CGFloat(radius * 2) + return CGRect(x: CGFloat(position.x - radius), y: CGFloat(position.y - radius), width: diameter, height: diameter) } internal mutating func update() { @@ -29,22 +29,35 @@ public struct Ball { } } +extension Ball: CustomStringConvertible { + public var description: String { + return "" + } +} + public class Field { public var size: CGSize { didSet { if size != oldValue { + NSLog("Updating size of field: old:\(oldValue), new:\(size)") let numberOfBallsBeforeFilter = balls.count // Remove balls that fall outside the new bounds. balls = balls.filter { bounds.contains($0.bounds) } // Update Metal state as needed. - updateThreadgroupSizes(withFieldSize: size) - parametersBuffer = nil - sampleTexture = nil +// updateThreadgroupSizes(withFieldSize: size) +// parametersBuffer = nil +// sampleTexture = nil if numberOfBallsBeforeFilter != balls.count { ballBuffer = nil } + do { + try updateBuffers() + } catch let e { + NSLog("Error updating size: \(e)") + return + } } } } @@ -65,9 +78,9 @@ public class Field { // Update position of ball. ball.update() - if !selfBounds.contains(ball.position) { + if !selfBounds.contains(ball.position.CGPoint) { // Degenerate case. If the ball finds itself outside the bounds of the field, plop it back in the center. - ball.position = CGPoint(x: selfBounds.midX, y: selfBounds.midY) + ball.position = Point(x: Float(selfBounds.midX), y: Float(selfBounds.midY)) } else { // Do collision detection with walls. let ballBounds = ball.bounds @@ -83,33 +96,33 @@ public class Field { } } - public func add(ballWithRadius radius: CGFloat) { - NSLog("Adding ball with r=\(radius); fieldSize=\(size)") - let insetBounds = bounds.insetBy(dx: radius, dy: radius) + public func add(ballWithRadius radius: Float) { + let insetBounds = bounds.insetBy(dx: CGFloat(radius), dy: CGFloat(radius)) // let x = CGFloat(UInt32(insetBounds.minX) + arc4random_uniform(UInt32(insetBounds.width))) // let y = CGFloat(UInt32(insetBounds.minY) + arc4random_uniform(UInt32(insetBounds.height))) - let position = CGPoint(x: insetBounds.midX, y: insetBounds.midY) + let position = Point(x: Float(insetBounds.midX), y: Float(insetBounds.midY)) // TODO: Randomly generate velocity too. - let ball = Ball(radius: radius, position: position, velocity: CGVector()) + let ball = Ball(radius: radius, position: position, velocity: Vector()) balls.append(ball) + NSLog("Added ball \(ball); fieldSize=\(size)") } // MARK: - Metal Configuration private var device: MTLDevice? - private var sampleComputeState: MTLComputePipelineState? - private var parametersBuffer: MTLBuffer? - private var ballBuffer: MTLBuffer? - public private(set) var sampleTexture: MTLTexture? +// private var sampleComputeState: MTLComputePipelineState? + public private(set) var parametersBuffer: MTLBuffer? + public private(set) var ballBuffer: MTLBuffer? +// public private(set) var sampleTexture: MTLTexture? - private var threadgroupCount = MTLSize() +// private var threadgroupCount = MTLSize() // TODO: It might be possible to (more dynamically) right-size this. - private var threadgroupSize = MTLSize(width: 16, height: 16, depth: 1) +// private var threadgroupSize = MTLSize(width: 16, height: 16, depth: 1) /// Create the Metal buffer containing basic parameters of the simulation. private func makeParametersBufferIfNeeded(withDevice device: MTLDevice) -> MTLBuffer? { if parametersBuffer == nil { - parametersBuffer = device.makeBuffer(length: MemoryLayout.size * 3, options: []) + parametersBuffer = device.makeBuffer(length: MemoryLayout.stride * 3, options: []) } return parametersBuffer } @@ -118,8 +131,8 @@ public class Field { /// @param device The Metal device to use to create the buffer. /// @return A new buffer containing metaball data. private func makeBallBufferIfNeeded(withDevice device: MTLDevice) -> MTLBuffer? { - if ballBuffer == nil { - let sizeOfBall = MemoryLayout.size + if ballBuffer == nil && balls.count > 0 { + let sizeOfBall = MemoryLayout.stride * 3 // A Ball in shader-land is a float3. let length = balls.count * sizeOfBall ballBuffer = device.makeBuffer(length: length, options: []) } @@ -129,63 +142,59 @@ public class Field { /// Create a Metal texture to hold sample values created by the sampling compute shader. /// @param device The Metal device to use to create the texture. /// @return A new texture. - private func makeSampleTextureIfNeeded(withDevice device: MTLDevice) -> MTLTexture? { - if sampleTexture == nil { - let desc = MTLTextureDescriptor() - desc.pixelFormat = .r16Float - desc.width = Int(size.width) - desc.height = Int(size.height) - desc.usage = [.shaderWrite, .shaderRead] - sampleTexture = device.makeTexture(descriptor: desc) - } - return sampleTexture - } +// private func makeSampleTextureIfNeeded(withDevice device: MTLDevice) -> MTLTexture? { +// if sampleTexture == nil { +// let desc = MTLTextureDescriptor() +// desc.pixelFormat = .r16Float +// desc.width = Int(size.width) +// desc.height = Int(size.height) +// desc.usage = [.shaderWrite, .shaderRead] +// sampleTexture = device.makeTexture(descriptor: desc) +// } +// return sampleTexture +// } /// Update the threadgroup divisions based on the size of the field. /// @param size The size of the field. - private func updateThreadgroupSizes(withFieldSize size: CGSize) { - let width = Int(size.width) - let height = Int(size.height) - threadgroupCount = MTLSize(width: width + threadgroupSize.width - 1, height: height + threadgroupSize.height - 1, depth: 1) - } +// private func updateThreadgroupSizes(withFieldSize size: CGSize) { +// let width = Int(size.width) +// let height = Int(size.height) +// threadgroupCount = MTLSize(width: width + threadgroupSize.width - 1, height: height + threadgroupSize.height - 1, depth: 1) +// } /// Copy metaballs data into the parameters buffer. - private func updateParametersBuffer() { - guard let parameters = parametersBuffer, - let balls = ballBuffer + public func updateBuffers() throws { + guard let device = self.device else { + throw MetaballsError.metalError("Missing Metal device for update") + } + + guard let parameters = makeParametersBufferIfNeeded(withDevice: device), + let balls = makeBallBufferIfNeeded(withDevice: device) else { - return + throw MetaballsError.metalError("Couldn't create buffers") } var ptr = parameters.contents() var width = Int(size.width) - ptr = writeValue(value: &width, to: ptr) + ptr = write(value: &width, to: ptr) var height = Int(size.height) - ptr = writeValue(value: &height, to: ptr) + ptr = write(value: &height, to: ptr) var numberOfBalls = self.balls.count - ptr = writeValue(value: &numberOfBalls, to: ptr) + ptr = write(value: &numberOfBalls, to: ptr) ptr = balls.contents() - for ball in self.balls { - var radius = Float(ball.radius) - ptr = writeValue(value: &radius, to: ptr) - - var posX = Float(ball.position.x) - ptr = writeValue(value: &posX, to: ptr) - var posY = Float(ball.position.y) - ptr = writeValue(value: &posY, to: ptr) - - var dx = Float(ball.velocity.dx) - ptr = writeValue(value: &dx, to: ptr) - var dy = Float(ball.velocity.dy) - ptr = writeValue(value: &dy, to: ptr) + for var ball in self.balls { + ptr = write(value: &ball.position.x, to: ptr) + ptr = write(value: &ball.position.y, to: ptr) + var r = ball.radius + ptr = write(value: &r, to: ptr) } } - private func writeValue(value: inout T, to ptr: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { - let sizeOfType = MemoryLayout.size + private func write(value: inout T, to ptr: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { + let sizeOfType = MemoryLayout.stride ptr.copyBytes(from: &value, count: sizeOfType) return ptr.advanced(by: sizeOfType) } @@ -194,39 +203,42 @@ public class Field { guard self.device == nil else { return } + NSLog("Setting up Metal") self.device = device - sampleComputeState = try computePipelineStateForSamplingKernel(withDevice: device) +// sampleComputeState = try computePipelineStateForSamplingKernel(withDevice: device) + parametersBuffer = makeParametersBufferIfNeeded(withDevice: device) + ballBuffer = makeBallBufferIfNeeded(withDevice: device) } - public func computePipelineStateForSamplingKernel(withDevice device: MTLDevice) throws -> MTLComputePipelineState? { - let bundle = Bundle(for: type(of: self)) - let library = try device.makeDefaultLibrary(bundle: bundle) - guard let samplingKernel = library.makeFunction(name: "sampleFieldKernel") else { - throw MetaballsError.metalError("Unable to create sampling kernel function") - } - let state = try device.makeComputePipelineState(function: samplingKernel) - return state - } +// public func computePipelineStateForSamplingKernel(withDevice device: MTLDevice) throws -> MTLComputePipelineState? { +// let bundle = Bundle(for: type(of: self)) +// let library = try device.makeDefaultLibrary(bundle: bundle) +// guard let samplingKernel = library.makeFunction(name: "sampleFieldKernel") else { +// throw MetaballsError.metalError("Unable to create sampling kernel function") +// } +// let state = try device.makeComputePipelineState(function: samplingKernel) +// return state +// } - public func computeEncoderForSamplingKernel(withDevice device: MTLDevice, commandBuffer buffer: MTLCommandBuffer) throws -> MTLComputeCommandEncoder { - guard let parametersBuffer = makeParametersBufferIfNeeded(withDevice: device), - let ballBuffer = makeBallBufferIfNeeded(withDevice: device), - let sampleTexture = makeSampleTextureIfNeeded(withDevice: device), - let state = sampleComputeState - else { - throw MetaballsError.metalError("Missing Metal buffers or compute state") - } - - let encoder = buffer.makeComputeCommandEncoder() - encoder.setComputePipelineState(state) - encoder.setBuffer(parametersBuffer, offset: 0, at: 0) - encoder.setBuffer(ballBuffer, offset: 0, at: 1) - encoder.setTexture(sampleTexture, at: 0) - encoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) - encoder.endEncoding() - - updateParametersBuffer() - - return encoder - } +// public func computeEncoderForSamplingKernel(withDevice device: MTLDevice, commandBuffer buffer: MTLCommandBuffer) throws -> MTLComputeCommandEncoder { +// guard let parametersBuffer = makeParametersBufferIfNeeded(withDevice: device), +// let ballBuffer = makeBallBufferIfNeeded(withDevice: device), +// let sampleTexture = makeSampleTextureIfNeeded(withDevice: device), +// let state = sampleComputeState +// else { +// throw MetaballsError.metalError("Missing Metal buffers or compute state") +// } +// +// let encoder = buffer.makeComputeCommandEncoder() +// encoder.setComputePipelineState(state) +// encoder.setBuffer(parametersBuffer, offset: 0, at: 0) +// encoder.setBuffer(ballBuffer, offset: 0, at: 1) +// encoder.setTexture(sampleTexture, at: 0) +// encoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) +// encoder.endEncoding() +// +// updateParametersBuffer() +// +// return encoder +// } }