├── .gitignore ├── FullShader.metal ├── LICENSE ├── LifeSaver ├── Info.plist ├── LifeSaverPreferences.xib ├── LifeSaverPreferencesController.swift └── LifeSaverView.swift ├── Metal-Life.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── Both.xcscheme │ ├── LifeSaver.xcscheme │ └── Metal-Life.xcscheme ├── Metal-Life ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon1024.png │ │ ├── icon256-1.png │ │ ├── icon256.png │ │ ├── icon512-1.png │ │ └── icon512.png │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── Life.swift ├── LifeViewController.swift └── Shaders.metal ├── README.md ├── Shared └── InitialPreferences.plist ├── assets ├── icon1024.png ├── icon256.png ├── icon512.png ├── logo-template.ai ├── thumbnail.png ├── thumbnail.psd └── thumbnail@2x.png └── screenshots ├── controls-320-high.gif ├── controls-480-high.gif ├── controls-640-low.gif ├── controls-640.gif ├── example-480.gif └── example.gif /.gitignore: -------------------------------------------------------------------------------- 1 | *.pbxuser 2 | !default.pbxuser 3 | *.mode1v3 4 | !default.mode1v3 5 | *.mode2v3 6 | !default.mode2v3 7 | *.perspectivev3 8 | !default.perspectivev3 9 | xcuserdata/ 10 | *.xcodeproj/* 11 | !*.xcodeproj/project.pbxproj 12 | !*.xcodeproj/xcshareddata/ 13 | !*.xcworkspace/contents.xcworkspacedata 14 | /*.gcno 15 | -------------------------------------------------------------------------------- /FullShader.metal: -------------------------------------------------------------------------------- 1 | // This is the older version of the shaders where all the logic 2 | // for calculating the grid and everything also lived here. 3 | // All the CPU would do was feed this shader an initial buffer of 4 | // unsigned chars and it did the rest. (The CPU would also 5 | // check on the buffer every few seconds to see if it had reached 6 | // an equilibrium, and restart it if needed. But otherwise, 7 | // everything was here.) 8 | // 9 | // Including this file for historical reasons, because I like it. 10 | // 11 | 12 | #include 13 | using namespace metal; 14 | 15 | 16 | constant float2 start(-12.0, -9.0); 17 | constant float2 end(12.0, 9.0); 18 | constant int gridWidth = 160; 19 | constant int gridHeight = 120; 20 | constant float pointSize = 9.0; 21 | constant int numCells = gridWidth * gridHeight; 22 | constant float intervalX = (end[0] - start[0]) / (gridWidth - 1); 23 | constant float intervalY = (end[1] - start[1]) / (gridHeight - 1); 24 | 25 | struct VertexOut { 26 | float4 position [[position]]; 27 | float pointSize [[point_size]]; 28 | uchar lifetime; 29 | }; 30 | 31 | struct NeighborIndices { 32 | uint4 cardinals; 33 | uint4 diagonals; 34 | }; 35 | 36 | struct Uniforms { 37 | float4x4 mvpMatrix; 38 | }; 39 | 40 | static NeighborIndices getNeighbors(unsigned int index) { 41 | NeighborIndices neighbors; 42 | 43 | neighbors.cardinals.x = index - 1; // west 44 | neighbors.cardinals.y = index + 1; // east 45 | neighbors.cardinals.z = (index - gridWidth) % numCells; // south 46 | neighbors.cardinals.w = (index + gridWidth) % numCells; // north 47 | 48 | neighbors.diagonals.x = neighbors.cardinals.z - 1; // southwest 49 | neighbors.diagonals.y = neighbors.cardinals.z + 1; // southeast 50 | neighbors.diagonals.z = neighbors.cardinals.w - 1; // northwest 51 | neighbors.diagonals.w = neighbors.cardinals.w + 1; // northeast 52 | 53 | if (index % gridWidth == 0) { 54 | // shift western up by one for wrap-around 55 | neighbors.cardinals.x += gridWidth; 56 | neighbors.diagonals.x += gridWidth; 57 | neighbors.diagonals.z += gridWidth; 58 | } 59 | if (index % gridWidth == gridWidth - 1) { 60 | // shift eastern down 61 | neighbors.cardinals.y -= gridWidth; 62 | neighbors.diagonals.y -= gridWidth; 63 | neighbors.diagonals.w -= gridWidth; 64 | } 65 | 66 | return neighbors; 67 | } 68 | 69 | 70 | vertex void lifeSimulate( 71 | const device uchar* current [[ buffer(0) ]], 72 | device uchar* future [[ buffer(1) ]], 73 | unsigned int vId [[ vertex_id ]] 74 | ) { 75 | 76 | NeighborIndices neighbors = getNeighbors(vId); 77 | uchar neighborCount = 0; 78 | 79 | if (current[neighbors.cardinals.x] > 0) { 80 | neighborCount++; 81 | } 82 | if (current[neighbors.cardinals.y] > 0) { 83 | neighborCount++; 84 | } 85 | if (current[neighbors.cardinals.z] > 0) { 86 | neighborCount++; 87 | } 88 | if (current[neighbors.cardinals.w] > 0) { 89 | neighborCount++; 90 | } 91 | if (current[neighbors.diagonals.x] > 0) { 92 | neighborCount++; 93 | } 94 | if (current[neighbors.diagonals.y] > 0) { 95 | neighborCount++; 96 | } 97 | if (current[neighbors.diagonals.z] > 0) { 98 | neighborCount++; 99 | } 100 | if (current[neighbors.diagonals.w] > 0) { 101 | neighborCount++; 102 | } 103 | 104 | future[vId] = current[vId]; 105 | if (current[vId] > 0) { 106 | if (neighborCount < 2) { 107 | future[vId] = 0; 108 | } 109 | else if (neighborCount > 3) { 110 | future[vId] = 0; 111 | } 112 | } 113 | else { 114 | if (neighborCount == 3) { 115 | future[vId] = 255; 116 | } 117 | } 118 | } 119 | 120 | vertex VertexOut lifeVertex ( 121 | const device uchar* current [[ buffer(0) ]], 122 | const device Uniforms& uniforms [[ buffer(2) ]], 123 | unsigned int vId [[ vertex_id ]] 124 | ) { 125 | 126 | int xIndex = vId % gridWidth; 127 | int yIndex = vId / gridWidth; 128 | 129 | float xCoord = start.x + (intervalX * xIndex); 130 | float yCoord = start.y + (intervalY * yIndex); 131 | 132 | VertexOut vOut; 133 | vOut.position = uniforms.mvpMatrix * float4(xCoord, yCoord, 0.0, 1.0); 134 | vOut.pointSize = pointSize; 135 | vOut.lifetime = current[vId]; 136 | 137 | return vOut; 138 | } 139 | 140 | fragment float4 lifeFragment(VertexOut interpolated [[stage_in]]) { 141 | float red = 0.0; 142 | if (interpolated.lifetime > 128) { 143 | red = 1.0; 144 | } 145 | else { 146 | red = 0.25; 147 | } 148 | return float4(red, 0.0, 0.0, 1.0); 149 | } 150 | 151 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Shane Liesegang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LifeSaver/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.3.1 19 | CFBundleVersion 20 | 1 21 | NSPrincipalClass 22 | LifeSaver.LifeSaverView 23 | 24 | 25 | -------------------------------------------------------------------------------- /LifeSaver/LifeSaverPreferences.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 44 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /LifeSaver/LifeSaverPreferencesController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import ScreenSaver 3 | import simd 4 | 5 | class LifeSaverPreferencesController: NSWindowController { 6 | let defaults: ScreenSaverDefaults? = ScreenSaverDefaults(forModuleWithName: Bundle(for: LifeSaverPreferencesController.self).bundleIdentifier!) 7 | 8 | @IBOutlet weak var bgColorWell: NSColorWell! 9 | @IBOutlet weak var fgColorWell: NSColorWell! 10 | @IBOutlet weak var pointSizeSlider: NSSlider! { 11 | didSet { 12 | pointSizeSlider.minValue = 1.0 13 | pointSizeSlider.maxValue = 13.0 14 | pointSizeSlider.doubleValue = 9.0 15 | } 16 | } 17 | @IBOutlet weak var pointSizeLabel: NSTextField! 18 | 19 | @IBOutlet weak var fpsMenu: NSPopUpButton! 20 | 21 | @IBAction func okButtonAction(_ sender: Any) { 22 | self.defaults?.set(pointSizeSlider.doubleValue, forKey: "pointSize") 23 | self.defaults?.set(bgColorWell.color.redComponent, forKey: "bgRed") 24 | self.defaults?.set(bgColorWell.color.greenComponent, forKey: "bgGreen") 25 | self.defaults?.set(bgColorWell.color.blueComponent, forKey: "bgBlue") 26 | self.defaults?.set(fgColorWell.color.redComponent, forKey: "fgRed") 27 | self.defaults?.set(fgColorWell.color.greenComponent, forKey: "fgGreen") 28 | self.defaults?.set(fgColorWell.color.blueComponent, forKey: "fgBlue") 29 | 30 | let req = fpsMenu.selectedItem?.title 31 | let reqInt = Int(req!)! 32 | self.defaults?.set(reqInt, forKey: "frameRate") 33 | 34 | self.defaults?.synchronize() 35 | NSApp.mainWindow?.endSheet(self.window!) 36 | } 37 | @IBAction func restoreButtonAction(_ sender: Any) { 38 | self.defaults?.set(9.0, forKey: "pointSize") 39 | self.defaults?.set(0.25, forKey: "bgRed") 40 | self.defaults?.set(0.0, forKey: "bgGreen") 41 | self.defaults?.set(0.0, forKey: "bgBlue") 42 | self.defaults?.set(1.0, forKey: "fgRed") 43 | self.defaults?.set(0.0, forKey: "fgGreen") 44 | self.defaults?.set(0.0, forKey: "fgBlue") 45 | self.defaults?.set(60, forKey: "frameRate") 46 | 47 | self.syncUI() 48 | } 49 | @IBAction func sliderValueChanged(_ sender: Any) { 50 | self.pointSizeLabel.stringValue = String(format: "%.1f", arguments: [self.pointSizeSlider.doubleValue]) 51 | } 52 | 53 | required init?(coder decoder: NSCoder) { 54 | super.init(coder: decoder) 55 | } 56 | override init(window: NSWindow?) { 57 | super.init(window: window) 58 | } 59 | 60 | override func windowDidLoad() { 61 | super.windowDidLoad() 62 | 63 | let prefsFilePath = Bundle(for: LifeSaverPreferencesController.self).path(forResource: "InitialPreferences", ofType: "plist") 64 | let nsDefaultPrefs = NSDictionary(contentsOfFile: prefsFilePath!) 65 | if let defaultPrefs: Dictionary = nsDefaultPrefs as? Dictionary { 66 | self.defaults?.register(defaults: defaultPrefs) 67 | } 68 | 69 | self.syncUI() 70 | } 71 | 72 | func syncUI() { 73 | self.pointSizeSlider.doubleValue = self.defaults!.double(forKey: "pointSize") 74 | self.pointSizeLabel.stringValue = String(format: "%.1f", arguments: [self.pointSizeSlider.doubleValue]) 75 | self.bgColorWell.color = NSColor( 76 | red: CGFloat(self.defaults!.double(forKey: "bgRed")), 77 | green: CGFloat(self.defaults!.double(forKey: "bgGreen")), 78 | blue: CGFloat(self.defaults!.double(forKey: "bgBlue")), 79 | alpha: 1.0 80 | ) 81 | self.fgColorWell.color = NSColor( 82 | red: CGFloat(self.defaults!.double(forKey: "fgRed")), 83 | green: CGFloat(self.defaults!.double(forKey: "fgGreen")), 84 | blue: CGFloat(self.defaults!.double(forKey: "fgBlue")), 85 | alpha: 1.0 86 | ) 87 | 88 | let frameRate = self.defaults!.integer(forKey: "frameRate") 89 | let menuItem = self.fpsMenu.item(withTitle: String(frameRate)) 90 | self.fpsMenu.select(menuItem) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /LifeSaver/LifeSaverView.swift: -------------------------------------------------------------------------------- 1 | import ScreenSaver 2 | import simd 3 | 4 | 5 | class LifeSaverView: ScreenSaverView { 6 | var lifeView: LifeView? 7 | var eqTimer: Timer? 8 | var prefsWindowController: LifeSaverPreferencesController? 9 | let defaults: ScreenSaverDefaults? = ScreenSaverDefaults(forModuleWithName: Bundle(for: LifeSaverPreferencesController.self).bundleIdentifier!) 10 | 11 | override init?(frame: NSRect, isPreview: Bool) { 12 | super.init(frame: frame, isPreview: isPreview) 13 | 14 | self.lifeView = LifeView(frame: frame) 15 | 16 | self.addSubview(lifeView!) 17 | } 18 | 19 | override func viewDidMoveToWindow() { 20 | super.viewDidMoveToWindow() 21 | 22 | if self.window == nil { 23 | self.stopAnimation() 24 | return 25 | } 26 | 27 | if (lifeView == nil) { 28 | return 29 | } 30 | 31 | lifeView!.setup() 32 | 33 | let prefsFilePath = Bundle(for: LifeSaverPreferencesController.self).path(forResource: "InitialPreferences", ofType: "plist") 34 | let nsDefaultPrefs = NSDictionary(contentsOfFile: prefsFilePath!) 35 | if let defaultPrefs: Dictionary = nsDefaultPrefs as? Dictionary { 36 | self.defaults?.register(defaults: defaultPrefs) 37 | } 38 | 39 | if (self.defaults != nil) { 40 | lifeView!.uniforms.pointSize = self.defaults!.float(forKey: "pointSize") 41 | lifeView!.uniforms.bgColor = float4( 42 | self.defaults!.float(forKey: "bgRed"), 43 | self.defaults!.float(forKey: "bgGreen"), 44 | self.defaults!.float(forKey: "bgBlue"), 45 | 1.0 46 | ) 47 | lifeView!.uniforms.fgColor = float4( 48 | self.defaults!.float(forKey: "fgRed"), 49 | self.defaults!.float(forKey: "fgGreen"), 50 | self.defaults!.float(forKey: "fgBlue"), 51 | 1.0 52 | ) 53 | let fr = self.defaults!.integer(forKey: "frameRate") 54 | lifeView!.numSkipFrames = (60 / fr) - 1 55 | } 56 | 57 | eqTimer = Timer.scheduledTimer(timeInterval: 3.0, target: self.lifeView!, selector: #selector(LifeView.checkForEquilibrium), userInfo: nil, repeats: true) 58 | } 59 | 60 | required init?(coder: NSCoder) { 61 | super.init(coder: coder) 62 | fatalError("init(coder:) has not been implemented") 63 | } 64 | 65 | override class func backingStoreType() -> NSWindow.BackingStoreType { 66 | return NSWindow.BackingStoreType.nonretained 67 | } 68 | 69 | override func startAnimation() { 70 | if (self.lifeView != nil) { 71 | self.lifeView!.start() 72 | } 73 | } 74 | 75 | override func stopAnimation() { 76 | if (self.lifeView != nil) { 77 | self.lifeView!.stop() 78 | } 79 | } 80 | 81 | override func animateOneFrame() { 82 | if (self.lifeView != nil) { 83 | self.lifeView!.render() 84 | } 85 | } 86 | 87 | override var isAnimating: Bool { 88 | get { 89 | if (self.lifeView == nil) { 90 | return false 91 | } 92 | return self.lifeView!.isAnimating() 93 | } 94 | } 95 | 96 | override var hasConfigureSheet: Bool { 97 | return true 98 | } 99 | 100 | override var configureSheet: NSWindow? { 101 | if let controller = self.prefsWindowController { 102 | return controller.window 103 | } 104 | 105 | self.prefsWindowController = LifeSaverPreferencesController(windowNibName: NSNib.Name(rawValue: "LifeSaverPreferences")) 106 | return self.prefsWindowController!.window 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Metal-Life.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | 3476829E1ECF537B009E9BD6 /* Both */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = 3476829F1ECF537B009E9BD6 /* Build configuration list for PBXAggregateTarget "Both" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | 347682A31ECF538C009E9BD6 /* PBXTargetDependency */, 17 | 347682A51ECF538E009E9BD6 /* PBXTargetDependency */, 18 | ); 19 | name = Both; 20 | productName = "com.shaneliesegang.Metal-Life.Both"; 21 | }; 22 | /* End PBXAggregateTarget section */ 23 | 24 | /* Begin PBXBuildFile section */ 25 | 341D1F4C1ECB39CD00D90465 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 3420D2751EC755090094C4B1 /* Shaders.metal */; }; 26 | 341D1F4D1ECB39D200D90465 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3420D2661EC7536B0094C4B1 /* AppDelegate.swift */; }; 27 | 341D1F4E1ECB39D600D90465 /* LifeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3420D2681EC7536B0094C4B1 /* LifeViewController.swift */; }; 28 | 341D1F4F1ECB39D900D90465 /* Life.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349DA86A1ECA18F70048C4FD /* Life.swift */; }; 29 | 341D1F501ECB39E500D90465 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3420D26C1EC7536B0094C4B1 /* Main.storyboard */; }; 30 | 341D1F511ECB39E700D90465 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3420D26A1EC7536B0094C4B1 /* Assets.xcassets */; }; 31 | 342B70DB1ECE98400074E764 /* LifeSaverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342B70DA1ECE98400074E764 /* LifeSaverView.swift */; }; 32 | 342B70DD1ECE9BE30074E764 /* Life.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349DA86A1ECA18F70048C4FD /* Life.swift */; }; 33 | 342B70DE1ECEA34A0074E764 /* Shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 3420D2751EC755090094C4B1 /* Shaders.metal */; }; 34 | 342B70E01ECEB3AC0074E764 /* LifeSaverPreferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 342B70DF1ECEB3AC0074E764 /* LifeSaverPreferences.xib */; }; 35 | 342B70E21ECEB5790074E764 /* LifeSaverPreferencesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342B70E11ECEB5790074E764 /* LifeSaverPreferencesController.swift */; }; 36 | 3476829B1ECEC70B009E9BD6 /* InitialPreferences.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3476829A1ECEC70B009E9BD6 /* InitialPreferences.plist */; }; 37 | 3476829C1ECF4918009E9BD6 /* InitialPreferences.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3476829A1ECEC70B009E9BD6 /* InitialPreferences.plist */; }; 38 | 347682A81ECF618A009E9BD6 /* thumbnail.png in Resources */ = {isa = PBXBuildFile; fileRef = 347682A61ECF618A009E9BD6 /* thumbnail.png */; }; 39 | 347682A91ECF618A009E9BD6 /* thumbnail@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 347682A71ECF618A009E9BD6 /* thumbnail@2x.png */; }; 40 | /* End PBXBuildFile section */ 41 | 42 | /* Begin PBXContainerItemProxy section */ 43 | 347682A21ECF538C009E9BD6 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 3420D25B1EC7536B0094C4B1 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = 341D1F3C1ECB39AC00D90465; 48 | remoteInfo = "Metal-Life"; 49 | }; 50 | 347682A41ECF538E009E9BD6 /* PBXContainerItemProxy */ = { 51 | isa = PBXContainerItemProxy; 52 | containerPortal = 3420D25B1EC7536B0094C4B1 /* Project object */; 53 | proxyType = 1; 54 | remoteGlobalIDString = 342B70CF1ECE98060074E764; 55 | remoteInfo = LifeSaver; 56 | }; 57 | /* End PBXContainerItemProxy section */ 58 | 59 | /* Begin PBXFileReference section */ 60 | 341D1F3D1ECB39AC00D90465 /* Metal-Life.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Metal-Life.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 61 | 3420D2661EC7536B0094C4B1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 62 | 3420D2681EC7536B0094C4B1 /* LifeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LifeViewController.swift; sourceTree = ""; }; 63 | 3420D26A1EC7536B0094C4B1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 64 | 3420D26D1EC7536B0094C4B1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 65 | 3420D26F1EC7536B0094C4B1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 66 | 3420D2751EC755090094C4B1 /* Shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = Shaders.metal; sourceTree = ""; }; 67 | 342B70D01ECE98060074E764 /* LifeSaver.saver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LifeSaver.saver; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 342B70D61ECE98060074E764 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../LifeSaver/Info.plist; sourceTree = ""; }; 69 | 342B70DA1ECE98400074E764 /* LifeSaverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifeSaverView.swift; sourceTree = ""; }; 70 | 342B70DF1ECEB3AC0074E764 /* LifeSaverPreferences.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LifeSaverPreferences.xib; sourceTree = ""; }; 71 | 342B70E11ECEB5790074E764 /* LifeSaverPreferencesController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LifeSaverPreferencesController.swift; sourceTree = ""; }; 72 | 3476829A1ECEC70B009E9BD6 /* InitialPreferences.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = InitialPreferences.plist; path = Shared/InitialPreferences.plist; sourceTree = ""; }; 73 | 347682A61ECF618A009E9BD6 /* thumbnail.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = thumbnail.png; path = assets/thumbnail.png; sourceTree = SOURCE_ROOT; }; 74 | 347682A71ECF618A009E9BD6 /* thumbnail@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "thumbnail@2x.png"; path = "assets/thumbnail@2x.png"; sourceTree = SOURCE_ROOT; }; 75 | 349DA86A1ECA18F70048C4FD /* Life.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Life.swift; sourceTree = ""; }; 76 | /* End PBXFileReference section */ 77 | 78 | /* Begin PBXFrameworksBuildPhase section */ 79 | 341D1F3A1ECB39AC00D90465 /* Frameworks */ = { 80 | isa = PBXFrameworksBuildPhase; 81 | buildActionMask = 2147483647; 82 | files = ( 83 | ); 84 | runOnlyForDeploymentPostprocessing = 0; 85 | }; 86 | 342B70CC1ECE98060074E764 /* Frameworks */ = { 87 | isa = PBXFrameworksBuildPhase; 88 | buildActionMask = 2147483647; 89 | files = ( 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXFrameworksBuildPhase section */ 94 | 95 | /* Begin PBXGroup section */ 96 | 3420D25A1EC7536B0094C4B1 = { 97 | isa = PBXGroup; 98 | children = ( 99 | 3420D2651EC7536B0094C4B1 /* Metal-Life */, 100 | 342B70D11ECE98060074E764 /* LifeSaver */, 101 | 3476829D1ECF4966009E9BD6 /* Shared */, 102 | 3420D2641EC7536B0094C4B1 /* Products */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | 3420D2641EC7536B0094C4B1 /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 341D1F3D1ECB39AC00D90465 /* Metal-Life.app */, 110 | 342B70D01ECE98060074E764 /* LifeSaver.saver */, 111 | ); 112 | name = Products; 113 | sourceTree = ""; 114 | }; 115 | 3420D2651EC7536B0094C4B1 /* Metal-Life */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 349DA86A1ECA18F70048C4FD /* Life.swift */, 119 | 3420D2751EC755090094C4B1 /* Shaders.metal */, 120 | 3420D2681EC7536B0094C4B1 /* LifeViewController.swift */, 121 | 3420D26C1EC7536B0094C4B1 /* Main.storyboard */, 122 | 3420D2661EC7536B0094C4B1 /* AppDelegate.swift */, 123 | 3420D26A1EC7536B0094C4B1 /* Assets.xcassets */, 124 | 3420D26F1EC7536B0094C4B1 /* Info.plist */, 125 | ); 126 | path = "Metal-Life"; 127 | sourceTree = ""; 128 | }; 129 | 342B70D11ECE98060074E764 /* LifeSaver */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 342B70DA1ECE98400074E764 /* LifeSaverView.swift */, 133 | 342B70E11ECEB5790074E764 /* LifeSaverPreferencesController.swift */, 134 | 342B70DF1ECEB3AC0074E764 /* LifeSaverPreferences.xib */, 135 | 342B70D61ECE98060074E764 /* Info.plist */, 136 | 347682A61ECF618A009E9BD6 /* thumbnail.png */, 137 | 347682A71ECF618A009E9BD6 /* thumbnail@2x.png */, 138 | ); 139 | path = LifeSaver; 140 | sourceTree = ""; 141 | }; 142 | 3476829D1ECF4966009E9BD6 /* Shared */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 3476829A1ECEC70B009E9BD6 /* InitialPreferences.plist */, 146 | ); 147 | name = Shared; 148 | sourceTree = ""; 149 | }; 150 | /* End PBXGroup section */ 151 | 152 | /* Begin PBXHeadersBuildPhase section */ 153 | 342B70CD1ECE98060074E764 /* Headers */ = { 154 | isa = PBXHeadersBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXHeadersBuildPhase section */ 161 | 162 | /* Begin PBXNativeTarget section */ 163 | 341D1F3C1ECB39AC00D90465 /* Metal-Life */ = { 164 | isa = PBXNativeTarget; 165 | buildConfigurationList = 341D1F4B1ECB39AD00D90465 /* Build configuration list for PBXNativeTarget "Metal-Life" */; 166 | buildPhases = ( 167 | 341D1F391ECB39AC00D90465 /* Sources */, 168 | 341D1F3A1ECB39AC00D90465 /* Frameworks */, 169 | 341D1F3B1ECB39AC00D90465 /* Resources */, 170 | ); 171 | buildRules = ( 172 | ); 173 | dependencies = ( 174 | ); 175 | name = "Metal-Life"; 176 | productName = "Metal-Life"; 177 | productReference = 341D1F3D1ECB39AC00D90465 /* Metal-Life.app */; 178 | productType = "com.apple.product-type.application"; 179 | }; 180 | 342B70CF1ECE98060074E764 /* LifeSaver */ = { 181 | isa = PBXNativeTarget; 182 | buildConfigurationList = 342B70D91ECE98060074E764 /* Build configuration list for PBXNativeTarget "LifeSaver" */; 183 | buildPhases = ( 184 | 342B70CB1ECE98060074E764 /* Sources */, 185 | 342B70CC1ECE98060074E764 /* Frameworks */, 186 | 342B70CD1ECE98060074E764 /* Headers */, 187 | 342B70CE1ECE98060074E764 /* Resources */, 188 | ); 189 | buildRules = ( 190 | ); 191 | dependencies = ( 192 | ); 193 | name = LifeSaver; 194 | productName = LifeSaver; 195 | productReference = 342B70D01ECE98060074E764 /* LifeSaver.saver */; 196 | productType = "com.apple.product-type.bundle"; 197 | }; 198 | /* End PBXNativeTarget section */ 199 | 200 | /* Begin PBXProject section */ 201 | 3420D25B1EC7536B0094C4B1 /* Project object */ = { 202 | isa = PBXProject; 203 | attributes = { 204 | LastSwiftUpdateCheck = 0830; 205 | LastUpgradeCheck = 0910; 206 | TargetAttributes = { 207 | 341D1F3C1ECB39AC00D90465 = { 208 | CreatedOnToolsVersion = 8.3.2; 209 | DevelopmentTeam = CFJYV723M9; 210 | LastSwiftMigration = 0910; 211 | ProvisioningStyle = Automatic; 212 | }; 213 | 342B70CF1ECE98060074E764 = { 214 | CreatedOnToolsVersion = 8.3.2; 215 | DevelopmentTeam = CFJYV723M9; 216 | LastSwiftMigration = 0910; 217 | ProvisioningStyle = Automatic; 218 | }; 219 | 3476829E1ECF537B009E9BD6 = { 220 | CreatedOnToolsVersion = 8.3.2; 221 | DevelopmentTeam = CFJYV723M9; 222 | ProvisioningStyle = Automatic; 223 | }; 224 | }; 225 | }; 226 | buildConfigurationList = 3420D25E1EC7536B0094C4B1 /* Build configuration list for PBXProject "Metal-Life" */; 227 | compatibilityVersion = "Xcode 3.2"; 228 | developmentRegion = English; 229 | hasScannedForEncodings = 0; 230 | knownRegions = ( 231 | en, 232 | Base, 233 | ); 234 | mainGroup = 3420D25A1EC7536B0094C4B1; 235 | productRefGroup = 3420D2641EC7536B0094C4B1 /* Products */; 236 | projectDirPath = ""; 237 | projectRoot = ""; 238 | targets = ( 239 | 3476829E1ECF537B009E9BD6 /* Both */, 240 | 341D1F3C1ECB39AC00D90465 /* Metal-Life */, 241 | 342B70CF1ECE98060074E764 /* LifeSaver */, 242 | ); 243 | }; 244 | /* End PBXProject section */ 245 | 246 | /* Begin PBXResourcesBuildPhase section */ 247 | 341D1F3B1ECB39AC00D90465 /* Resources */ = { 248 | isa = PBXResourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | 341D1F501ECB39E500D90465 /* Main.storyboard in Resources */, 252 | 341D1F511ECB39E700D90465 /* Assets.xcassets in Resources */, 253 | 3476829C1ECF4918009E9BD6 /* InitialPreferences.plist in Resources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | 342B70CE1ECE98060074E764 /* Resources */ = { 258 | isa = PBXResourcesBuildPhase; 259 | buildActionMask = 2147483647; 260 | files = ( 261 | 3476829B1ECEC70B009E9BD6 /* InitialPreferences.plist in Resources */, 262 | 347682A91ECF618A009E9BD6 /* thumbnail@2x.png in Resources */, 263 | 342B70E01ECEB3AC0074E764 /* LifeSaverPreferences.xib in Resources */, 264 | 347682A81ECF618A009E9BD6 /* thumbnail.png in Resources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXResourcesBuildPhase section */ 269 | 270 | /* Begin PBXSourcesBuildPhase section */ 271 | 341D1F391ECB39AC00D90465 /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 341D1F4C1ECB39CD00D90465 /* Shaders.metal in Sources */, 276 | 341D1F4D1ECB39D200D90465 /* AppDelegate.swift in Sources */, 277 | 341D1F4E1ECB39D600D90465 /* LifeViewController.swift in Sources */, 278 | 341D1F4F1ECB39D900D90465 /* Life.swift in Sources */, 279 | ); 280 | runOnlyForDeploymentPostprocessing = 0; 281 | }; 282 | 342B70CB1ECE98060074E764 /* Sources */ = { 283 | isa = PBXSourcesBuildPhase; 284 | buildActionMask = 2147483647; 285 | files = ( 286 | 342B70DE1ECEA34A0074E764 /* Shaders.metal in Sources */, 287 | 342B70E21ECEB5790074E764 /* LifeSaverPreferencesController.swift in Sources */, 288 | 342B70DD1ECE9BE30074E764 /* Life.swift in Sources */, 289 | 342B70DB1ECE98400074E764 /* LifeSaverView.swift in Sources */, 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | }; 293 | /* End PBXSourcesBuildPhase section */ 294 | 295 | /* Begin PBXTargetDependency section */ 296 | 347682A31ECF538C009E9BD6 /* PBXTargetDependency */ = { 297 | isa = PBXTargetDependency; 298 | target = 341D1F3C1ECB39AC00D90465 /* Metal-Life */; 299 | targetProxy = 347682A21ECF538C009E9BD6 /* PBXContainerItemProxy */; 300 | }; 301 | 347682A51ECF538E009E9BD6 /* PBXTargetDependency */ = { 302 | isa = PBXTargetDependency; 303 | target = 342B70CF1ECE98060074E764 /* LifeSaver */; 304 | targetProxy = 347682A41ECF538E009E9BD6 /* PBXContainerItemProxy */; 305 | }; 306 | /* End PBXTargetDependency section */ 307 | 308 | /* Begin PBXVariantGroup section */ 309 | 3420D26C1EC7536B0094C4B1 /* Main.storyboard */ = { 310 | isa = PBXVariantGroup; 311 | children = ( 312 | 3420D26D1EC7536B0094C4B1 /* Base */, 313 | ); 314 | name = Main.storyboard; 315 | sourceTree = ""; 316 | }; 317 | /* End PBXVariantGroup section */ 318 | 319 | /* Begin XCBuildConfiguration section */ 320 | 341D1F491ECB39AD00D90465 /* Debug */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 324 | COMBINE_HIDPI_IMAGES = YES; 325 | DEVELOPMENT_TEAM = CFJYV723M9; 326 | INFOPLIST_FILE = "Metal-Life/Info.plist"; 327 | INSTALL_PATH = /; 328 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 329 | PRODUCT_BUNDLE_IDENTIFIER = "com.shaneliesegang.Metal-Life"; 330 | PRODUCT_NAME = "$(TARGET_NAME)"; 331 | }; 332 | name = Debug; 333 | }; 334 | 341D1F4A1ECB39AD00D90465 /* Release */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 338 | COMBINE_HIDPI_IMAGES = YES; 339 | DEVELOPMENT_TEAM = CFJYV723M9; 340 | INFOPLIST_FILE = "Metal-Life/Info.plist"; 341 | INSTALL_PATH = /; 342 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 343 | PRODUCT_BUNDLE_IDENTIFIER = "com.shaneliesegang.Metal-Life"; 344 | PRODUCT_NAME = "$(TARGET_NAME)"; 345 | }; 346 | name = Release; 347 | }; 348 | 3420D2701EC7536B0094C4B1 /* Debug */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | ALWAYS_SEARCH_USER_PATHS = NO; 352 | CLANG_ANALYZER_NONNULL = YES; 353 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 354 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 355 | CLANG_CXX_LIBRARY = "libc++"; 356 | CLANG_ENABLE_MODULES = YES; 357 | CLANG_ENABLE_OBJC_ARC = YES; 358 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 359 | CLANG_WARN_BOOL_CONVERSION = YES; 360 | CLANG_WARN_COMMA = YES; 361 | CLANG_WARN_CONSTANT_CONVERSION = YES; 362 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 363 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 364 | CLANG_WARN_EMPTY_BODY = YES; 365 | CLANG_WARN_ENUM_CONVERSION = YES; 366 | CLANG_WARN_INFINITE_RECURSION = YES; 367 | CLANG_WARN_INT_CONVERSION = YES; 368 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 369 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 370 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 371 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 372 | CLANG_WARN_STRICT_PROTOTYPES = YES; 373 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 374 | CLANG_WARN_UNREACHABLE_CODE = YES; 375 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 376 | CODE_SIGN_IDENTITY = "-"; 377 | COPY_PHASE_STRIP = NO; 378 | DEBUG_INFORMATION_FORMAT = dwarf; 379 | ENABLE_STRICT_OBJC_MSGSEND = YES; 380 | ENABLE_TESTABILITY = YES; 381 | GCC_C_LANGUAGE_STANDARD = gnu99; 382 | GCC_DYNAMIC_NO_PIC = NO; 383 | GCC_NO_COMMON_BLOCKS = YES; 384 | GCC_OPTIMIZATION_LEVEL = 0; 385 | GCC_PREPROCESSOR_DEFINITIONS = ( 386 | "DEBUG=1", 387 | "$(inherited)", 388 | ); 389 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 390 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 391 | GCC_WARN_UNDECLARED_SELECTOR = YES; 392 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 393 | GCC_WARN_UNUSED_FUNCTION = YES; 394 | GCC_WARN_UNUSED_VARIABLE = YES; 395 | MACOSX_DEPLOYMENT_TARGET = 10.12; 396 | MTL_ENABLE_DEBUG_INFO = YES; 397 | ONLY_ACTIVE_ARCH = YES; 398 | SDKROOT = macosx; 399 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 400 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 401 | SWIFT_VERSION = 4.0; 402 | }; 403 | name = Debug; 404 | }; 405 | 3420D2711EC7536B0094C4B1 /* Release */ = { 406 | isa = XCBuildConfiguration; 407 | buildSettings = { 408 | ALWAYS_SEARCH_USER_PATHS = NO; 409 | CLANG_ANALYZER_NONNULL = YES; 410 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 411 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 412 | CLANG_CXX_LIBRARY = "libc++"; 413 | CLANG_ENABLE_MODULES = YES; 414 | CLANG_ENABLE_OBJC_ARC = YES; 415 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 416 | CLANG_WARN_BOOL_CONVERSION = YES; 417 | CLANG_WARN_COMMA = YES; 418 | CLANG_WARN_CONSTANT_CONVERSION = YES; 419 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 420 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 421 | CLANG_WARN_EMPTY_BODY = YES; 422 | CLANG_WARN_ENUM_CONVERSION = YES; 423 | CLANG_WARN_INFINITE_RECURSION = YES; 424 | CLANG_WARN_INT_CONVERSION = YES; 425 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 426 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 427 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 428 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 429 | CLANG_WARN_STRICT_PROTOTYPES = YES; 430 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 431 | CLANG_WARN_UNREACHABLE_CODE = YES; 432 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 433 | CODE_SIGN_IDENTITY = "-"; 434 | COPY_PHASE_STRIP = NO; 435 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 436 | ENABLE_NS_ASSERTIONS = NO; 437 | ENABLE_STRICT_OBJC_MSGSEND = YES; 438 | GCC_C_LANGUAGE_STANDARD = gnu99; 439 | GCC_NO_COMMON_BLOCKS = YES; 440 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 441 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 442 | GCC_WARN_UNDECLARED_SELECTOR = YES; 443 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 444 | GCC_WARN_UNUSED_FUNCTION = YES; 445 | GCC_WARN_UNUSED_VARIABLE = YES; 446 | MACOSX_DEPLOYMENT_TARGET = 10.12; 447 | MTL_ENABLE_DEBUG_INFO = NO; 448 | SDKROOT = macosx; 449 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 450 | SWIFT_VERSION = 4.0; 451 | }; 452 | name = Release; 453 | }; 454 | 342B70D71ECE98060074E764 /* Debug */ = { 455 | isa = XCBuildConfiguration; 456 | buildSettings = { 457 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 458 | CLANG_ENABLE_MODULES = YES; 459 | COMBINE_HIDPI_IMAGES = YES; 460 | DEVELOPMENT_TEAM = CFJYV723M9; 461 | INFOPLIST_FILE = LifeSaver/Info.plist; 462 | INSTALL_PATH = /; 463 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 464 | PRODUCT_BUNDLE_IDENTIFIER = com.shaneliesegang.LifeSaver; 465 | PRODUCT_NAME = "$(TARGET_NAME)"; 466 | SKIP_INSTALL = NO; 467 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 468 | WRAPPER_EXTENSION = saver; 469 | }; 470 | name = Debug; 471 | }; 472 | 342B70D81ECE98060074E764 /* Release */ = { 473 | isa = XCBuildConfiguration; 474 | buildSettings = { 475 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 476 | CLANG_ENABLE_MODULES = YES; 477 | COMBINE_HIDPI_IMAGES = YES; 478 | DEVELOPMENT_TEAM = CFJYV723M9; 479 | INFOPLIST_FILE = LifeSaver/Info.plist; 480 | INSTALL_PATH = /; 481 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 482 | PRODUCT_BUNDLE_IDENTIFIER = com.shaneliesegang.LifeSaver; 483 | PRODUCT_NAME = "$(TARGET_NAME)"; 484 | SKIP_INSTALL = NO; 485 | WRAPPER_EXTENSION = saver; 486 | }; 487 | name = Release; 488 | }; 489 | 347682A01ECF537B009E9BD6 /* Debug */ = { 490 | isa = XCBuildConfiguration; 491 | buildSettings = { 492 | DEVELOPMENT_TEAM = CFJYV723M9; 493 | PRODUCT_NAME = "$(TARGET_NAME)"; 494 | }; 495 | name = Debug; 496 | }; 497 | 347682A11ECF537B009E9BD6 /* Release */ = { 498 | isa = XCBuildConfiguration; 499 | buildSettings = { 500 | DEVELOPMENT_TEAM = CFJYV723M9; 501 | PRODUCT_NAME = "$(TARGET_NAME)"; 502 | }; 503 | name = Release; 504 | }; 505 | /* End XCBuildConfiguration section */ 506 | 507 | /* Begin XCConfigurationList section */ 508 | 341D1F4B1ECB39AD00D90465 /* Build configuration list for PBXNativeTarget "Metal-Life" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | 341D1F491ECB39AD00D90465 /* Debug */, 512 | 341D1F4A1ECB39AD00D90465 /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | 3420D25E1EC7536B0094C4B1 /* Build configuration list for PBXProject "Metal-Life" */ = { 518 | isa = XCConfigurationList; 519 | buildConfigurations = ( 520 | 3420D2701EC7536B0094C4B1 /* Debug */, 521 | 3420D2711EC7536B0094C4B1 /* Release */, 522 | ); 523 | defaultConfigurationIsVisible = 0; 524 | defaultConfigurationName = Release; 525 | }; 526 | 342B70D91ECE98060074E764 /* Build configuration list for PBXNativeTarget "LifeSaver" */ = { 527 | isa = XCConfigurationList; 528 | buildConfigurations = ( 529 | 342B70D71ECE98060074E764 /* Debug */, 530 | 342B70D81ECE98060074E764 /* Release */, 531 | ); 532 | defaultConfigurationIsVisible = 0; 533 | defaultConfigurationName = Release; 534 | }; 535 | 3476829F1ECF537B009E9BD6 /* Build configuration list for PBXAggregateTarget "Both" */ = { 536 | isa = XCConfigurationList; 537 | buildConfigurations = ( 538 | 347682A01ECF537B009E9BD6 /* Debug */, 539 | 347682A11ECF537B009E9BD6 /* Release */, 540 | ); 541 | defaultConfigurationIsVisible = 0; 542 | defaultConfigurationName = Release; 543 | }; 544 | /* End XCConfigurationList section */ 545 | }; 546 | rootObject = 3420D25B1EC7536B0094C4B1 /* Project object */; 547 | } 548 | -------------------------------------------------------------------------------- /Metal-Life.xcodeproj/xcshareddata/xcschemes/Both.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 49 | 50 | 56 | 57 | 59 | 60 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /Metal-Life.xcodeproj/xcshareddata/xcschemes/LifeSaver.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 48 | 50 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 70 | 71 | 77 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 95 | 101 | 102 | 103 | 104 | 106 | 107 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /Metal-Life.xcodeproj/xcshareddata/xcschemes/Metal-Life.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Metal-Life/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | @NSApplicationMain 4 | class AppDelegate: NSObject, NSApplicationDelegate { 5 | 6 | 7 | 8 | func applicationDidFinishLaunching(_ aNotification: Notification) { 9 | // Insert code here to initialize your application 10 | } 11 | 12 | func applicationWillTerminate(_ aNotification: Notification) { 13 | // Insert code here to tear down your application 14 | } 15 | 16 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 17 | return true 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Metal-Life/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "size" : "128x128", 30 | "idiom" : "mac", 31 | "filename" : "icon256-1.png", 32 | "scale" : "2x" 33 | }, 34 | { 35 | "size" : "256x256", 36 | "idiom" : "mac", 37 | "filename" : "icon256.png", 38 | "scale" : "1x" 39 | }, 40 | { 41 | "size" : "256x256", 42 | "idiom" : "mac", 43 | "filename" : "icon512-1.png", 44 | "scale" : "2x" 45 | }, 46 | { 47 | "size" : "512x512", 48 | "idiom" : "mac", 49 | "filename" : "icon512.png", 50 | "scale" : "1x" 51 | }, 52 | { 53 | "size" : "512x512", 54 | "idiom" : "mac", 55 | "filename" : "icon1024.png", 56 | "scale" : "2x" 57 | } 58 | ], 59 | "info" : { 60 | "version" : 1, 61 | "author" : "xcode" 62 | } 63 | } -------------------------------------------------------------------------------- /Metal-Life/Assets.xcassets/AppIcon.appiconset/icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/Metal-Life/Assets.xcassets/AppIcon.appiconset/icon1024.png -------------------------------------------------------------------------------- /Metal-Life/Assets.xcassets/AppIcon.appiconset/icon256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/Metal-Life/Assets.xcassets/AppIcon.appiconset/icon256-1.png -------------------------------------------------------------------------------- /Metal-Life/Assets.xcassets/AppIcon.appiconset/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/Metal-Life/Assets.xcassets/AppIcon.appiconset/icon256.png -------------------------------------------------------------------------------- /Metal-Life/Assets.xcassets/AppIcon.appiconset/icon512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/Metal-Life/Assets.xcassets/AppIcon.appiconset/icon512-1.png -------------------------------------------------------------------------------- /Metal-Life/Assets.xcassets/AppIcon.appiconset/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/Metal-Life/Assets.xcassets/AppIcon.appiconset/icon512.png -------------------------------------------------------------------------------- /Metal-Life/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Metal-Life/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | Default 512 | 513 | 514 | 515 | 516 | 517 | 518 | Left to Right 519 | 520 | 521 | 522 | 523 | 524 | 525 | Right to Left 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | Default 537 | 538 | 539 | 540 | 541 | 542 | 543 | Left to Right 544 | 545 | 546 | 547 | 548 | 549 | 550 | Right to Left 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | -------------------------------------------------------------------------------- /Metal-Life/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.3.1 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSMainStoryboardFile 26 | Main 27 | NSPrincipalClass 28 | NSApplication 29 | 30 | 31 | -------------------------------------------------------------------------------- /Metal-Life/Life.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Metal 3 | import simd 4 | 5 | struct Uniforms { 6 | var mvpMatrix = matrix_identity_float4x4 7 | var bgColor: float4 = float4(0.0) 8 | var fgColor: float4 = float4(1.0) 9 | var pointSize: Float = 9.0 // 1.0 to 14.0 10 | var gridDimensions: uint2 = uint2(32, 32) 11 | } 12 | 13 | class LifeView : NSView { 14 | let device: MTLDevice 15 | var displayLink: CVDisplayLink! 16 | var numSkipFrames: Int = 0 17 | private var frameSkipCounter: Int 18 | var simulationPipelineState: MTLRenderPipelineState! 19 | var renderingPipelineState: MTLRenderPipelineState! 20 | var vertexProgram: MTLFunction! 21 | var simulateProgram: MTLFunction! 22 | var commandQueue: MTLCommandQueue! 23 | 24 | var vertexBufferA: MTLBuffer? = nil 25 | var vertexBufferB: MTLBuffer? = nil 26 | var drawingA: Bool = true 27 | var uniforms: Uniforms = Uniforms() 28 | var numCells:Int = 1024 29 | var cpuCellsBuffer: [UInt8] = [] 30 | var needsReset: Bool = false 31 | 32 | let pixelSpacing: Float = (3.0 / 128.0) 33 | let gridSpacing: Float = (40.0 / 3.0) 34 | var viewBounds: NSRect! 35 | 36 | required init?(coder: NSCoder) { 37 | if let defaultDev = MTLCreateSystemDefaultDevice() { 38 | self.device = defaultDev 39 | } 40 | else { 41 | return nil 42 | } 43 | 44 | self.frameSkipCounter = self.numSkipFrames 45 | 46 | super.init(coder: coder) 47 | self.wantsLayer = true 48 | 49 | viewBounds = self.bounds 50 | } 51 | 52 | override init(frame: NSRect) { 53 | let defaultDev = MTLCreateSystemDefaultDevice() 54 | self.device = defaultDev! 55 | 56 | self.frameSkipCounter = self.numSkipFrames 57 | 58 | super.init(frame: frame) 59 | self.wantsLayer = true 60 | 61 | viewBounds = self.bounds 62 | } 63 | 64 | deinit { 65 | CVDisplayLinkStop(displayLink) 66 | } 67 | 68 | override var acceptsFirstResponder: Bool { 69 | return true 70 | } 71 | 72 | func setup() { 73 | if let window = self.superview?.window { 74 | let mLayer = CAMetalLayer() 75 | mLayer.device = self.device 76 | mLayer.pixelFormat = .bgra8Unorm 77 | mLayer.framebufferOnly = true 78 | mLayer.frame = self.layer!.frame 79 | mLayer.contentsScale = window.backingScaleFactor 80 | mLayer.isOpaque = true 81 | self.layer = mLayer 82 | 83 | self.displayLink = makeDisplayLink(window: window) 84 | } 85 | 86 | var metalLib = device.makeDefaultLibrary() 87 | if (metalLib == nil) { 88 | try! metalLib = device.makeLibrary(filepath: Bundle(for: LifeView.self).path(forResource: "default", ofType: "metallib")!) 89 | } 90 | let fragmentProgram = metalLib!.makeFunction(name: "lifeFragment") 91 | vertexProgram = metalLib!.makeFunction(name: "lifeVertex") 92 | simulateProgram = metalLib!.makeFunction(name: "lifeSimulate") 93 | 94 | let renderStateDescriptor = MTLRenderPipelineDescriptor() 95 | renderStateDescriptor.vertexFunction = vertexProgram 96 | renderStateDescriptor.fragmentFunction = fragmentProgram 97 | renderStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm 98 | 99 | renderingPipelineState = try! device.makeRenderPipelineState(descriptor: renderStateDescriptor) 100 | 101 | let simulateStateDescriptor = MTLRenderPipelineDescriptor() 102 | simulateStateDescriptor.isRasterizationEnabled = false 103 | simulateStateDescriptor.vertexFunction = simulateProgram 104 | simulateStateDescriptor.fragmentFunction = nil 105 | simulateStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm 106 | 107 | simulationPipelineState = try! device.makeRenderPipelineState(descriptor: simulateStateDescriptor) 108 | 109 | commandQueue = device.makeCommandQueue() 110 | 111 | resetBuffers() 112 | } 113 | 114 | func start() { 115 | CVDisplayLinkStart(displayLink!) 116 | } 117 | 118 | func stop() { 119 | CVDisplayLinkStop(displayLink!) 120 | } 121 | 122 | func isAnimating() -> Bool { 123 | return CVDisplayLinkIsRunning(displayLink!) 124 | } 125 | 126 | private func makeDisplayLink(window: NSWindow) -> CVDisplayLink { 127 | func displayLinkOutputCallback(_ displayLink: CVDisplayLink, _ nowPtr: UnsafePointer, _ outputTimePtr: UnsafePointer, _ flagsIn: CVOptionFlags, _ flagsOut: UnsafeMutablePointer, _ displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn { 128 | let me: LifeView = unsafeBitCast(displayLinkContext, to: LifeView.self) 129 | me.render() 130 | return kCVReturnSuccess 131 | } 132 | 133 | var link: CVDisplayLink? 134 | let screens = NSScreen.screens 135 | let screensID = screens.index(of: window.screen!) 136 | CVDisplayLinkCreateWithCGDisplay(CGDirectDisplayID(screensID!), &link) 137 | CVDisplayLinkSetOutputCallback(link!, displayLinkOutputCallback, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())) 138 | return link! 139 | } 140 | 141 | override func viewDidEndLiveResize() { 142 | viewBounds = self.bounds 143 | self.needsReset = true 144 | } 145 | 146 | func makeOrthoMatrix(left: Float, right: Float, bottom: Float, top: Float, near: Float, far: Float) -> float4x4 { 147 | 148 | var forReturn: float4x4 = float4x4(); 149 | 150 | forReturn[0][0] = Float(2.0) / (right - left); 151 | forReturn[0][1] = Float(0.0); 152 | forReturn[0][2] = Float(0.0); 153 | forReturn[0][3] = Float(0.0); 154 | forReturn[1][0] = Float(0.0); 155 | forReturn[1][1] = Float(2.0) / (top - bottom); 156 | forReturn[1][2] = Float(0.0); 157 | forReturn[1][3] = Float(0.0); 158 | forReturn[2][0] = Float(0.0); 159 | forReturn[2][1] = Float(0.0); 160 | forReturn[2][2] = Float(1.0) / (far - near); 161 | forReturn[2][3] = Float(1.0); 162 | forReturn[3][0] = (left + right) / (left - right); 163 | forReturn[3][1] = (top + bottom) / (bottom - top); 164 | forReturn[3][2] = near / (near - far); 165 | forReturn[3][3] = Float(1.0); 166 | 167 | return forReturn; 168 | } 169 | 170 | 171 | func checkForEquilibrium() { 172 | if (self.needsReset || self.inLiveResize) { 173 | return 174 | } 175 | let current = self.vertexBufferA!.contents() 176 | var currentInts = [UInt8](repeating: 0, count: self.numCells) 177 | var changeCount: Int = 0 178 | for i in 0...stride, as: UInt8.self) 180 | if self.cpuCellsBuffer[i] != c { 181 | changeCount += 1 182 | } 183 | currentInts[i] = c 184 | } 185 | self.cpuCellsBuffer = currentInts 186 | 187 | let change = Float(changeCount) / Float(self.numCells) 188 | // NSLog("\(change * 100.0)%% change since last check.") 189 | 190 | let liveCells = self.cpuCellsBuffer.filter({$0 > 0}).count 191 | // NSLog("\(liveCells) live cells.") 192 | 193 | if change < 0.001 || liveCells == 0 { 194 | self.needsReset = true 195 | } 196 | } 197 | 198 | func resetBuffers() { 199 | let unitDimensions = float2(Float(self.viewBounds.width) * pixelSpacing, Float(self.viewBounds.height) * pixelSpacing) * 0.5 200 | 201 | (self.layer as! CAMetalLayer).drawableSize = CGSize( 202 | width: self.viewBounds.width * self.layer!.contentsScale, 203 | height: self.viewBounds.height * self.layer!.contentsScale 204 | ) 205 | 206 | let ortho = makeOrthoMatrix(left: -unitDimensions.x, right: unitDimensions.x, bottom: -unitDimensions.y, top: unitDimensions.y, near: -1.0, far: 1.0) 207 | self.uniforms.mvpMatrix = ortho 208 | self.uniforms.gridDimensions.x = UInt32(unitDimensions.x * gridSpacing) 209 | self.uniforms.gridDimensions.y = UInt32(unitDimensions.y * gridSpacing) 210 | self.numCells = Int(self.uniforms.gridDimensions.x * self.uniforms.gridDimensions.y) 211 | 212 | self.cpuCellsBuffer = [UInt8](repeating: 0, count: self.numCells) 213 | let memSize: Int = self.numCells * MemoryLayout.stride 214 | 215 | if (vertexBufferB == nil || vertexBufferB!.length != memSize) { 216 | self.vertexBufferB = device.makeBuffer(bytes: self.cpuCellsBuffer, length: self.numCells * MemoryLayout.stride, options: [.storageModeManaged]) 217 | } 218 | else { 219 | memcpy(self.vertexBufferB!.contents(), self.cpuCellsBuffer, memSize) 220 | self.vertexBufferB!.didModifyRange(0...stride, options: [.storageModeManaged]) 233 | } 234 | else { 235 | memcpy(vertexBufferA!.contents(), self.cpuCellsBuffer, memSize) 236 | self.vertexBufferA!.didModifyRange(0...stride, index: 2) 278 | encoder?.setFragmentBytes(&self.uniforms, length: MemoryLayout.stride, index: 2) 279 | 280 | encoder?.setRenderPipelineState(simulationPipelineState) 281 | encoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: self.numCells) 282 | 283 | encoder?.setRenderPipelineState(renderingPipelineState) 284 | encoder?.drawPrimitives(type: .point, vertexStart: 0, vertexCount: self.numCells) 285 | 286 | encoder?.endEncoding() 287 | 288 | commandBuffer?.present(drawable) 289 | commandBuffer?.commit() 290 | } 291 | 292 | self.drawingA = !self.drawingA 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /Metal-Life/LifeViewController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Metal 3 | import simd 4 | 5 | 6 | class LifeViewController: NSViewController { 7 | var eqTimer: Timer! 8 | var mouseTimer: Timer! 9 | var hidingMouse: Bool = false 10 | let defaults: UserDefaults = UserDefaults.standard 11 | 12 | @IBOutlet weak var lifeView: LifeView! 13 | @IBOutlet weak var controlGroup: NSBox! 14 | @IBOutlet weak var defaultsButton: NSButton! 15 | @IBOutlet weak var pointSizeSlider: NSSlider! { 16 | didSet { 17 | pointSizeSlider.minValue = 1.0 18 | pointSizeSlider.maxValue = 13.0 19 | pointSizeSlider.doubleValue = 9.0 20 | } 21 | } 22 | @IBAction func pointSizeChanged(sender: NSSlider) { 23 | lifeView.uniforms.pointSize = Float(self.pointSizeSlider.doubleValue) 24 | self.defaults.set(self.pointSizeSlider.doubleValue, forKey: "pointSize") 25 | } 26 | @IBOutlet weak var fpsMenu: NSPopUpButton! 27 | @IBAction func fpsChangeRequested(sender: NSPopUpButton) { 28 | let req = fpsMenu.selectedItem?.title 29 | let reqInt = Int(req!)! 30 | lifeView.numSkipFrames = (60 / reqInt) - 1 31 | 32 | self.defaults.set(reqInt, forKey: "frameRate") 33 | } 34 | 35 | @IBOutlet weak var bgColorWell: NSColorWell! 36 | @IBOutlet weak var fgColorWell: NSColorWell! 37 | 38 | @IBAction func fgColorChanged(sender: NSColorWell) { 39 | let c = fgColorWell.color 40 | lifeView.uniforms.fgColor = float4(Float(c.redComponent), Float(c.greenComponent), Float(c.blueComponent), Float(c.alphaComponent)) 41 | 42 | self.defaults.set(c.redComponent, forKey: "fgRed") 43 | self.defaults.set(c.greenComponent, forKey: "fgGreen") 44 | self.defaults.set(c.blueComponent, forKey: "fgBlue") 45 | 46 | } 47 | @IBAction func bgColorChanged(sender: NSColorWell) { 48 | let c = bgColorWell.color 49 | lifeView.uniforms.bgColor = float4(Float(c.redComponent), Float(c.greenComponent), Float(c.blueComponent), Float(c.alphaComponent)) 50 | 51 | self.defaults.set(c.redComponent, forKey: "bgRed") 52 | self.defaults.set(c.greenComponent, forKey: "bgGreen") 53 | self.defaults.set(c.blueComponent, forKey: "bgBlue") 54 | } 55 | @IBAction func defaultsButtonAction(sender: NSButton) { 56 | self.defaults.set(60, forKey: "frameRate") 57 | self.defaults.set(9.0, forKey: "pointSize") 58 | self.defaults.set(0.25, forKey: "bgRed") 59 | self.defaults.set(0.0, forKey: "bgGreen") 60 | self.defaults.set(0.0, forKey: "bgBlue") 61 | self.defaults.set(1.0, forKey: "fgRed") 62 | self.defaults.set(0.0, forKey: "fgGreen") 63 | self.defaults.set(0.0, forKey: "fgBlue") 64 | 65 | self.syncRendering() 66 | self.syncUI() 67 | } 68 | 69 | func setHidingMouse(on: Bool) { 70 | if (on) { 71 | NSCursor.setHiddenUntilMouseMoves(true) 72 | self.hidingMouse = true 73 | } 74 | else { 75 | NSCursor.setHiddenUntilMouseMoves(false) 76 | self.hidingMouse = false 77 | self.mouseTimer?.invalidate() 78 | } 79 | } 80 | 81 | override func mouseMoved(with event: NSEvent) { 82 | if (self.hidingMouse) { 83 | self.mouseTimer?.invalidate() 84 | self.mouseTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(LifeViewController.mouseStopped), userInfo: nil, repeats: false) 85 | } 86 | } 87 | 88 | override func mouseDown(with event: NSEvent) { 89 | if !self.pointSizeSlider.isEnabled { 90 | return 91 | } 92 | 93 | if (!self.bgColorWell.isActive && !self.fgColorWell.isActive) { 94 | self.controlGroup.alphaValue = 0.0 95 | self.bgColorWell.isEnabled = false 96 | self.fgColorWell.isEnabled = false 97 | self.pointSizeSlider.isEnabled = false 98 | self.fpsMenu.isEnabled = false 99 | self.defaultsButton.isEnabled = false 100 | self.defaults.synchronize() 101 | } 102 | } 103 | 104 | @objc func mouseStopped() { 105 | NSCursor.setHiddenUntilMouseMoves(true) 106 | self.hidingMouse = true 107 | } 108 | 109 | override func flagsChanged(with event: NSEvent) { 110 | if (event.modifierFlags.intersection(NSEvent.ModifierFlags.deviceIndependentFlagsMask) == NSEvent.ModifierFlags.control) { 111 | self.controlGroup.alphaValue = 1.0 112 | self.bgColorWell.isEnabled = true 113 | self.fgColorWell.isEnabled = true 114 | self.pointSizeSlider.isEnabled = true 115 | self.fpsMenu.isEnabled = true 116 | self.defaultsButton.isEnabled = true 117 | self.syncUI() 118 | } 119 | else { 120 | if (!self.bgColorWell.isActive && !self.fgColorWell.isActive) { 121 | self.controlGroup.alphaValue = 0.0 122 | self.bgColorWell.isEnabled = false 123 | self.fgColorWell.isEnabled = false 124 | self.pointSizeSlider.isEnabled = false 125 | self.fpsMenu.isEnabled = false 126 | self.defaultsButton.isEnabled = false 127 | self.defaults.synchronize() 128 | } 129 | } 130 | } 131 | 132 | override func viewWillAppear() { 133 | super.viewWillAppear() 134 | 135 | lifeView.setup() 136 | 137 | let prefsFilePath = Bundle.main.path(forResource: "InitialPreferences", ofType: "plist") 138 | let nsDefaultPrefs = NSDictionary(contentsOfFile: prefsFilePath!) 139 | if let defaultPrefs: Dictionary = nsDefaultPrefs as? Dictionary { 140 | self.defaults.register(defaults: defaultPrefs) 141 | } 142 | 143 | self.syncRendering() 144 | 145 | self.syncUI() 146 | self.controlGroup.alphaValue = 0.0 147 | self.bgColorWell.isEnabled = false 148 | self.fgColorWell.isEnabled = false 149 | self.pointSizeSlider.isEnabled = false 150 | self.fpsMenu.isEnabled = false 151 | 152 | eqTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { 153 | _ in 154 | self.lifeView.checkForEquilibrium() 155 | } 156 | 157 | self.view.window?.delegate = self 158 | self.view.window?.acceptsMouseMovedEvents = true 159 | 160 | lifeView.start() 161 | } 162 | 163 | func syncRendering() { 164 | self.lifeView.uniforms.pointSize = self.defaults.float(forKey: "pointSize") 165 | self.lifeView.uniforms.bgColor = float4( 166 | self.defaults.float(forKey: "bgRed"), 167 | self.defaults.float(forKey: "bgGreen"), 168 | self.defaults.float(forKey: "bgBlue"), 169 | 1.0 170 | ) 171 | self.lifeView.uniforms.fgColor = float4( 172 | self.defaults.float(forKey: "fgRed"), 173 | self.defaults.float(forKey: "fgGreen"), 174 | self.defaults.float(forKey: "fgBlue"), 175 | 1.0 176 | ) 177 | let fr = self.defaults.integer(forKey: "frameRate") 178 | self.lifeView.numSkipFrames = (60 / fr) - 1 179 | } 180 | 181 | func syncUI() { 182 | self.pointSizeSlider.doubleValue = self.defaults.double(forKey: "pointSize") 183 | self.bgColorWell.color = NSColor( 184 | red: CGFloat(self.defaults.double(forKey: "bgRed")), 185 | green: CGFloat(self.defaults.double(forKey: "bgGreen")), 186 | blue: CGFloat(self.defaults.double(forKey: "bgBlue")), 187 | alpha: 1.0 188 | ) 189 | self.fgColorWell.color = NSColor( 190 | red: CGFloat(self.defaults.double(forKey: "fgRed")), 191 | green: CGFloat(self.defaults.double(forKey: "fgGreen")), 192 | blue: CGFloat(self.defaults.double(forKey: "fgBlue")), 193 | alpha: 1.0 194 | ) 195 | 196 | let frameRate = self.defaults.integer(forKey: "frameRate") 197 | let menuItem = self.fpsMenu.item(withTitle: String(frameRate)) 198 | self.fpsMenu.select(menuItem) 199 | } 200 | } 201 | 202 | extension LifeViewController: NSWindowDelegate { 203 | func windowDidEnterFullScreen(_ notification: Notification) { 204 | self.setHidingMouse(on: true) 205 | } 206 | 207 | func windowDidExitFullScreen(_ notification: Notification) { 208 | self.setHidingMouse(on: false) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /Metal-Life/Shaders.metal: -------------------------------------------------------------------------------- 1 | #include 2 | using namespace metal; 3 | 4 | 5 | constant float gridSpacing = (1.5 / 20.0); 6 | 7 | struct VertexOut { 8 | float4 position [[position]]; 9 | float pointSize [[point_size]]; 10 | uchar lifetime; 11 | }; 12 | 13 | struct NeighborIndices { 14 | uint4 cardinals; 15 | uint4 diagonals; 16 | }; 17 | 18 | struct Uniforms { 19 | float4x4 mvpMatrix; 20 | float4 bgColor; 21 | float4 fgColor; 22 | float pointSize; 23 | uint2 gridDimensions; 24 | }; 25 | 26 | static NeighborIndices getNeighbors(unsigned int index, uint2 gridDimensions) { 27 | NeighborIndices neighbors; 28 | 29 | neighbors.cardinals.x = index - 1; // west 30 | neighbors.cardinals.y = index + 1; // east 31 | neighbors.cardinals.z = (index - gridDimensions.x) % (gridDimensions.x * gridDimensions.y); // south 32 | neighbors.cardinals.w = (index + gridDimensions.x) % (gridDimensions.x * gridDimensions.y); // north 33 | 34 | neighbors.diagonals.x = neighbors.cardinals.z - 1; // southwest 35 | neighbors.diagonals.y = neighbors.cardinals.z + 1; // southeast 36 | neighbors.diagonals.z = neighbors.cardinals.w - 1; // northwest 37 | neighbors.diagonals.w = neighbors.cardinals.w + 1; // northeast 38 | 39 | if (index % gridDimensions.x == 0) { 40 | // shift western up by one for wrap-around 41 | neighbors.cardinals.x += gridDimensions.x; 42 | neighbors.diagonals.x += gridDimensions.x; 43 | neighbors.diagonals.z += gridDimensions.x; 44 | } 45 | if (index % gridDimensions.x == gridDimensions.x - 1) { 46 | // shift eastern down 47 | neighbors.cardinals.y -= gridDimensions.x; 48 | neighbors.diagonals.y -= gridDimensions.x; 49 | neighbors.diagonals.w -= gridDimensions.x; 50 | } 51 | 52 | return neighbors; 53 | } 54 | 55 | 56 | vertex void lifeSimulate( 57 | const device uchar* current [[ buffer(0) ]], 58 | device uchar* future [[ buffer(1) ]], 59 | const device Uniforms& uniforms [[ buffer(2) ]], 60 | unsigned int vId [[ vertex_id ]] 61 | ) { 62 | 63 | NeighborIndices neighbors = getNeighbors(vId, uniforms.gridDimensions); 64 | uchar neighborCount = 0; 65 | 66 | if (current[neighbors.cardinals.x] > 0) { 67 | neighborCount++; 68 | } 69 | if (current[neighbors.cardinals.y] > 0) { 70 | neighborCount++; 71 | } 72 | if (current[neighbors.cardinals.z] > 0) { 73 | neighborCount++; 74 | } 75 | if (current[neighbors.cardinals.w] > 0) { 76 | neighborCount++; 77 | } 78 | if (current[neighbors.diagonals.x] > 0) { 79 | neighborCount++; 80 | } 81 | if (current[neighbors.diagonals.y] > 0) { 82 | neighborCount++; 83 | } 84 | if (current[neighbors.diagonals.z] > 0) { 85 | neighborCount++; 86 | } 87 | if (current[neighbors.diagonals.w] > 0) { 88 | neighborCount++; 89 | } 90 | 91 | future[vId] = current[vId]; 92 | if (current[vId] > 0) { 93 | if (neighborCount < 2) { 94 | future[vId] = 0; 95 | } 96 | else if (neighborCount > 3) { 97 | future[vId] = 0; 98 | } 99 | } 100 | else { 101 | if (neighborCount == 3) { 102 | future[vId] = 255; 103 | } 104 | } 105 | } 106 | 107 | vertex VertexOut lifeVertex ( 108 | const device uchar* current [[ buffer(0) ]], 109 | const device Uniforms& uniforms [[ buffer(2) ]], 110 | unsigned int vId [[ vertex_id ]] 111 | ) { 112 | 113 | int xIndex = vId % uniforms.gridDimensions.x; 114 | int yIndex = vId / uniforms.gridDimensions.x; 115 | 116 | float startX = uniforms.gridDimensions.x * -gridSpacing; 117 | float startY = uniforms.gridDimensions.y * -gridSpacing; 118 | float endX = uniforms.gridDimensions.x * gridSpacing; 119 | float endY = uniforms.gridDimensions.y * gridSpacing; 120 | 121 | float xCoord = startX + (((endX - startX) / (uniforms.gridDimensions.x - 1)) * xIndex); 122 | float yCoord = startY + (((endY - startY) / (uniforms.gridDimensions.y - 1)) * yIndex); 123 | 124 | VertexOut vOut; 125 | vOut.position = uniforms.mvpMatrix * float4(xCoord, yCoord, 0.0, 1.0); 126 | vOut.pointSize = uniforms.pointSize; 127 | vOut.lifetime = current[vId]; 128 | 129 | return vOut; 130 | } 131 | 132 | fragment float4 lifeFragment( 133 | VertexOut interpolated [[stage_in]], 134 | const device Uniforms& uniforms [[ buffer(2) ]] 135 | ) { 136 | if (interpolated.lifetime > 128) { 137 | return uniforms.fgColor; 138 | } 139 | else { 140 | return uniforms.bgColor; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metal Life 2 | 3 | [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) (a.k.a. the "Hello World" of programmable graphics), implemented in [Metal](https://developer.apple.com/metal/) Shaders. 4 | 5 | (Originally the CPU was doing nothing at all, just telling the GPU when to reset its buffers. Then I got greedy and wanted to be able to change the colors and point size without restarting, which meant the CPU had to get more involved to coordinate the UI with the shaders. Less pure, but more fun to play with. Mission of learning about Metal, Swift, and brushing up on Cocoa UI setups: accomplished.) 6 | 7 | Now there's a screensaver, too! :D 8 | 9 | ## The program in action 10 | ![life at work](screenshots/example.gif) 11 | 12 | ## With controls! 13 | ![shrunk but high framerate of the controls](screenshots/controls-480-high.gif) 14 | -------------------------------------------------------------------------------- /Shared/InitialPreferences.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | frameRate 6 | 60 7 | pointSize 8 | 9 9 | fgRed 10 | 1 11 | fgBlue 12 | 0 13 | fgGreen 14 | 0 15 | bgRed 16 | 0.25 17 | bgBlue 18 | 0 19 | bgGreen 20 | 0 21 | 22 | 23 | -------------------------------------------------------------------------------- /assets/icon1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/assets/icon1024.png -------------------------------------------------------------------------------- /assets/icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/assets/icon256.png -------------------------------------------------------------------------------- /assets/icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/assets/icon512.png -------------------------------------------------------------------------------- /assets/logo-template.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/assets/logo-template.ai -------------------------------------------------------------------------------- /assets/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/assets/thumbnail.png -------------------------------------------------------------------------------- /assets/thumbnail.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/assets/thumbnail.psd -------------------------------------------------------------------------------- /assets/thumbnail@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/assets/thumbnail@2x.png -------------------------------------------------------------------------------- /screenshots/controls-320-high.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/screenshots/controls-320-high.gif -------------------------------------------------------------------------------- /screenshots/controls-480-high.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/screenshots/controls-480-high.gif -------------------------------------------------------------------------------- /screenshots/controls-640-low.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/screenshots/controls-640-low.gif -------------------------------------------------------------------------------- /screenshots/controls-640.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/screenshots/controls-640.gif -------------------------------------------------------------------------------- /screenshots/example-480.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/screenshots/example-480.gif -------------------------------------------------------------------------------- /screenshots/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjml/Metal-Life/0bd873ea176d8188d9b73c2d926b2f3e6642de1c/screenshots/example.gif --------------------------------------------------------------------------------