diff --git a/Foil.xcodeproj/xcshareddata/xcschemes/Foil.xcscheme b/Foil.xcodeproj/xcshareddata/xcschemes/Foil.xcscheme new file mode 100644 index 0000000..57e6adb --- /dev/null +++ b/Foil.xcodeproj/xcshareddata/xcschemes/Foil.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Foil.xcodeproj/xcuserdata/rob.xcuserdatad/xcschemes/xcschememanagement.plist b/Foil.xcodeproj/xcuserdata/rob.xcuserdatad/xcschemes/xcschememanagement.plist index e118444..be42479 100644 --- a/Foil.xcodeproj/xcuserdata/rob.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Foil.xcodeproj/xcuserdata/rob.xcuserdatad/xcschemes/xcschememanagement.plist @@ -10,5 +10,13 @@ 0 + SuppressBuildableAutocreation + + D4AC68B3245CF43F00917569 + + primary + + + diff --git a/Foil/Base.lproj/Main.storyboard b/Foil/Base.lproj/Main.storyboard index 0ff74c7..fb54775 100644 --- a/Foil/Base.lproj/Main.storyboard +++ b/Foil/Base.lproj/Main.storyboard @@ -679,7 +679,7 @@ - + diff --git a/Foil/FoilViewController.swift b/Foil/FoilViewController.swift index f110d6a..8e8f755 100644 --- a/Foil/FoilViewController.swift +++ b/Foil/FoilViewController.swift @@ -14,17 +14,24 @@ class FoilViewController: NSViewController, MTKViewDelegate { assert(false) } + let IntelGPUInMetalDevicesArray = 0 + let RadeonGPUInMetalDevicesArray = 1 + + static let frameTime: Double = 1.0 / 60.0 + static let second: Double = frameTime * 60.0 + static let metersPerSecond: Double = 600.0 + // Table with various simulation configurations. Apps would typically load simulation parameters // such as these from a file or UI controls, but to simplify the sample and focus on Metal usage, // this table is hardcoded static let FoilSimulationConfigTable = [ // damping softening numBodies clusterScale velocityScale renderScale renderBodies simInterval simDuration - FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 16384, clusterScale: 1.54, velocityScale: 8, renderScale: 25.0, renderBodies: 16384, simInterval: 0.0160, simDuration: 500.0), - FoilSimulationConfig(damping: 1.0, softeningSqr: 0.100, numBodies: 16384, clusterScale: 0.32, velocityScale: 276, renderScale: 2.5, renderBodies: 16384, simInterval: 0.0006, simDuration: 5.0), - FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 16384, clusterScale: 0.68, velocityScale: 20, renderScale: 1700.0, renderBodies: 16384, simInterval: 0.0160, simDuration: 5.0), - FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 16384, clusterScale: 1.54, velocityScale: 8, renderScale: 25.0, renderBodies: 16384, simInterval: 0.0160, simDuration: 5.0), - FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 16384, clusterScale: 6.04, velocityScale: 0, renderScale: 300.0, renderBodies: 16384, simInterval: 0.0160, simDuration: 5.0), - FoilSimulationConfig(damping: 1.0, softeningSqr: 0.145, numBodies: 16384, clusterScale: 0.32, velocityScale: 272, renderScale: 2.5, renderBodies: 16384, simInterval: 0.0006, simDuration: 5.0) + FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 4096, clusterScale: 0.32, velocityScale: metersPerSecond / 30, renderScale: 2.5, renderBodies: 4096, simInterval: frameTime / 30, simDuration: 10.0 * second / 30), + FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 4096, clusterScale: 6.04, velocityScale: 0, renderScale: 75.0, renderBodies: 4096, simInterval: frameTime, simDuration: 10.0 * second), + FoilSimulationConfig(damping: 1.0, softeningSqr: 0.145, numBodies: 4096, clusterScale: 0.32, velocityScale: metersPerSecond / 30, renderScale: 2.5, renderBodies: 4096, simInterval: frameTime / 30, simDuration: 10.0 * second / 30), + FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 4096, clusterScale: 1.54, velocityScale: metersPerSecond / 30, renderScale: 75.0, renderBodies: 4096, simInterval: frameTime, simDuration: 10.0 * second), + FoilSimulationConfig(damping: 1.0, softeningSqr: 0.100, numBodies: 4096, clusterScale: 0.68, velocityScale: metersPerSecond / 30, renderScale: 1000.0, renderBodies: 4096, simInterval: frameTime, simDuration: 10.0 * second), + FoilSimulationConfig(damping: 1.0, softeningSqr: 1.000, numBodies: 4096, clusterScale: 1.54, velocityScale: metersPerSecond / 30, renderScale: 75.0, renderBodies: 4096, simInterval: frameTime, simDuration: 10.0 * second) ] static let FoilNumSimulationConfigs = FoilSimulationConfigTable.count @@ -33,24 +40,49 @@ class FoilViewController: NSViewController, MTKViewDelegate { var renderer: FoilRenderer! var simulation: FoilSimulation! + // The current time (in simulation time units) that the simulation has processed var simulationTime: CFAbsoluteTime = 0 + + // When rendering is paused (such as immediately after a simulation has completed), the time + // to unpause and continue simulations. var continuationTime: CFAbsoluteTime = 0 var computeDevice: MTLDevice! + // Index of the current simulation config in the simulation config table var configNum = 0 + + // Currently running simulation config var config: FoilSimulationConfig! + // Command queue used when simulation and renderer are using the same device. + // Set to nil when using different devices var commandQueue: MTLCommandQueue! + // When true, stop running any more simulations (such as when the window closes). var terminateAllSimulations = false + + // When true, restart the current simulation if it was interrupted and data could not + // be retrieved var restartSimulation = false + // UI showing current simulation name and percentage complete @IBOutlet var _simulationName: NSTextField! @IBOutlet var _simulationPercentage: NSTextField! + // Timer used to make the text fields blink when results have been completed var blinker: Timer! + let viewControllerispatchQueue = DispatchQueue( + label: "viewController.q", qos: .default, attributes: [/*serial*/], + target: DispatchQueue.global(qos: .default) + ) + + let dataUpdateDispatchQueue = DispatchQueue( + label: "dataUpdate.q", qos: .default, attributes: [/*serial*/], + target: DispatchQueue.global(qos: .default) + ) + override func viewDidLoad() { super.viewDidLoad() selectDevices() @@ -58,16 +90,10 @@ class FoilViewController: NSViewController, MTKViewDelegate { _view.delegate = self } - override var representedObject: Any? { - didSet { assert(false) } - } - - static let FoilSecondsToPresentSimulationResults = CFTimeInterval(4.0) - override func viewDidAppear() { beginSimulation() } override func viewDidDisappear() { - LikeObjcSync.synced(self) { + viewControllerispatchQueue.sync { // Stop simulation if on another thread self.simulation.halt = true; @@ -81,39 +107,16 @@ class FoilViewController: NSViewController, MTKViewDelegate { precondition(!availableDevices.isEmpty, "Metal is not supported on this Mac") - // Select compute device - for device in availableDevices { - if device.isRemovable { - // Select removable device if available since if there is one, it's probably the most - // powerful device available - computeDevice = device; - break; - } else if device.isHeadless { - // Select headless device since if there is one it's probably dedicated to compute - // tasks - computeDevice = device; - } - } - - if computeDevice == nil { - guard let cd = MTLCreateSystemDefaultDevice() else { fatalError() } - computeDevice = cd - } - + computeDevice = availableDevices[RadeonGPUInMetalDevicesArray] NSLog("Selected compute device: \(computeDevice.name)") - // Select renderer device (stored as _view.device) - - // Query for device driving the display - let key = NSDeviceDescriptionKey("NSScreenNumber") - let viewDisplayID = (_view.window?.screen?.deviceDescription[key] as? CGDirectDisplayID) ?? CGDirectDisplayID() - - let rendererDevice = CGDirectDisplayCopyCurrentMetalDevice(viewDisplayID); + // Select renderer device + let rendererDevice = availableDevices[RadeonGPUInMetalDevicesArray] if rendererDevice !== _view.device { _view.device = rendererDevice; - NSLog("New render device: '\(_view.device!.name)'") + NSLog("New render device: '\(rendererDevice.name)'") renderer = FoilRenderer(_view) renderer.drawableSizeWillChange(size: _view.drawableSize) @@ -151,12 +154,10 @@ class FoilViewController: NSViewController, MTKViewDelegate { let updateHandler: (NSData, CFAbsoluteTime) -> () = { // Update the renderer's position data so that it can show forward progress -// print("update handler st = \($1)") self.updateWithNewPositionData(updateData: $0, forSimulationTime: $1) } let dataProvider: (NSData, NSData, CFAbsoluteTime) -> () = { - print("dataProvider st = \($2)") self.handleFullyProvidedSetOfPositionData(positionData: $0, velocityData: $1, forSimulationTime: $2) } @@ -166,12 +167,12 @@ class FoilViewController: NSViewController, MTKViewDelegate { /// Receive and update of new positions for the simulation time given. func updateWithNewPositionData(updateData: NSData, forSimulationTime simulationTime: CFAbsoluteTime) { // Lock with updateData so thus thread does not update data during an update on another thread - LikeObjcSync.synced(updateData) { + dataUpdateDispatchQueue.sync { // Update the renderer's position data so that it can show forward progress self.renderer.providePositionData(data: updateData) } - LikeObjcSync.synced(self) { + viewControllerispatchQueue.sync { // Lock around _simulation time since it will be accessed on another thread self.simulationTime = simulationTime; } @@ -183,14 +184,12 @@ class FoilViewController: NSViewController, MTKViewDelegate { positionData: NSData, velocityData: NSData, forSimulationTime simulationTime: CFAbsoluteTime ) { - LikeObjcSync.synced(self) { + viewControllerispatchQueue.sync { if self.terminateAllSimulations { NSLog("Terminating all simulations") return } - print("here self.simulationTime = simulationTime: \(self.simulationTime) = \(simulationTime), \(config.simDuration)") - self.simulationTime = simulationTime; if simulationTime >= config.simDuration { @@ -231,6 +230,8 @@ class FoilViewController: NSViewController, MTKViewDelegate { /// Called whenever view changes orientation or layout is changed func drawableSizeWillChange(size: CGSize) { renderer.drawableSizeWillChange(size: size) } + static let FoilSecondsToPresentSimulationResults = CFTimeInterval(4.0) + /// Called whenever the view needs to render func draw(in view: MTKView) { // Number of bodies to render this frame @@ -303,18 +304,15 @@ class FoilViewController: NSViewController, MTKViewDelegate { commandBuffer.popDebugGroup() - let st = simulationTime simulationTime += Double(config.simInterval) - print("draw st before \(st) after \(simulationTime)") } else { - print("no draw ish \(simulationTime)") renderer.drawProvidedPositionDataWithNumBodies(numParticles: numBodies, inView: _view) } var percentComplete = 0 // Lock when using _simulationTime since it can be updated on a separate thread - LikeObjcSync.synced(self) { + viewControllerispatchQueue.sync { percentComplete = Int((simulationTime / config.simDuration) * 100) } diff --git a/Foil/LikeObjcSync.swift b/Foil/LikeObjcSync.swift index 22c9310..59bfe17 100644 --- a/Foil/LikeObjcSync.swift +++ b/Foil/LikeObjcSync.swift @@ -1,6 +1,6 @@ import Foundation -class LikeObjcSync { +class ikeObjcSync { public class func synced(_ lock: Any, closure: () -> ()) { objc_sync_enter(lock) defer { objc_sync_exit(lock) } diff --git a/Renderer/FoilMathUtilities.swift b/Renderer/FoilMathUtilities.swift index 4dc3b7b..7b8b259 100644 --- a/Renderer/FoilMathUtilities.swift +++ b/Renderer/FoilMathUtilities.swift @@ -1,34 +1,46 @@ import simd -func generate_random_vector(min: Float, max: Float) -> vector_float3 { - let range = max - min; +enum FoilMath { + static func generateRandomNormalizedVector(_ min: Float, _ max: Float, _ maxlength: Float) -> vector_float3 { + var rand = vector_float3() - let x = Float.random(in: 0..<1) * range + min; - let y = Float.random(in: 0..<1) * range + min; - let z = Float.random(in: 0..<1) * range + min; + repeat { + rand = generateRandomVector(min, max); + } while(simd_length(rand) > maxlength) - return vector_float3(x, y, z) -} + return simd_normalize(rand) + } -func matrix_ortho_left_hand( - left: Float, right: Float, bottom: Float, top: Float, nearZ: Float, farZ: Float -) -> matrix_float4x4 { - return matrix_make_rows( - 2 / (right - left), 0, 0, (left + right) / (left - right), - 0, 2 / (top - bottom), 0, (top + bottom) / (bottom - top), - 0, 0, 1 / (farZ - nearZ), nearZ / (nearZ - farZ), - 0, 0, 0, 1 ); -} + static func generateRandomVector(_ min: Float, _ max: Float) -> vector_float3 { + let range = max - min; + + let x = Float.random(in: 0..<1) * range + min; + let y = Float.random(in: 0..<1) * range + min; + let z = Float.random(in: 0..<1) * range + min; + + return vector_float3(x, y, z) + } + + static func matrixOrthoLeftHand( + left: Float, right: Float, bottom: Float, top: Float, nearZ: Float, farZ: Float + ) -> matrix_float4x4 { + return matriMakeRows( + 2 / (right - left), 0, 0, (left + right) / (left - right), + 0, 2 / (top - bottom), 0, (top + bottom) / (bottom - top), + 0, 0, 1 / (farZ - nearZ), nearZ / (nearZ - farZ), + 0, 0, 0, 1 ); + } -func matrix_make_rows( - _ m00: Float, _ m10: Float, _ m20: Float, _ m30: Float, - _ m01: Float, _ m11: Float, _ m21: Float, _ m31: Float, - _ m02: Float, _ m12: Float, _ m22: Float, _ m32: Float, - _ m03: Float, _ m13: Float, _ m23: Float, _ m33: Float -) -> matrix_float4x4 { - return matrix_float4x4([ - [ m00, m01, m02, m03 ], // each line here provides column data - [ m10, m11, m12, m13 ], - [ m20, m21, m22, m23 ], - [ m30, m31, m32, m33 ] ] ) + static func matriMakeRows( + _ m00: Float, _ m10: Float, _ m20: Float, _ m30: Float, + _ m01: Float, _ m11: Float, _ m21: Float, _ m31: Float, + _ m02: Float, _ m12: Float, _ m22: Float, _ m32: Float, + _ m03: Float, _ m13: Float, _ m23: Float, _ m33: Float + ) -> matrix_float4x4 { + return matrix_float4x4([ + [ m00, m01, m02, m03 ], // each line here provides column data + [ m10, m11, m12, m13 ], + [ m20, m21, m22, m23 ], + [ m30, m31, m32, m33 ] ] ) + } } diff --git a/Renderer/FoilRenderer.swift b/Renderer/FoilRenderer.swift index e997602..b16c899 100644 --- a/Renderer/FoilRenderer.swift +++ b/Renderer/FoilRenderer.swift @@ -2,22 +2,18 @@ import Foundation import MetalKit class FoilRenderer: NSObject { - static let maxConcurrentRenderBuffers = 3 - - // The point size (in pixels) of rendered bodied + // The point size (in pixels) of rendered bodies static let bodyPointSize: Float = 15; // Size of gaussian map to create rounded smooth points static let GaussianMapSize = 64 - let pipelineThrottle: DispatchSemaphore - let gaussianMap: MTLTexture var device: MTLDevice! var commandQueue: MTLCommandQueue! - var colors: MTLBuffer? + var colorsBuffer: MTLBuffer? // Metal objects var depthState: MTLDepthStencilState! @@ -33,16 +29,17 @@ class FoilRenderer: NSObject { var renderScale: Float = 0 + let rendererDispatchQueue = DispatchQueue( + label: "renderer.q", qos: .default, attributes: [/*serial*/], + target: DispatchQueue.global(qos: .default) + ) + /// Initialize with the MetalKit view with the Metal device used to render. This MetalKit view /// object will also be used to set the pixelFormat and other properties of the drawable init(_ mtkView: MTKView) { self.device = mtkView.device! self.commandQueue = self.device.makeCommandQueue()! - self.pipelineThrottle = DispatchSemaphore( - value: FoilRenderer.maxConcurrentRenderBuffers - ) - self.gaussianMap = FoilRenderer.generateGaussianMap(self.device) super.init() @@ -60,18 +57,8 @@ class FoilRenderer: NSObject { numBodies: Int, view: MTKView ) { - // Limit the action in the pipeline; see whether we can increase - // the max; the original code is kind of old - pipelineThrottle.wait() - commandBuffer.pushDebugGroup("Draw Simulation Data") - // Add completion hander which signals pipelineThrottle when Metal and the GPU has fully - // finished processing the commands encoded this frame. This indicates when the dynamic - // buffers, written to this frame, will no longer be needed by Metal and the GPU, meaning the - // buffer contents can be changed without corrupting rendering - commandBuffer.addCompletedHandler { [weak self] _ in self?.pipelineThrottle.signal() } - setNumRenderBodies(numBodies) updateState() @@ -89,14 +76,14 @@ class FoilRenderer: NSObject { precondition(positionsBuffer.length > 0) // Synchronize since positions buffer may be created on another thread - LikeObjcSync.synced(self) { + rendererDispatchQueue.sync { renderEncoder.setVertexBuffer( positionsBuffer, offset: 0, index: FoilRenderBufferIndex.positions.rawValue ) } renderEncoder.setVertexBuffer( - colors, offset: 0, index: FoilRenderBufferIndex.colors.rawValue + colorsBuffer, offset: 0, index: FoilRenderBufferIndex.colors.rawValue ) renderEncoder.setVertexBuffer( @@ -218,23 +205,19 @@ class FoilRenderer: NSObject { depthStateDesc.isDepthWriteEnabled = true; depthState = device.makeDepthStencilState(descriptor: depthStateDesc) - // Create and allocate the dynamic uniform buffer objects. - for i in 0...stride - guard let dub = device.makeBuffer(length: stride, options: storageMode) - else { fatalError() } + // Indicate shared storage so that both the CPU can access the buffers + let storageMode = MTLResourceOptions.storageModeShared + let stride = MemoryLayout.stride + guard let dub = device.makeBuffer(length: stride, options: storageMode) + else { fatalError() } - dub.label = "UniformBuffer\(i)" - dynamicUniformBuffers.append(dub) - } + dub.label = "UniformBuffer" + dynamicUniformBuffers.append(dub) // Initialize number of bodies to render setNumRenderBodies(64 * 1024) commandQueue = device.makeCommandQueue() - } /// Update any render state (including updating dynamically changing Metal buffers) @@ -247,7 +230,7 @@ class FoilRenderer: NSObject { } func providePositionData(data: NSData) { - LikeObjcSync.synced(self) { + rendererDispatchQueue.sync { // Cast from 'const void *' to 'void *' which is okay in this case since updateData was // created with -[NSData initWithBytesNoCopy:length:deallocator:] and underlying memory was // allocated with vm_allocate @@ -267,28 +250,28 @@ class FoilRenderer: NSObject { } func setNumRenderBodies(_ numBodies: Int) { - if colors == nil || ((colors!.length / MemoryLayout.stride) < numBodies) { + if colorsBuffer == nil || ((colorsBuffer!.length / MemoryLayout.stride) < numBodies) { // If the number of colors stored is less than the number of bodies, recreate the color buffer - let bufferSize = numBodies * MemoryLayout.stride + let bufferSize = numBodies * MemoryLayout.stride - colors = device.makeBuffer(length: bufferSize, options: .storageModeManaged) + colorsBuffer = device.makeBuffer(length: bufferSize, options: .storageModeManaged) - colors!.label = "Colors"; + colorsBuffer!.label = "Colors"; - let contents = colors!.contents().bindMemory(to: vector_uchar4.self, capacity: numBodies) + let colors = colorsBuffer!.contents().bindMemory(to: vector_uchar4.self, capacity: numBodies) for i in 0.. Float { return simd_length(vector) } - static func vector_normalize(_ vector: vector_float3) -> vector_float3 { return simd_normalize(vector) } - - /// Utility function providing a random with which to initialize simulation - static func generate_random_normalized_vector(min: Float, max: Float, maxlength: Float) -> vector_float3 { - var rand = vector_float3() - - repeat { - rand = generate_random_vector(min: min, max: max); - } while(vector_length(rand) > maxlength) - - return vector_normalize(rand) - } - /// Initialize Metal objects and set simulation parameters func createMetalObjectsAndMemory() { // Create compute pipeline for simulation @@ -250,14 +247,6 @@ class FoilSimulation { return (buffer, data) } - static func vector_dot(_ nrpos: vector_float3, _ axis: vector_float3) -> Float { - return simd_dot(nrpos, axis) - } - - static func vector_cross(_ position: vector_float3, _ axis: vector_float3) -> vector_float3 { - return simd_cross(position, axis) - } - /// Set the initial positions and velocities of the simulation based upon the simulation's config func initializeData() { let pscale = config.clusterScale @@ -273,25 +262,25 @@ class FoilSimulation { let velocities = self.velocities[oldBufferIndex].contents().assumingMemoryBound(to: vector_float4.self) for i in 0.. Void ) { - commandBuffer.addCompletedHandler { _ in - print("runAsyncLoopWithUpdateHandler.6(\(loopCounter))") - updateHandler() - print("runAsyncLoopWithUpdateHandler.7(\(loopCounter))") - } + commandBuffer.addCompletedHandler { _ in updateHandler() } } }