[MetaballsKit] Do not compute a texture, just provide buffers for balls and parameter data

This commit is contained in:
Eryn Wells 2017-08-05 11:10:06 -07:00
parent 2441bd0b76
commit 8b4f2d8cc2

View file

@ -14,13 +14,13 @@ public enum MetaballsError: Error {
} }
public struct Ball { public struct Ball {
let radius: CGFloat let radius: Float
var position = CGPoint() var position = Point()
var velocity = CGVector() var velocity = Vector()
internal var bounds: CGRect { internal var bounds: CGRect {
let diameter = radius * 2 let diameter = CGFloat(radius * 2)
return CGRect(x: position.x - radius, y: position.y - radius, width: diameter, height: diameter) return CGRect(x: CGFloat(position.x - radius), y: CGFloat(position.y - radius), width: diameter, height: diameter)
} }
internal mutating func update() { internal mutating func update() {
@ -29,22 +29,35 @@ public struct Ball {
} }
} }
extension Ball: CustomStringConvertible {
public var description: String {
return "<Ball p:\(position), r:\(radius), v:\(velocity)>"
}
}
public class Field { public class Field {
public var size: CGSize { public var size: CGSize {
didSet { didSet {
if size != oldValue { if size != oldValue {
NSLog("Updating size of field: old:\(oldValue), new:\(size)")
let numberOfBallsBeforeFilter = balls.count let numberOfBallsBeforeFilter = balls.count
// Remove balls that fall outside the new bounds. // Remove balls that fall outside the new bounds.
balls = balls.filter { bounds.contains($0.bounds) } balls = balls.filter { bounds.contains($0.bounds) }
// Update Metal state as needed. // Update Metal state as needed.
updateThreadgroupSizes(withFieldSize: size) // updateThreadgroupSizes(withFieldSize: size)
parametersBuffer = nil // parametersBuffer = nil
sampleTexture = nil // sampleTexture = nil
if numberOfBallsBeforeFilter != balls.count { if numberOfBallsBeforeFilter != balls.count {
ballBuffer = nil 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. // Update position of ball.
ball.update() 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. // 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 { } else {
// Do collision detection with walls. // Do collision detection with walls.
let ballBounds = ball.bounds let ballBounds = ball.bounds
@ -83,33 +96,33 @@ public class Field {
} }
} }
public func add(ballWithRadius radius: CGFloat) { public func add(ballWithRadius radius: Float) {
NSLog("Adding ball with r=\(radius); fieldSize=\(size)") let insetBounds = bounds.insetBy(dx: CGFloat(radius), dy: CGFloat(radius))
let insetBounds = bounds.insetBy(dx: radius, dy: radius)
// let x = CGFloat(UInt32(insetBounds.minX) + arc4random_uniform(UInt32(insetBounds.width))) // let x = CGFloat(UInt32(insetBounds.minX) + arc4random_uniform(UInt32(insetBounds.width)))
// let y = CGFloat(UInt32(insetBounds.minY) + arc4random_uniform(UInt32(insetBounds.height))) // 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. // 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) balls.append(ball)
NSLog("Added ball \(ball); fieldSize=\(size)")
} }
// MARK: - Metal Configuration // MARK: - Metal Configuration
private var device: MTLDevice? private var device: MTLDevice?
private var sampleComputeState: MTLComputePipelineState? // private var sampleComputeState: MTLComputePipelineState?
private var parametersBuffer: MTLBuffer? public private(set) var parametersBuffer: MTLBuffer?
private var ballBuffer: MTLBuffer? public private(set) var ballBuffer: MTLBuffer?
public private(set) var sampleTexture: MTLTexture? // 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. // 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. /// Create the Metal buffer containing basic parameters of the simulation.
private func makeParametersBufferIfNeeded(withDevice device: MTLDevice) -> MTLBuffer? { private func makeParametersBufferIfNeeded(withDevice device: MTLDevice) -> MTLBuffer? {
if parametersBuffer == nil { if parametersBuffer == nil {
parametersBuffer = device.makeBuffer(length: MemoryLayout<Int>.size * 3, options: []) parametersBuffer = device.makeBuffer(length: MemoryLayout<Int>.stride * 3, options: [])
} }
return parametersBuffer return parametersBuffer
} }
@ -118,8 +131,8 @@ public class Field {
/// @param device The Metal device to use to create the buffer. /// @param device The Metal device to use to create the buffer.
/// @return A new buffer containing metaball data. /// @return A new buffer containing metaball data.
private func makeBallBufferIfNeeded(withDevice device: MTLDevice) -> MTLBuffer? { private func makeBallBufferIfNeeded(withDevice device: MTLDevice) -> MTLBuffer? {
if ballBuffer == nil { if ballBuffer == nil && balls.count > 0 {
let sizeOfBall = MemoryLayout<Ball>.size let sizeOfBall = MemoryLayout<Float>.stride * 3 // A Ball in shader-land is a float3.
let length = balls.count * sizeOfBall let length = balls.count * sizeOfBall
ballBuffer = device.makeBuffer(length: length, options: []) 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. /// 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. /// @param device The Metal device to use to create the texture.
/// @return A new texture. /// @return A new texture.
private func makeSampleTextureIfNeeded(withDevice device: MTLDevice) -> MTLTexture? { // private func makeSampleTextureIfNeeded(withDevice device: MTLDevice) -> MTLTexture? {
if sampleTexture == nil { // if sampleTexture == nil {
let desc = MTLTextureDescriptor() // let desc = MTLTextureDescriptor()
desc.pixelFormat = .r16Float // desc.pixelFormat = .r16Float
desc.width = Int(size.width) // desc.width = Int(size.width)
desc.height = Int(size.height) // desc.height = Int(size.height)
desc.usage = [.shaderWrite, .shaderRead] // desc.usage = [.shaderWrite, .shaderRead]
sampleTexture = device.makeTexture(descriptor: desc) // sampleTexture = device.makeTexture(descriptor: desc)
} // }
return sampleTexture // return sampleTexture
} // }
/// Update the threadgroup divisions based on the size of the field. /// Update the threadgroup divisions based on the size of the field.
/// @param size The size of the field. /// @param size The size of the field.
private func updateThreadgroupSizes(withFieldSize size: CGSize) { // private func updateThreadgroupSizes(withFieldSize size: CGSize) {
let width = Int(size.width) // let width = Int(size.width)
let height = Int(size.height) // let height = Int(size.height)
threadgroupCount = MTLSize(width: width + threadgroupSize.width - 1, height: height + threadgroupSize.height - 1, depth: 1) // threadgroupCount = MTLSize(width: width + threadgroupSize.width - 1, height: height + threadgroupSize.height - 1, depth: 1)
} // }
/// Copy metaballs data into the parameters buffer. /// Copy metaballs data into the parameters buffer.
private func updateParametersBuffer() { public func updateBuffers() throws {
guard let parameters = parametersBuffer, guard let device = self.device else {
let balls = ballBuffer throw MetaballsError.metalError("Missing Metal device for update")
}
guard let parameters = makeParametersBufferIfNeeded(withDevice: device),
let balls = makeBallBufferIfNeeded(withDevice: device)
else { else {
return throw MetaballsError.metalError("Couldn't create buffers")
} }
var ptr = parameters.contents() var ptr = parameters.contents()
var width = Int(size.width) var width = Int(size.width)
ptr = writeValue(value: &width, to: ptr) ptr = write(value: &width, to: ptr)
var height = Int(size.height) var height = Int(size.height)
ptr = writeValue(value: &height, to: ptr) ptr = write(value: &height, to: ptr)
var numberOfBalls = self.balls.count var numberOfBalls = self.balls.count
ptr = writeValue(value: &numberOfBalls, to: ptr) ptr = write(value: &numberOfBalls, to: ptr)
ptr = balls.contents() ptr = balls.contents()
for ball in self.balls { for var ball in self.balls {
var radius = Float(ball.radius) ptr = write(value: &ball.position.x, to: ptr)
ptr = writeValue(value: &radius, to: ptr) ptr = write(value: &ball.position.y, to: ptr)
var r = ball.radius
var posX = Float(ball.position.x) ptr = write(value: &r, to: ptr)
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)
} }
} }
private func writeValue<T>(value: inout T, to ptr: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { private func write<T>(value: inout T, to ptr: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer {
let sizeOfType = MemoryLayout<T>.size let sizeOfType = MemoryLayout<T>.stride
ptr.copyBytes(from: &value, count: sizeOfType) ptr.copyBytes(from: &value, count: sizeOfType)
return ptr.advanced(by: sizeOfType) return ptr.advanced(by: sizeOfType)
} }
@ -194,39 +203,42 @@ public class Field {
guard self.device == nil else { guard self.device == nil else {
return return
} }
NSLog("Setting up Metal")
self.device = device 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? { // public func computePipelineStateForSamplingKernel(withDevice device: MTLDevice) throws -> MTLComputePipelineState? {
let bundle = Bundle(for: type(of: self)) // let bundle = Bundle(for: type(of: self))
let library = try device.makeDefaultLibrary(bundle: bundle) // let library = try device.makeDefaultLibrary(bundle: bundle)
guard let samplingKernel = library.makeFunction(name: "sampleFieldKernel") else { // guard let samplingKernel = library.makeFunction(name: "sampleFieldKernel") else {
throw MetaballsError.metalError("Unable to create sampling kernel function") // throw MetaballsError.metalError("Unable to create sampling kernel function")
} // }
let state = try device.makeComputePipelineState(function: samplingKernel) // let state = try device.makeComputePipelineState(function: samplingKernel)
return state // return state
} // }
public func computeEncoderForSamplingKernel(withDevice device: MTLDevice, commandBuffer buffer: MTLCommandBuffer) throws -> MTLComputeCommandEncoder { // public func computeEncoderForSamplingKernel(withDevice device: MTLDevice, commandBuffer buffer: MTLCommandBuffer) throws -> MTLComputeCommandEncoder {
guard let parametersBuffer = makeParametersBufferIfNeeded(withDevice: device), // guard let parametersBuffer = makeParametersBufferIfNeeded(withDevice: device),
let ballBuffer = makeBallBufferIfNeeded(withDevice: device), // let ballBuffer = makeBallBufferIfNeeded(withDevice: device),
let sampleTexture = makeSampleTextureIfNeeded(withDevice: device), // let sampleTexture = makeSampleTextureIfNeeded(withDevice: device),
let state = sampleComputeState // let state = sampleComputeState
else { // else {
throw MetaballsError.metalError("Missing Metal buffers or compute state") // throw MetaballsError.metalError("Missing Metal buffers or compute state")
} // }
//
let encoder = buffer.makeComputeCommandEncoder() // let encoder = buffer.makeComputeCommandEncoder()
encoder.setComputePipelineState(state) // encoder.setComputePipelineState(state)
encoder.setBuffer(parametersBuffer, offset: 0, at: 0) // encoder.setBuffer(parametersBuffer, offset: 0, at: 0)
encoder.setBuffer(ballBuffer, offset: 0, at: 1) // encoder.setBuffer(ballBuffer, offset: 0, at: 1)
encoder.setTexture(sampleTexture, at: 0) // encoder.setTexture(sampleTexture, at: 0)
encoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize) // encoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
encoder.endEncoding() // encoder.endEncoding()
//
updateParametersBuffer() // updateParametersBuffer()
//
return encoder // return encoder
} // }
} }