├── .gitattributes ├── .gitignore ├── LICENSE ├── MetalBench.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── chris.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcshareddata │ └── xcschemes │ └── MetalBench.xcscheme ├── MetalBench ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── Main.storyboard ├── Info.plist ├── MetalBench.entitlements ├── MtlDeviceExtensions.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Renderer.swift ├── Scenes.metal └── UI │ ├── ContentView.swift │ ├── Modifiers.swift │ ├── PreviewView.swift │ └── StatusOverlay.swift ├── MetalBenchTests ├── Info.plist └── MetalBenchTests.swift ├── MetalBenchUITests ├── Info.plist └── MetalBenchUITests.swift ├── README.md └── Screenshots ├── Forest.jpg └── Glass.jpg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alia 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 | -------------------------------------------------------------------------------- /MetalBench.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 833BAEFC24D2E5BE00AC2D46 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAEFB24D2E5BE00AC2D46 /* AppDelegate.swift */; }; 11 | 833BAEFE24D2E5BE00AC2D46 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAEFD24D2E5BE00AC2D46 /* ContentView.swift */; }; 12 | 833BAF0024D2E5C000AC2D46 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 833BAEFF24D2E5C000AC2D46 /* Assets.xcassets */; }; 13 | 833BAF0324D2E5C000AC2D46 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 833BAF0224D2E5C000AC2D46 /* Preview Assets.xcassets */; }; 14 | 833BAF0624D2E5C000AC2D46 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 833BAF0424D2E5C000AC2D46 /* Main.storyboard */; }; 15 | 833BAF1224D2E5C000AC2D46 /* MetalBenchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAF1124D2E5C000AC2D46 /* MetalBenchTests.swift */; }; 16 | 833BAF1D24D2E5C000AC2D46 /* MetalBenchUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAF1C24D2E5C000AC2D46 /* MetalBenchUITests.swift */; }; 17 | 833BAF2B24D2E67600AC2D46 /* StatusOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAF2A24D2E67600AC2D46 /* StatusOverlay.swift */; }; 18 | 833BAF2E24D2EB7000AC2D46 /* Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAF2D24D2EB7000AC2D46 /* Modifiers.swift */; }; 19 | 833BAF3024D2EBFF00AC2D46 /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAF2F24D2EBFF00AC2D46 /* Renderer.swift */; }; 20 | 833BAF3224D2F30200AC2D46 /* MtlDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAF3124D2F30200AC2D46 /* MtlDeviceExtensions.swift */; }; 21 | 833BAF3424D321AB00AC2D46 /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 833BAF3324D321AB00AC2D46 /* PreviewView.swift */; }; 22 | 83EC850924D4613E00B4CB40 /* Scenes.metal in Sources */ = {isa = PBXBuildFile; fileRef = 83EC850824D4613E00B4CB40 /* Scenes.metal */; }; 23 | /* End PBXBuildFile section */ 24 | 25 | /* Begin PBXContainerItemProxy section */ 26 | 833BAF0E24D2E5C000AC2D46 /* PBXContainerItemProxy */ = { 27 | isa = PBXContainerItemProxy; 28 | containerPortal = 833BAEF024D2E5BE00AC2D46 /* Project object */; 29 | proxyType = 1; 30 | remoteGlobalIDString = 833BAEF724D2E5BE00AC2D46; 31 | remoteInfo = MetalBench; 32 | }; 33 | 833BAF1924D2E5C000AC2D46 /* PBXContainerItemProxy */ = { 34 | isa = PBXContainerItemProxy; 35 | containerPortal = 833BAEF024D2E5BE00AC2D46 /* Project object */; 36 | proxyType = 1; 37 | remoteGlobalIDString = 833BAEF724D2E5BE00AC2D46; 38 | remoteInfo = MetalBench; 39 | }; 40 | /* End PBXContainerItemProxy section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 2C53231A24D8577C005AFEB5 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; 44 | 2C53231B24D8579F005AFEB5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 45 | 2C53231D24D85944005AFEB5 /* Screenshots */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Screenshots; sourceTree = ""; }; 46 | 833BAEF824D2E5BE00AC2D46 /* MetalBench.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MetalBench.app; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 833BAEFB24D2E5BE00AC2D46 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 48 | 833BAEFD24D2E5BE00AC2D46 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 49 | 833BAEFF24D2E5C000AC2D46 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 50 | 833BAF0224D2E5C000AC2D46 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 51 | 833BAF0524D2E5C000AC2D46 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 52 | 833BAF0724D2E5C000AC2D46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | 833BAF0824D2E5C000AC2D46 /* MetalBench.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MetalBench.entitlements; sourceTree = ""; }; 54 | 833BAF0D24D2E5C000AC2D46 /* MetalBenchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MetalBenchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 55 | 833BAF1124D2E5C000AC2D46 /* MetalBenchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalBenchTests.swift; sourceTree = ""; }; 56 | 833BAF1324D2E5C000AC2D46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 57 | 833BAF1824D2E5C000AC2D46 /* MetalBenchUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MetalBenchUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | 833BAF1C24D2E5C000AC2D46 /* MetalBenchUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalBenchUITests.swift; sourceTree = ""; }; 59 | 833BAF1E24D2E5C000AC2D46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 60 | 833BAF2A24D2E67600AC2D46 /* StatusOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusOverlay.swift; sourceTree = ""; }; 61 | 833BAF2D24D2EB7000AC2D46 /* Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modifiers.swift; sourceTree = ""; }; 62 | 833BAF2F24D2EBFF00AC2D46 /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = ""; }; 63 | 833BAF3124D2F30200AC2D46 /* MtlDeviceExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MtlDeviceExtensions.swift; sourceTree = ""; }; 64 | 833BAF3324D321AB00AC2D46 /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; 65 | 83EC850824D4613E00B4CB40 /* Scenes.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Scenes.metal; sourceTree = ""; }; 66 | /* End PBXFileReference section */ 67 | 68 | /* Begin PBXFrameworksBuildPhase section */ 69 | 833BAEF524D2E5BE00AC2D46 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | 833BAF0A24D2E5C000AC2D46 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 2147483647; 79 | files = ( 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | 833BAF1524D2E5C000AC2D46 /* Frameworks */ = { 84 | isa = PBXFrameworksBuildPhase; 85 | buildActionMask = 2147483647; 86 | files = ( 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXFrameworksBuildPhase section */ 91 | 92 | /* Begin PBXGroup section */ 93 | 833BAEEF24D2E5BE00AC2D46 = { 94 | isa = PBXGroup; 95 | children = ( 96 | 2C53231B24D8579F005AFEB5 /* README.md */, 97 | 2C53231D24D85944005AFEB5 /* Screenshots */, 98 | 2C53231A24D8577C005AFEB5 /* .gitignore */, 99 | 833BAEFA24D2E5BE00AC2D46 /* MetalBench */, 100 | 833BAF1024D2E5C000AC2D46 /* MetalBenchTests */, 101 | 833BAF1B24D2E5C000AC2D46 /* MetalBenchUITests */, 102 | 833BAEF924D2E5BE00AC2D46 /* Products */, 103 | ); 104 | sourceTree = ""; 105 | }; 106 | 833BAEF924D2E5BE00AC2D46 /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 833BAEF824D2E5BE00AC2D46 /* MetalBench.app */, 110 | 833BAF0D24D2E5C000AC2D46 /* MetalBenchTests.xctest */, 111 | 833BAF1824D2E5C000AC2D46 /* MetalBenchUITests.xctest */, 112 | ); 113 | name = Products; 114 | sourceTree = ""; 115 | }; 116 | 833BAEFA24D2E5BE00AC2D46 /* MetalBench */ = { 117 | isa = PBXGroup; 118 | children = ( 119 | 833BAEFB24D2E5BE00AC2D46 /* AppDelegate.swift */, 120 | 833BAF2C24D2EB6100AC2D46 /* UI */, 121 | 833BAEFF24D2E5C000AC2D46 /* Assets.xcassets */, 122 | 833BAF0424D2E5C000AC2D46 /* Main.storyboard */, 123 | 833BAF0724D2E5C000AC2D46 /* Info.plist */, 124 | 833BAF0824D2E5C000AC2D46 /* MetalBench.entitlements */, 125 | 833BAF0124D2E5C000AC2D46 /* Preview Content */, 126 | 833BAF2F24D2EBFF00AC2D46 /* Renderer.swift */, 127 | 833BAF3124D2F30200AC2D46 /* MtlDeviceExtensions.swift */, 128 | 83EC850824D4613E00B4CB40 /* Scenes.metal */, 129 | ); 130 | path = MetalBench; 131 | sourceTree = ""; 132 | }; 133 | 833BAF0124D2E5C000AC2D46 /* Preview Content */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 833BAF0224D2E5C000AC2D46 /* Preview Assets.xcassets */, 137 | ); 138 | path = "Preview Content"; 139 | sourceTree = ""; 140 | }; 141 | 833BAF1024D2E5C000AC2D46 /* MetalBenchTests */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | 833BAF1124D2E5C000AC2D46 /* MetalBenchTests.swift */, 145 | 833BAF1324D2E5C000AC2D46 /* Info.plist */, 146 | ); 147 | path = MetalBenchTests; 148 | sourceTree = ""; 149 | }; 150 | 833BAF1B24D2E5C000AC2D46 /* MetalBenchUITests */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 833BAF1C24D2E5C000AC2D46 /* MetalBenchUITests.swift */, 154 | 833BAF1E24D2E5C000AC2D46 /* Info.plist */, 155 | ); 156 | path = MetalBenchUITests; 157 | sourceTree = ""; 158 | }; 159 | 833BAF2C24D2EB6100AC2D46 /* UI */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 833BAEFD24D2E5BE00AC2D46 /* ContentView.swift */, 163 | 833BAF2A24D2E67600AC2D46 /* StatusOverlay.swift */, 164 | 833BAF2D24D2EB7000AC2D46 /* Modifiers.swift */, 165 | 833BAF3324D321AB00AC2D46 /* PreviewView.swift */, 166 | ); 167 | path = UI; 168 | sourceTree = ""; 169 | }; 170 | /* End PBXGroup section */ 171 | 172 | /* Begin PBXNativeTarget section */ 173 | 833BAEF724D2E5BE00AC2D46 /* MetalBench */ = { 174 | isa = PBXNativeTarget; 175 | buildConfigurationList = 833BAF2124D2E5C000AC2D46 /* Build configuration list for PBXNativeTarget "MetalBench" */; 176 | buildPhases = ( 177 | 833BAEF424D2E5BE00AC2D46 /* Sources */, 178 | 833BAEF524D2E5BE00AC2D46 /* Frameworks */, 179 | 833BAEF624D2E5BE00AC2D46 /* Resources */, 180 | ); 181 | buildRules = ( 182 | ); 183 | dependencies = ( 184 | ); 185 | name = MetalBench; 186 | productName = MetalBench; 187 | productReference = 833BAEF824D2E5BE00AC2D46 /* MetalBench.app */; 188 | productType = "com.apple.product-type.application"; 189 | }; 190 | 833BAF0C24D2E5C000AC2D46 /* MetalBenchTests */ = { 191 | isa = PBXNativeTarget; 192 | buildConfigurationList = 833BAF2424D2E5C000AC2D46 /* Build configuration list for PBXNativeTarget "MetalBenchTests" */; 193 | buildPhases = ( 194 | 833BAF0924D2E5C000AC2D46 /* Sources */, 195 | 833BAF0A24D2E5C000AC2D46 /* Frameworks */, 196 | 833BAF0B24D2E5C000AC2D46 /* Resources */, 197 | ); 198 | buildRules = ( 199 | ); 200 | dependencies = ( 201 | 833BAF0F24D2E5C000AC2D46 /* PBXTargetDependency */, 202 | ); 203 | name = MetalBenchTests; 204 | productName = MetalBenchTests; 205 | productReference = 833BAF0D24D2E5C000AC2D46 /* MetalBenchTests.xctest */; 206 | productType = "com.apple.product-type.bundle.unit-test"; 207 | }; 208 | 833BAF1724D2E5C000AC2D46 /* MetalBenchUITests */ = { 209 | isa = PBXNativeTarget; 210 | buildConfigurationList = 833BAF2724D2E5C000AC2D46 /* Build configuration list for PBXNativeTarget "MetalBenchUITests" */; 211 | buildPhases = ( 212 | 833BAF1424D2E5C000AC2D46 /* Sources */, 213 | 833BAF1524D2E5C000AC2D46 /* Frameworks */, 214 | 833BAF1624D2E5C000AC2D46 /* Resources */, 215 | ); 216 | buildRules = ( 217 | ); 218 | dependencies = ( 219 | 833BAF1A24D2E5C000AC2D46 /* PBXTargetDependency */, 220 | ); 221 | name = MetalBenchUITests; 222 | productName = MetalBenchUITests; 223 | productReference = 833BAF1824D2E5C000AC2D46 /* MetalBenchUITests.xctest */; 224 | productType = "com.apple.product-type.bundle.ui-testing"; 225 | }; 226 | /* End PBXNativeTarget section */ 227 | 228 | /* Begin PBXProject section */ 229 | 833BAEF024D2E5BE00AC2D46 /* Project object */ = { 230 | isa = PBXProject; 231 | attributes = { 232 | LastSwiftUpdateCheck = 1200; 233 | LastUpgradeCheck = 1200; 234 | TargetAttributes = { 235 | 833BAEF724D2E5BE00AC2D46 = { 236 | CreatedOnToolsVersion = 12.0; 237 | }; 238 | 833BAF0C24D2E5C000AC2D46 = { 239 | CreatedOnToolsVersion = 12.0; 240 | TestTargetID = 833BAEF724D2E5BE00AC2D46; 241 | }; 242 | 833BAF1724D2E5C000AC2D46 = { 243 | CreatedOnToolsVersion = 12.0; 244 | TestTargetID = 833BAEF724D2E5BE00AC2D46; 245 | }; 246 | }; 247 | }; 248 | buildConfigurationList = 833BAEF324D2E5BE00AC2D46 /* Build configuration list for PBXProject "MetalBench" */; 249 | compatibilityVersion = "Xcode 9.3"; 250 | developmentRegion = en; 251 | hasScannedForEncodings = 0; 252 | knownRegions = ( 253 | en, 254 | Base, 255 | ); 256 | mainGroup = 833BAEEF24D2E5BE00AC2D46; 257 | productRefGroup = 833BAEF924D2E5BE00AC2D46 /* Products */; 258 | projectDirPath = ""; 259 | projectRoot = ""; 260 | targets = ( 261 | 833BAEF724D2E5BE00AC2D46 /* MetalBench */, 262 | 833BAF0C24D2E5C000AC2D46 /* MetalBenchTests */, 263 | 833BAF1724D2E5C000AC2D46 /* MetalBenchUITests */, 264 | ); 265 | }; 266 | /* End PBXProject section */ 267 | 268 | /* Begin PBXResourcesBuildPhase section */ 269 | 833BAEF624D2E5BE00AC2D46 /* Resources */ = { 270 | isa = PBXResourcesBuildPhase; 271 | buildActionMask = 2147483647; 272 | files = ( 273 | 833BAF0624D2E5C000AC2D46 /* Main.storyboard in Resources */, 274 | 833BAF0324D2E5C000AC2D46 /* Preview Assets.xcassets in Resources */, 275 | 833BAF0024D2E5C000AC2D46 /* Assets.xcassets in Resources */, 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | 833BAF0B24D2E5C000AC2D46 /* Resources */ = { 280 | isa = PBXResourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | ); 284 | runOnlyForDeploymentPostprocessing = 0; 285 | }; 286 | 833BAF1624D2E5C000AC2D46 /* Resources */ = { 287 | isa = PBXResourcesBuildPhase; 288 | buildActionMask = 2147483647; 289 | files = ( 290 | ); 291 | runOnlyForDeploymentPostprocessing = 0; 292 | }; 293 | /* End PBXResourcesBuildPhase section */ 294 | 295 | /* Begin PBXSourcesBuildPhase section */ 296 | 833BAEF424D2E5BE00AC2D46 /* Sources */ = { 297 | isa = PBXSourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | 833BAF2E24D2EB7000AC2D46 /* Modifiers.swift in Sources */, 301 | 833BAEFE24D2E5BE00AC2D46 /* ContentView.swift in Sources */, 302 | 833BAF3424D321AB00AC2D46 /* PreviewView.swift in Sources */, 303 | 833BAEFC24D2E5BE00AC2D46 /* AppDelegate.swift in Sources */, 304 | 833BAF3024D2EBFF00AC2D46 /* Renderer.swift in Sources */, 305 | 833BAF2B24D2E67600AC2D46 /* StatusOverlay.swift in Sources */, 306 | 83EC850924D4613E00B4CB40 /* Scenes.metal in Sources */, 307 | 833BAF3224D2F30200AC2D46 /* MtlDeviceExtensions.swift in Sources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | 833BAF0924D2E5C000AC2D46 /* Sources */ = { 312 | isa = PBXSourcesBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | 833BAF1224D2E5C000AC2D46 /* MetalBenchTests.swift in Sources */, 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | 833BAF1424D2E5C000AC2D46 /* Sources */ = { 320 | isa = PBXSourcesBuildPhase; 321 | buildActionMask = 2147483647; 322 | files = ( 323 | 833BAF1D24D2E5C000AC2D46 /* MetalBenchUITests.swift in Sources */, 324 | ); 325 | runOnlyForDeploymentPostprocessing = 0; 326 | }; 327 | /* End PBXSourcesBuildPhase section */ 328 | 329 | /* Begin PBXTargetDependency section */ 330 | 833BAF0F24D2E5C000AC2D46 /* PBXTargetDependency */ = { 331 | isa = PBXTargetDependency; 332 | target = 833BAEF724D2E5BE00AC2D46 /* MetalBench */; 333 | targetProxy = 833BAF0E24D2E5C000AC2D46 /* PBXContainerItemProxy */; 334 | }; 335 | 833BAF1A24D2E5C000AC2D46 /* PBXTargetDependency */ = { 336 | isa = PBXTargetDependency; 337 | target = 833BAEF724D2E5BE00AC2D46 /* MetalBench */; 338 | targetProxy = 833BAF1924D2E5C000AC2D46 /* PBXContainerItemProxy */; 339 | }; 340 | /* End PBXTargetDependency section */ 341 | 342 | /* Begin PBXVariantGroup section */ 343 | 833BAF0424D2E5C000AC2D46 /* Main.storyboard */ = { 344 | isa = PBXVariantGroup; 345 | children = ( 346 | 833BAF0524D2E5C000AC2D46 /* Base */, 347 | ); 348 | name = Main.storyboard; 349 | sourceTree = ""; 350 | }; 351 | /* End PBXVariantGroup section */ 352 | 353 | /* Begin XCBuildConfiguration section */ 354 | 833BAF1F24D2E5C000AC2D46 /* Debug */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ALWAYS_SEARCH_USER_PATHS = NO; 358 | CLANG_ANALYZER_NONNULL = YES; 359 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 360 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 361 | CLANG_CXX_LIBRARY = "libc++"; 362 | CLANG_ENABLE_MODULES = YES; 363 | CLANG_ENABLE_OBJC_ARC = YES; 364 | CLANG_ENABLE_OBJC_WEAK = YES; 365 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 366 | CLANG_WARN_BOOL_CONVERSION = YES; 367 | CLANG_WARN_COMMA = YES; 368 | CLANG_WARN_CONSTANT_CONVERSION = YES; 369 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 370 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 371 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 372 | CLANG_WARN_EMPTY_BODY = YES; 373 | CLANG_WARN_ENUM_CONVERSION = YES; 374 | CLANG_WARN_INFINITE_RECURSION = YES; 375 | CLANG_WARN_INT_CONVERSION = YES; 376 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 377 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 378 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 379 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 380 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 381 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 382 | CLANG_WARN_STRICT_PROTOTYPES = YES; 383 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 384 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 385 | CLANG_WARN_UNREACHABLE_CODE = YES; 386 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 387 | COPY_PHASE_STRIP = NO; 388 | DEBUG_INFORMATION_FORMAT = dwarf; 389 | ENABLE_STRICT_OBJC_MSGSEND = YES; 390 | ENABLE_TESTABILITY = YES; 391 | GCC_C_LANGUAGE_STANDARD = gnu11; 392 | GCC_DYNAMIC_NO_PIC = NO; 393 | GCC_NO_COMMON_BLOCKS = YES; 394 | GCC_OPTIMIZATION_LEVEL = 0; 395 | GCC_PREPROCESSOR_DEFINITIONS = ( 396 | "DEBUG=1", 397 | "$(inherited)", 398 | ); 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | MACOSX_DEPLOYMENT_TARGET = 10.15; 406 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 407 | MTL_FAST_MATH = YES; 408 | ONLY_ACTIVE_ARCH = YES; 409 | SDKROOT = macosx; 410 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 411 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 412 | }; 413 | name = Debug; 414 | }; 415 | 833BAF2024D2E5C000AC2D46 /* Release */ = { 416 | isa = XCBuildConfiguration; 417 | buildSettings = { 418 | ALWAYS_SEARCH_USER_PATHS = NO; 419 | CLANG_ANALYZER_NONNULL = YES; 420 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 421 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 422 | CLANG_CXX_LIBRARY = "libc++"; 423 | CLANG_ENABLE_MODULES = YES; 424 | CLANG_ENABLE_OBJC_ARC = YES; 425 | CLANG_ENABLE_OBJC_WEAK = YES; 426 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 427 | CLANG_WARN_BOOL_CONVERSION = YES; 428 | CLANG_WARN_COMMA = YES; 429 | CLANG_WARN_CONSTANT_CONVERSION = YES; 430 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 431 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 432 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 433 | CLANG_WARN_EMPTY_BODY = YES; 434 | CLANG_WARN_ENUM_CONVERSION = YES; 435 | CLANG_WARN_INFINITE_RECURSION = YES; 436 | CLANG_WARN_INT_CONVERSION = YES; 437 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 438 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 439 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 440 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 441 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 442 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 443 | CLANG_WARN_STRICT_PROTOTYPES = YES; 444 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 445 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 446 | CLANG_WARN_UNREACHABLE_CODE = YES; 447 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 448 | COPY_PHASE_STRIP = NO; 449 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 450 | ENABLE_NS_ASSERTIONS = NO; 451 | ENABLE_STRICT_OBJC_MSGSEND = YES; 452 | GCC_C_LANGUAGE_STANDARD = gnu11; 453 | GCC_NO_COMMON_BLOCKS = YES; 454 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 455 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 456 | GCC_WARN_UNDECLARED_SELECTOR = YES; 457 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 458 | GCC_WARN_UNUSED_FUNCTION = YES; 459 | GCC_WARN_UNUSED_VARIABLE = YES; 460 | MACOSX_DEPLOYMENT_TARGET = 10.15; 461 | MTL_ENABLE_DEBUG_INFO = NO; 462 | MTL_FAST_MATH = YES; 463 | SDKROOT = macosx; 464 | SWIFT_COMPILATION_MODE = wholemodule; 465 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 466 | }; 467 | name = Release; 468 | }; 469 | 833BAF2224D2E5C000AC2D46 /* Debug */ = { 470 | isa = XCBuildConfiguration; 471 | buildSettings = { 472 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 473 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 474 | CODE_SIGN_ENTITLEMENTS = MetalBench/MetalBench.entitlements; 475 | CODE_SIGN_STYLE = Automatic; 476 | COMBINE_HIDPI_IMAGES = YES; 477 | CURRENT_PROJECT_VERSION = 2000; 478 | DEVELOPMENT_ASSET_PATHS = "\"MetalBench/Preview Content\""; 479 | DEVELOPMENT_TEAM = ""; 480 | ENABLE_HARDENED_RUNTIME = YES; 481 | ENABLE_PREVIEWS = YES; 482 | INFOPLIST_FILE = MetalBench/Info.plist; 483 | LD_RUNPATH_SEARCH_PATHS = ( 484 | "$(inherited)", 485 | "@executable_path/../Frameworks", 486 | ); 487 | MARKETING_VERSION = 2.0; 488 | PRODUCT_BUNDLE_IDENTIFIER = com.example.MetalBench; 489 | PRODUCT_NAME = "$(TARGET_NAME)"; 490 | SWIFT_VERSION = 5.0; 491 | }; 492 | name = Debug; 493 | }; 494 | 833BAF2324D2E5C000AC2D46 /* Release */ = { 495 | isa = XCBuildConfiguration; 496 | buildSettings = { 497 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 498 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 499 | CODE_SIGN_ENTITLEMENTS = MetalBench/MetalBench.entitlements; 500 | CODE_SIGN_STYLE = Automatic; 501 | COMBINE_HIDPI_IMAGES = YES; 502 | CURRENT_PROJECT_VERSION = 2000; 503 | DEVELOPMENT_ASSET_PATHS = "\"MetalBench/Preview Content\""; 504 | DEVELOPMENT_TEAM = ""; 505 | ENABLE_HARDENED_RUNTIME = YES; 506 | ENABLE_PREVIEWS = YES; 507 | INFOPLIST_FILE = MetalBench/Info.plist; 508 | LD_RUNPATH_SEARCH_PATHS = ( 509 | "$(inherited)", 510 | "@executable_path/../Frameworks", 511 | ); 512 | MARKETING_VERSION = 2.0; 513 | PRODUCT_BUNDLE_IDENTIFIER = com.example.MetalBench; 514 | PRODUCT_NAME = "$(TARGET_NAME)"; 515 | SWIFT_VERSION = 5.0; 516 | }; 517 | name = Release; 518 | }; 519 | 833BAF2524D2E5C000AC2D46 /* Debug */ = { 520 | isa = XCBuildConfiguration; 521 | buildSettings = { 522 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 523 | BUNDLE_LOADER = "$(TEST_HOST)"; 524 | CODE_SIGN_STYLE = Automatic; 525 | COMBINE_HIDPI_IMAGES = YES; 526 | DEVELOPMENT_TEAM = 8JFK58KR9V; 527 | INFOPLIST_FILE = MetalBenchTests/Info.plist; 528 | LD_RUNPATH_SEARCH_PATHS = ( 529 | "$(inherited)", 530 | "@executable_path/../Frameworks", 531 | "@loader_path/../Frameworks", 532 | ); 533 | PRODUCT_BUNDLE_IDENTIFIER = com.interealtime.MetalBenchTests; 534 | PRODUCT_NAME = "$(TARGET_NAME)"; 535 | SWIFT_VERSION = 5.0; 536 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MetalBench.app/Contents/MacOS/MetalBench"; 537 | }; 538 | name = Debug; 539 | }; 540 | 833BAF2624D2E5C000AC2D46 /* Release */ = { 541 | isa = XCBuildConfiguration; 542 | buildSettings = { 543 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 544 | BUNDLE_LOADER = "$(TEST_HOST)"; 545 | CODE_SIGN_STYLE = Automatic; 546 | COMBINE_HIDPI_IMAGES = YES; 547 | DEVELOPMENT_TEAM = 8JFK58KR9V; 548 | INFOPLIST_FILE = MetalBenchTests/Info.plist; 549 | LD_RUNPATH_SEARCH_PATHS = ( 550 | "$(inherited)", 551 | "@executable_path/../Frameworks", 552 | "@loader_path/../Frameworks", 553 | ); 554 | PRODUCT_BUNDLE_IDENTIFIER = com.interealtime.MetalBenchTests; 555 | PRODUCT_NAME = "$(TARGET_NAME)"; 556 | SWIFT_VERSION = 5.0; 557 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MetalBench.app/Contents/MacOS/MetalBench"; 558 | }; 559 | name = Release; 560 | }; 561 | 833BAF2824D2E5C000AC2D46 /* Debug */ = { 562 | isa = XCBuildConfiguration; 563 | buildSettings = { 564 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 565 | CODE_SIGN_STYLE = Automatic; 566 | COMBINE_HIDPI_IMAGES = YES; 567 | DEVELOPMENT_TEAM = 8JFK58KR9V; 568 | INFOPLIST_FILE = MetalBenchUITests/Info.plist; 569 | LD_RUNPATH_SEARCH_PATHS = ( 570 | "$(inherited)", 571 | "@executable_path/../Frameworks", 572 | "@loader_path/../Frameworks", 573 | ); 574 | PRODUCT_BUNDLE_IDENTIFIER = com.interealtime.MetalBenchUITests; 575 | PRODUCT_NAME = "$(TARGET_NAME)"; 576 | SWIFT_VERSION = 5.0; 577 | TEST_TARGET_NAME = MetalBench; 578 | }; 579 | name = Debug; 580 | }; 581 | 833BAF2924D2E5C000AC2D46 /* Release */ = { 582 | isa = XCBuildConfiguration; 583 | buildSettings = { 584 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 585 | CODE_SIGN_STYLE = Automatic; 586 | COMBINE_HIDPI_IMAGES = YES; 587 | DEVELOPMENT_TEAM = 8JFK58KR9V; 588 | INFOPLIST_FILE = MetalBenchUITests/Info.plist; 589 | LD_RUNPATH_SEARCH_PATHS = ( 590 | "$(inherited)", 591 | "@executable_path/../Frameworks", 592 | "@loader_path/../Frameworks", 593 | ); 594 | PRODUCT_BUNDLE_IDENTIFIER = com.interealtime.MetalBenchUITests; 595 | PRODUCT_NAME = "$(TARGET_NAME)"; 596 | SWIFT_VERSION = 5.0; 597 | TEST_TARGET_NAME = MetalBench; 598 | }; 599 | name = Release; 600 | }; 601 | /* End XCBuildConfiguration section */ 602 | 603 | /* Begin XCConfigurationList section */ 604 | 833BAEF324D2E5BE00AC2D46 /* Build configuration list for PBXProject "MetalBench" */ = { 605 | isa = XCConfigurationList; 606 | buildConfigurations = ( 607 | 833BAF1F24D2E5C000AC2D46 /* Debug */, 608 | 833BAF2024D2E5C000AC2D46 /* Release */, 609 | ); 610 | defaultConfigurationIsVisible = 0; 611 | defaultConfigurationName = Release; 612 | }; 613 | 833BAF2124D2E5C000AC2D46 /* Build configuration list for PBXNativeTarget "MetalBench" */ = { 614 | isa = XCConfigurationList; 615 | buildConfigurations = ( 616 | 833BAF2224D2E5C000AC2D46 /* Debug */, 617 | 833BAF2324D2E5C000AC2D46 /* Release */, 618 | ); 619 | defaultConfigurationIsVisible = 0; 620 | defaultConfigurationName = Release; 621 | }; 622 | 833BAF2424D2E5C000AC2D46 /* Build configuration list for PBXNativeTarget "MetalBenchTests" */ = { 623 | isa = XCConfigurationList; 624 | buildConfigurations = ( 625 | 833BAF2524D2E5C000AC2D46 /* Debug */, 626 | 833BAF2624D2E5C000AC2D46 /* Release */, 627 | ); 628 | defaultConfigurationIsVisible = 0; 629 | defaultConfigurationName = Release; 630 | }; 631 | 833BAF2724D2E5C000AC2D46 /* Build configuration list for PBXNativeTarget "MetalBenchUITests" */ = { 632 | isa = XCConfigurationList; 633 | buildConfigurations = ( 634 | 833BAF2824D2E5C000AC2D46 /* Debug */, 635 | 833BAF2924D2E5C000AC2D46 /* Release */, 636 | ); 637 | defaultConfigurationIsVisible = 0; 638 | defaultConfigurationName = Release; 639 | }; 640 | /* End XCConfigurationList section */ 641 | }; 642 | rootObject = 833BAEF024D2E5BE00AC2D46 /* Project object */; 643 | } 644 | -------------------------------------------------------------------------------- /MetalBench.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MetalBench.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MetalBench.xcodeproj/project.xcworkspace/xcuserdata/chris.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alia-Traces/MetalBench/c7143a06ca1d0772bc54498295797f4f4b0278b2/MetalBench.xcodeproj/project.xcworkspace/xcuserdata/chris.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /MetalBench.xcodeproj/xcshareddata/xcschemes/MetalBench.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 43 | 49 | 50 | 51 | 52 | 53 | 64 | 66 | 72 | 73 | 74 | 75 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /MetalBench/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MetalBench 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import Cocoa 9 | import SwiftUI 10 | 11 | // @main 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate, NSAlertDelegate, NSWindowDelegate { 14 | 15 | var window: NSWindow! 16 | let renderer = Renderer() 17 | 18 | func applicationDidFinishLaunching(_ aNotification: Notification) { 19 | 20 | // Test to ensure a metal device is available... 21 | if MTLCreateSystemDefaultDevice() == nil { 22 | // The app has an "oh shit" moment 23 | let alert = NSAlert.init() 24 | alert.alertStyle = .critical 25 | alert.messageText = "No Metal capable GPU available" 26 | alert.informativeText = "Time to upgrade your Mac?" 27 | 28 | alert.addButton(withTitle: "Quit") 29 | alert.addButton(withTitle: "Fix this error") 30 | 31 | let okButton = alert.buttons[0] 32 | okButton.target = self 33 | okButton.action = #selector(AppDelegate.killApp) 34 | let fixButton = alert.buttons[1] 35 | fixButton.target = self 36 | fixButton.action = #selector(AppDelegate.fixError) 37 | 38 | alert.delegate = self 39 | 40 | alert.runModal() 41 | 42 | } else { 43 | // Create the SwiftUI view that provides the window contents. 44 | let contentView = ContentView() 45 | .environmentObject(renderer) 46 | 47 | // Create the window and set the content view. 48 | window = NSWindow( 49 | contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), 50 | styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView], 51 | // styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 52 | backing: .buffered, defer: false) 53 | window.titlebarAppearsTransparent = true 54 | window.isReleasedWhenClosed = false 55 | window.center() 56 | window.setFrameAutosaveName("Main Window") 57 | window.contentView = NSHostingView(rootView: contentView) 58 | window.makeKeyAndOrderFront(nil) 59 | window.delegate = self 60 | } 61 | } 62 | 63 | func applicationWillTerminate(_ aNotification: Notification) { 64 | // Insert code here to tear down your application 65 | } 66 | 67 | 68 | // Alert button methods 69 | 70 | @objc func killApp() { 71 | exit(EXIT_FAILURE) 72 | } 73 | 74 | @objc func fixError() { 75 | guard let url = URL(string: "http://www.apple.com/mac/") else { 76 | exit(EXIT_FAILURE) 77 | } 78 | 79 | NSWorkspace.shared.open(url) 80 | exit(EXIT_FAILURE) 81 | } 82 | 83 | // Window delegate methods 84 | 85 | func windowWillClose(_ notification: Notification) { 86 | // Quit on close 87 | exit(EXIT_SUCCESS) 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /MetalBench/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MetalBench/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "scale" : "1x", 6 | "size" : "16x16" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x", 11 | "size" : "16x16" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "scale" : "1x", 16 | "size" : "32x32" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "scale" : "2x", 21 | "size" : "32x32" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "scale" : "1x", 26 | "size" : "128x128" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "scale" : "2x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "scale" : "1x", 36 | "size" : "256x256" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "scale" : "2x", 41 | "size" : "256x256" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "scale" : "1x", 46 | "size" : "512x512" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "scale" : "2x", 51 | "size" : "512x512" 52 | } 53 | ], 54 | "info" : { 55 | "author" : "xcode", 56 | "version" : 1 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MetalBench/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MetalBench/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 | -------------------------------------------------------------------------------- /MetalBench/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSMainStoryboardFile 26 | Main 27 | NSPrincipalClass 28 | NSApplication 29 | 30 | 31 | -------------------------------------------------------------------------------- /MetalBench/MetalBench.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MetalBench/MtlDeviceExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MtlDeviceExtensions.swift 3 | // MetalBench 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import Metal 9 | 10 | extension MTLDevice { 11 | var uiDescription: String { 12 | get { 13 | return self.name + (self.isLowPower ? " (low power)" : "") + (self.isRemovable ? " (eGPU)" : "") 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MetalBench/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MetalBench/Renderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Renderer.swift 3 | // MetalBench 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import AppKit 9 | import MetalKit 10 | 11 | enum Scene: Int, CaseIterable { 12 | case glass = 0, forest = 1 13 | 14 | var name: String { 15 | get { 16 | switch self { 17 | case .glass: 18 | return "Glass" 19 | case .forest: 20 | return "Forest floor" 21 | } 22 | } 23 | } 24 | 25 | var isRealtime: Bool { 26 | get { 27 | switch self { 28 | case .glass: 29 | return true 30 | case .forest: 31 | return false 32 | } 33 | } 34 | } 35 | } 36 | 37 | class Renderer: NSObject, ObservableObject { 38 | @Published var gpuList: [MTLDevice] 39 | @Published var selectedGPU = 0 { 40 | didSet { 41 | dev = gpuList[selectedGPU] 42 | } 43 | } 44 | 45 | @Published var sceneList: [Scene] 46 | @Published var selectedScene = 0 { 47 | didSet { 48 | isRealtime = sceneList[selectedScene].isRealtime 49 | } 50 | } 51 | var isRealtime = true { 52 | didSet { 53 | currentRayCount = isRealtime ? 1 : 2 54 | } 55 | } 56 | 57 | @Published var fps = 0.0 58 | @Published var megaRaysPerSecond = 0 59 | @Published var averageMegaRaysPerSecond = 0 60 | 61 | var last30RayCounts = Array(repeating: 0, count: 30) 62 | var count30 = 0 // Incremented when adding a ray count value, average is appended to previous averages when counter hits 30 63 | var previousRayCountAverages = [Int]() 64 | var wasReset = true // When this is true, last30RayCounts is populated with the next value 65 | 66 | var currentRayCount = 1 67 | 68 | var dev: MTLDevice 69 | 70 | override init() { 71 | print("Initialising preview") 72 | 73 | // Get GPU list, use first GPU 74 | let gpus = MTLCopyAllDevices() 75 | guard gpus.count > 0 else { 76 | exit(EXIT_FAILURE) 77 | } 78 | 79 | gpuList = gpus 80 | dev = gpus.first! 81 | 82 | sceneList = Scene.allCases 83 | } 84 | 85 | func addFrameDuration(_ duration: CFTimeInterval) { 86 | guard duration > 0.0, currentRayCount > 0 else { 87 | return 88 | } 89 | 90 | // If we're not realtime then duration represents 1/32 of a frame, scale accordingly 91 | let totalDuration = duration * (isRealtime ? 1.0 : 32.0) 92 | 93 | // FPS is live value 94 | fps = 1.0 / totalDuration 95 | 96 | // RPS is based on resolution * rays divided by time taken 97 | let raysPerSecond = Int(CFTimeInterval(1280 * 720 * currentRayCount) / totalDuration) 98 | 99 | // Add to last 30 100 | if wasReset { 101 | // After reset, the short term history is filled with the initial value 102 | last30RayCounts = Array(repeating: raysPerSecond, count: 30) 103 | wasReset = false 104 | } else { 105 | last30RayCounts[count30] = raysPerSecond 106 | } 107 | count30 += 1 108 | 109 | // If last 30 is full, average and append to long term history 110 | if count30 == 30 { 111 | let sum = last30RayCounts.reduce(0, +) 112 | previousRayCountAverages.append(sum / 30) 113 | count30 = 0 114 | } 115 | 116 | // Find short term average ray count 117 | let currentAverage = last30RayCounts.reduce(0, +) / 30 118 | 119 | // The standard MRAYS value is based on short term average 120 | megaRaysPerSecond = currentAverage / 1_000_000 121 | 122 | // Average rays is average over complete history 123 | // Add current average to sum of previous averages, divide by count 124 | averageMegaRaysPerSecond = ((previousRayCountAverages.reduce(0, +) + currentAverage) / (previousRayCountAverages.count + 1)) / 1_000_000 125 | 126 | // If realtime, calculate optimum ray count for target fps 127 | if sceneList[selectedScene].isRealtime { 128 | let raysPerPixel = Int(floor(Double(currentAverage) / (1280.0 * 720.0 * 30.0))) 129 | currentRayCount = max(1, raysPerPixel) 130 | } 131 | } 132 | 133 | func resetStats() { 134 | count30 = 0 135 | previousRayCountAverages.removeAll() 136 | wasReset = true 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /MetalBench/Scenes.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Scenes.metal 3 | // MetalBench 4 | // 5 | // Created by Alia on 31/07/2020. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | #define GLASSSCENE 0 12 | #define FORESTSCENE 1 13 | 14 | constant int SCENEFUNC [[function_constant(0)]]; 15 | //constant int SCENEFUNC = FORESTSCENE; 16 | constant int INCREMENTALRENDER = SCENEFUNC == FORESTSCENE; 17 | //constant int REALTIMERENDER = !INCREMENTALRENDER; 18 | 19 | #define GroundMaterial {0, {.3,.8,.3}, .2, .2, 0, 0, .8} 20 | 21 | // Camera values 22 | //#define CameraPosition float3(fract(ray.origin.w/8.) * -30 + 15,12,-25) 23 | //#define CameraTarget float3(fract(ray.origin.w/8.) * -40 + 20,4,0) 24 | //#define CameraPosition float3(-11,40,1) 25 | //#define CameraPosition float3(sin(ray.origin.w)*15, 12, cos(ray.origin.w)*15) 26 | 27 | //#define Zoom 4//2 //(uv.x<0?128:2) // Lens zoom. 28 | //#define Fisheye 1.7 // Use with wider lenses (zoom of 1 or less) 29 | //#define Aperture 0 //(0.5*(sin(ray.origin.w)*.5+.5)) // For depth of field 30 | //#define SoftLens 2 31 | //#define BokehShape .25 // Changes the bokeh shape between gaussian (1), disk (0.25) and ring (0) 32 | 33 | // The scene can have a main light (a sphere) 34 | //#define LightPosition float3(-500,200,-400)*5 35 | //#define LightSize 500 // Sphere radius 36 | //#define LightCol float3(150,120,100)*4 37 | 38 | // Materials 39 | #define GlassClear 0 40 | #define GlassBlue 1 41 | #define GlassPink 2 42 | #define Leaf 3 43 | #define MatTest 4 44 | 45 | #define GroundMat {0,{.27,.16,.1},.1} // {0, {1, .8, .6}, .2} 46 | 47 | // 2d inplace rotation. p = float2 to rotate, a = angle 48 | #define Rotate2D(p,a) p=cos(a)*p+sin(a)*float2(-p.y,p.x); 49 | #define mod(a, b) (a - b * floor(a/b)) 50 | 51 | // Minor values 52 | #undef Epsilon 53 | #define Epsilon 1e-3 // epsilon 54 | 55 | #pragma mark - Structs 56 | 57 | struct Ray { // Ray 58 | float4 origin; 59 | float3 dir; 60 | bool inside; // Used to indicate ray is inside surface (e.g. glass) 61 | }; 62 | 63 | struct Box { 64 | float3 a, b; 65 | }; 66 | 67 | struct Triangle { 68 | float3 v, a, b; 69 | }; 70 | 71 | struct Material{ 72 | int id; // 0: standard, 1: light, 2: glass 73 | float3 color; 74 | // Standard materials have 2 layers, allowing for e.g. matte base and reflective coat 75 | float baseSmoothness, coatSmoothness, coatOpacity, fresnel, coatSaturation; 76 | }; 77 | 78 | /// Stores the result of an intersection test - normal, distance, material 79 | struct Intersection { 80 | float4 result; // Result: xyz = normal, w = dist 81 | Material material; // Material 82 | // S scene; // S number 83 | }; 84 | 85 | 86 | #pragma mark - Hashing 87 | 88 | /// Basic hash function 89 | float3 hash(float3 p){ 90 | p=fract(p*float3(443.897,441.423,437.195)); 91 | p+=dot(p,p.yxz+19.19); 92 | return fract((p.xxy+p.yxx)*p.zyx); 93 | } 94 | 95 | #pragma mark - Sampling 96 | 97 | /// Returns a point on a sphere, r is in 0..1 range 98 | float3 pointOnSphere(float2 r) { 99 | r=float2(6.283185*r.x,2*r.y-1); 100 | return float3(sqrt(1+Epsilon-r.y*r.y)*float2(cos(r.x),sin(r.x)),r.y); // 1.001 required to avoid NaN 101 | } 102 | 103 | /// Returns a cosine weighted sample 104 | float3 lambertSample(float3 n,float2 r) { 105 | return normalize(n*(1+Epsilon)+pointOnSphere(r)); // 1.001 required to avoid NaN 106 | } 107 | 108 | #pragma mark Prototypes 109 | 110 | Intersection intersectScene(thread Ray &ray, float3 k); 111 | 112 | #pragma mark Misc 113 | 114 | #pragma mark - Lighting 115 | 116 | bool lightSample(thread Ray &ray, Intersection hit, float3 k, float4 i, thread float3 &o) { 117 | float3 lightPosition, lightSize; 118 | if (SCENEFUNC == FORESTSCENE) { 119 | lightPosition = float3(-280,250,200)*2; 120 | lightSize = 50; 121 | } else { 122 | lightPosition = float3(0,0,0); 123 | lightSize = 0; 124 | } 125 | 126 | // Get r to random point on light 127 | float3 d = normalize(lightPosition + pointOnSphere(k.xy) * k.z * lightSize - ray.origin.xyz); 128 | 129 | // Get cosine of r and normal 130 | float c=dot(d,hit.result.xyz); 131 | 132 | // Reject if facing away from light 133 | if(c<=0) return 0; 134 | 135 | // See if the ray hits a light (can be any light or the sky) 136 | Ray ray2 = ray; 137 | ray2.dir = d; 138 | hit = intersectScene(ray2,k); 139 | 140 | if(hit.material.id==1){ 141 | o+=i.xyz*c*hit.material.color*(hit.material.color/(4*M_PI_F*hit.result.w)); 142 | } 143 | if(hit.material.id==2){ 144 | ray=ray2; 145 | return 1; 146 | } 147 | return 0; 148 | } 149 | 150 | #pragma mark - Materials 151 | 152 | /// Applies glass, including fresnel reflection 153 | void applyGlass(thread Ray&ray, Intersection hit, float3 k, thread float4 &rayColouration){ 154 | 155 | // Fresnel term 156 | float fresnel = 1 - max(0., -dot(ray.dir, hit.result.xyz)); 157 | fresnel = pow(fresnel, 2.); 158 | 159 | // Randomly reflect or refract, probability based on fresnel term 160 | // If non-fresnel, refract 161 | if (k.z > fresnel) { 162 | // refraction 163 | // Index of refraction 164 | float ior = ray.inside ? 1.5 : 1./1.5; 165 | 166 | // Find refraction angle, accounting for surface roughness 167 | float3 rayDir = normalize( // Ray dir must be normalised 168 | mix( // Mix between... 169 | refract(ray.dir, hit.result.xyz, ior), // the refracted angle 170 | lambertSample(-hit.result.xyz, k.xy), // and a random angle projected into the surface 171 | hit.material.baseSmoothness // based on how smooth the glass surface is 172 | ) 173 | ); 174 | 175 | // Test for total internal reflection 176 | if(dot(hit.result.xyz, rayDir) < 0) { 177 | // Not TIR, we're OK to refract 178 | // Step through suface along normal 179 | ray.origin.xyz -= hit.result.xyz * Epsilon * 4; 180 | 181 | // Set the ray direction 182 | ray.dir=rayDir; 183 | 184 | // Flip the inside value as we pass through the surface 185 | ray.inside=!ray.inside; 186 | return; 187 | } 188 | } 189 | 190 | // Ray failed to refract, therefore reflection 191 | // Step away from the surface along the normal 192 | ray.origin.xyz+=hit.result.xyz*Epsilon*2; 193 | 194 | // Standard reflection 195 | ray.dir=reflect(ray.dir,hit.result.xyz);//mix(reflect(ray.dir,hit.result.xyz),lambertSample(hit.result.xyz,k.xy),hit.material.baseSmoothness); 196 | } 197 | 198 | 199 | /// This is hacky, I need to rework it, but it works for basic materials for now 200 | void applyMaterial(thread Ray&ray, thread Intersection &hit, float3 k, thread float4 &rayColouration, thread float3 &outputColour){ 201 | if (SCENEFUNC == GLASSSCENE) { 202 | // useROI, useMainLight, angular (0=volume) 203 | // b, cs, o, f, s 204 | // There's no refraction so step away from suRayFactorace to prevent re-intersection 205 | ray.origin.xyz+=hit.result.xyz*Epsilon*2; 206 | 207 | // Fresnel value 208 | float f=1+dot(ray.dir,hit.result.xyz),c; // 0 head on, 1 at oblique angle 209 | f*=f*f*hit.material.fresnel; // Fresnel is dependent on the coating 210 | 211 | // Add the coat opacity. This means the outer suRayFactorace will be visible, but still respects fresnel 212 | f+=(hit.material.coatOpacity); 213 | 214 | // Split randomly between coat and base 215 | // Should write this without conditionals - coatLayer = 0 | 1, then just change base values to base or coat accordingly 216 | c=step(k.z,f); 217 | 218 | // Colour by m, blended into whe (no colouration) according to coat colouration (allows for coloured reflections) 219 | rayColouration.xyz*=mix(hit.material.color,mix(1,hit.material.color,hit.material.coatSaturation),c); 220 | 221 | // We'll use base m only, so set that to base or coat 222 | hit.material.baseSmoothness=pow(mix(hit.material.baseSmoothness,hit.material.coatSmoothness,c),.5); 223 | // hit.material.baseSmoothness = 0.0; 224 | // If there's a main light, sample it 225 | // if(lightSample(ray, hit, k, rayColouration, outputColour)) return; 226 | // ROI ray? 227 | // if (hit.material.baseSmoothness < k.z) { 228 | //// if (roiCast(ray, hit.result.xyz, k)) return; 229 | // } 230 | // lightSample(r,h,k,i,o); 231 | 232 | // ray.dir = LightPosition + pointOnSphere(k.xy) * k.z * LightSize; 233 | // ray.dir = normalize(ray.dir - ray.origin.xyz); 234 | // return; 235 | // } 236 | 237 | // 238 | // Standard suRayFactorace handling 239 | // Properties: base roughness, coat roughness, coat opacity, coat fresnel 240 | 241 | // Get random direction 242 | float3 s=pointOnSphere(k.xy),n=normalize(hit.result.xyz+s*.9*(1-pow(hit.material.baseSmoothness,.125))); 243 | ray.dir=normalize(reflect(ray.dir,n)*hit.material.baseSmoothness // mirror 244 | +s*pow(k.z+Epsilon,hit.material.baseSmoothness*hit.material.baseSmoothness*10) // rough 245 | ); 246 | 247 | // If normal points away from r, flip it 248 | if(dot(ray.dir,hit.result.xyz)<0)ray.dir=normalize(hit.result.xyz*(1+Epsilon)+s*pow(k.z+Epsilon,0.25)); 249 | } else { 250 | // There's no refraction so step away from suRayFactorace to prevent re-intersection 251 | ray.origin.xyz+=hit.result.xyz*Epsilon*2; 252 | 253 | 254 | // Fresnel value 255 | float f=1+dot(ray.dir,hit.result.xyz); // 0 head on, 1 at oblique angle 256 | f*=f*f*hit.material.fresnel; // Fresnel is dependent on the coating 257 | 258 | // float spec = k.z < hit.material.coatOpacity ? hit.material.coatSmoothness : hit.material.baseSmoothness; 259 | float spec = mix(hit.material.baseSmoothness, hit.material.coatSmoothness, step(1-hit.material.coatOpacity, k.z)); 260 | spec = f > k.z ? hit.material.coatSmoothness : spec; 261 | spec = spec * 20 + .5; 262 | 263 | // rayColouration.xyz *= hit.material.color; 264 | rayColouration.xyz *= pow(hit.material.color, 1/spec); 265 | 266 | if(lightSample(ray, hit, k, rayColouration, outputColour)) return; 267 | // return; 268 | k = pointOnSphere(k.xy); 269 | float3 n = normalize(k + hit.result.xyz*spec); 270 | // float d = dot(n, hit.result.xyz); 271 | 272 | // If normal points in same direction as ray, colourise and flip 273 | if (dot(n, hit.result.xyz)<0) { 274 | n = -n; 275 | } 276 | // d = dot(n, ray.dir); 277 | if (dot(n, ray.dir)>0){ 278 | rayColouration.xyz *= pow(hit.material.color, 1/spec); 279 | ray.dir = normalize(hit.result.xyz + k); 280 | } else ray.dir = reflect(ray.dir, n); 281 | } 282 | } 283 | 284 | void check(Ray ray, thread Intersection &hit, float s) { 285 | ray.origin.xyz = ray.origin.xyz + ray.dir.xyz * hit.result.w; 286 | ray.origin.xz = mod(floor(ray.origin.xz * s), 2.0); 287 | hit.material.color *= floor(fmod(ray.origin.x + ray.origin.z, 2.0) * .95 + .05); 288 | } 289 | // 290 | //void polka(Ray ray, thread Intersection &hit, float s){ // C+O 291 | // ray.origin.xyz = ray.origin.xyz + ray.dir.xyz * hit.result.w; 292 | // hit.material.color *= step(0.35, length(mod(ray.origin.xz * s, 1.0) - 0.5)); 293 | //} 294 | // 295 | //void lightPolka(Ray ray, thread Intersection &hit, float s, float ls){ // C+O 296 | // ray.origin.xyz = ray.origin.xyz + ray.dir.xyz * hit.result.w; 297 | // hit.material.color *= (1.-step(ls, length(mod(ray.origin.xz * s, 1.0) - 0.5)))*2.; 298 | //// float2 p = abs(fract(ray.origin.xz * s)-.5); 299 | //// p = step(ls, p); 300 | //// hit.material.color = float3(1-min(p.x, p.y)); 301 | // hit.material.id = 1; 302 | //} 303 | 304 | #pragma mark - returnay intersection functions 305 | 306 | /// Ground plane intersection 307 | void intersectGround(Ray ray, thread Intersection &hit, Material material){ 308 | ray.origin.w = -ray.origin.y / ray.dir.y; 309 | if(ray.origin.w > 0 && ray.origin.w < hit.result.w){ 310 | hit.result = float4(0,1,0, ray.origin.w); 311 | hit.material = material; 312 | } 313 | } 314 | 315 | /// Sphere intersection 316 | void intersectSphere(Ray ray, float4 sphere, thread Intersection &hit, Material material){ 317 | ray.origin.xyz -= sphere.xyz; 318 | ray.origin.w = dot(ray.dir.xyz, ray.origin.xyz) * 2; 319 | float a = dot(ray.origin.xyz, ray.origin.xyz) - sphere.w * sphere.w; 320 | a = ray.origin.w * ray.origin.w - 4 * a; 321 | if (a < 0) return; 322 | a = sqrt(a); 323 | float2 g = (float2(-a, a) - ray.origin.w) / 2; 324 | a = g.x < 0 ? g.y : g.x; 325 | sphere.w *= sign(g.x); 326 | if (a> hit.result.w || a < 0) return; 327 | hit.result = float4((ray.dir.xyz * a + ray.origin.xyz) / sphere.w, a); 328 | hit.material = material; 329 | } 330 | 331 | /// Sphere intersection points 332 | //float2 intersectSphere(float3 o, float3 d, float4 sphere, thread float3 &n){ 333 | // o -= sphere.xyz; 334 | // float w = dot(d, o) * 2, 335 | // a = dot(o, o) - sphere.w * sphere.w; 336 | // a = w * w - 4 * a; 337 | // if (a < 0) return -MAXFLOAT; 338 | // a = sqrt(a); 339 | // float2 g = (float2(-a, a) - w) / 2; 340 | // n = normalize(d * (g.x < 0 ? g.y : g.x) + o);// * sign(g.x); 341 | // return g; 342 | //} 343 | 344 | /// Cube intersection 345 | //void intersectCube(Ray r,Box c,thread Intersection&hit,Material m){ 346 | // float3 a=(c.a-r.origin.xyz)/r.dir, // near 347 | // b=(c.b-r.origin.xyz)/r.dir, // far 348 | // f=max(a,b), // furthest 349 | // n=min(a,b); // nearest 350 | // float x=min(f.x,min(f.y,f.z)), // furthest plane 351 | // d=max(n.x,max(n.y,n.z)), // nearest plane 352 | // o=d<0?x:d; // nearest in front 353 | // if(isnan(n.x)|d>=x|o>hit.result.w|o<0)return; // d>=x = invalid, o>t = behind other geometry, o<0 behind 354 | // hit.result.w=o; 355 | // hit.result.xyz=normalize(step(Epsilon,abs(a-hit.result.w))-step(Epsilon,abs(b-hit.result.w)))*sign(d); 356 | // hit.material=m; 357 | //} 358 | 359 | 360 | /// Intersection test for triangle 361 | //void intersectTriangle(Ray r, Triangle t, thread Intersection &hit, Material m){ 362 | // float3 p=cross(r.dir,t.b),q,s; 363 | // float e=dot(t.a,p),u,v; 364 | // if(ehit.result.w)return; 382 | // p=normalize(cross(t.a,t.b)); 383 | // hit = {float4(p*sign(e),u),m}; 384 | //} 385 | 386 | void intersectPlane(Ray r, float4 plane, thread Intersection &hit, Material m) { 387 | float dist = dot(plane.xyz * plane.w - r.origin.xyz, plane.xyz) / dot(r.dir, plane.xyz); 388 | if (dist < hit.result.w && dist > 0) { 389 | hit.result = float4(plane.xyz, dist); 390 | hit.material = m; 391 | } 392 | } 393 | 394 | void sphereSliceMirror(Ray ray, thread Intersection &hit, Material mat, float4 sphere, float4 plane, float planeOffset) { 395 | float3 p = ray.origin.xyz - sphere.xyz; 396 | // Intersect sphere 397 | float w = dot(ray.dir, p) * 2, w2, 398 | a = dot(p, p) - sphere.w * sphere.w; 399 | a = w * w - 4 * a; 400 | if (a < 0) return; 401 | a = sqrt(a); 402 | float2 g = (float2(-a, a) - w) / 2; 403 | if (max(g.x,g.y)<0) return; 404 | a = g.x<0?g.y:g.x; // Distance to first intersection 405 | if (a<0) return; 406 | float3 n = normalize(ray.dir * a + p) * sign(g.x); 407 | 408 | w = dot(plane.xyz * (plane.w + planeOffset) - p, plane.xyz) / dot(ray.dir, plane.xyz); // Dist to plane 409 | w2 = dot(-plane.xyz * (plane.w - planeOffset) - p, -plane.xyz) / dot(ray.dir, -plane.xyz); // Dist to plane 410 | 411 | bool facingPlane = dot(ray.dir, plane.xyz) < 0;//, facingPlane2 = !facingPlane; 412 | float3 pn = plane.xyz * (facingPlane * 2 - 1), pn2 = -pn; 413 | 414 | if (g.x>0) { 415 | if (max(w, w2) < 0) { 416 | // past far plane 417 | return; 418 | } else if (a > max(w, w2)) { 419 | // Ray intersects sphere past far plane 420 | return; 421 | } else { 422 | a = max(a,min(w, w2)); 423 | n = a==w ? pn : (a==w2 ? pn2 : n); 424 | } 425 | } else { 426 | if (max(w, w2) < 0) { 427 | // Past furthest point 428 | return; 429 | } else if (min(w, w2) < 0) { 430 | // Inside slice, take minimum to sphere, further plane 431 | a = min(a, max(w, w2)); 432 | n = a==w ? pn : (a==w2 ? pn2 : n); 433 | } else if (a>w) { 434 | return; 435 | } 436 | } 437 | 438 | if (length(p + ray.dir * a) > sphere.w + Epsilon) return; // Exit if ray intersects sphere past the cutoff 439 | if (a>hit.result.w) return; // Exit if beyond nearest surface 440 | if (dot(ray.dir, n) > 0) {n = -n;} 441 | 442 | // Set material 443 | hit.result = float4(n, a); 444 | hit.material = mat; 445 | } 446 | 447 | #pragma mark - Distance functions 448 | 449 | /// Box, origin is o, size is s 450 | float boxDist(float3 p, float3 o, float3 s){ 451 | float3 q = abs(p-o) - s/2; 452 | return length(max(q,0.)) + min(max(q.x,max(q.y,q.z)),0.); 453 | } 454 | 455 | /// Torus 456 | float torus(float3 p, float2 t){ 457 | float2 q = float2(length(p.xz)-t.x,p.y); 458 | return length(q)-t.y; 459 | } 460 | 461 | float cylinderDist(float3 p, float3 o, float2 s){ 462 | p -= o; 463 | float2 d = abs(float2(length(p.xz),p.y))-s; 464 | return min(max(d.x,d.y),0.) + length(max(d,0.)); 465 | } 466 | 467 | float smin(float a, float b, float k) { 468 | float res = exp2(-k*a) + exp2(-k*b); 469 | return -log2(res)/k; 470 | } 471 | 472 | #pragma mark Forest scene 473 | 474 | // Dist, stencil 475 | float2 leaf(float3 p, float3 o, float3 s) { 476 | // Twist and bend 477 | p -= o; 478 | Rotate2D(p.xz, s.z * 6); 479 | p.z -= s.z * 5; 480 | Rotate2D(p.yz, .5*p.z/20 * s.y); 481 | p.z += s.z * 5; 482 | Rotate2D(p.xy, sin(p.z/3) * s.y*.3); 483 | float3 q = p; 484 | 485 | float e = s.x*3+3, 486 | d = max(length(p.xy) - (p.z+e)/50, length(p)- 10+s.y*3),f; // d = stalk 487 | // d = max(length(p.xy) - sqrt((max(0.,p.z+e)))/10, length(p)- 10+s.y*3),f; // d = stalk 488 | 489 | p.x = abs(p.x); // Mirror 490 | p.z = fract(p.z+p.x)-.5; // Ribs 491 | f = length(p.yz) - .15; // f = rib tubes 492 | 493 | p = q; 494 | p.x = abs(p.x) + e; // Oval leaf shape 495 | 496 | // e = length(p) - 10+s.y*3; // e = leaf volume (sphere-ish) 497 | e = max(length(p.xz) - 10+s.y*3, -p.y); // e = leaf volume (Tube-ish) 498 | p.y = abs(p.y)-.1; // Leaf thickness 499 | f = max(f, e); // Clamp rib tubes to leaf outline 500 | // e = max(e, p.y); //Leaf is now flat 501 | d = min(d, min(max(e, p.y), f)); // Add leaf to ribs and stalk 502 | return {d, e}; 503 | } 504 | 505 | // Dist, stencil, colour 506 | float3 leafLayer(float3 p) { 507 | p.xz /= 20; 508 | float3 s = hash(floor(p.xzz)+.1), 509 | l = float3(leaf(float3(fract(p.xz) *20 - 10, p.y).xzy, {0,.1,0}, s), -(s.x+s.y+s.z+1)); 510 | p-=.5; 511 | Rotate2D(p.xz, 3.15); 512 | s = hash(floor(p.xzz)+.1); 513 | s = float3(leaf(float3(fract(p.xz) *20 - 10, p.y).xzy, {0,.1,0}, s), -(s.x+s.y+s.z+1)); 514 | // p.xz = fract(p.xz) *20 - 10; 515 | return l.x 7 && ray.dir.y > 0) return; 563 | // Check if we're still in bounds. If not, either trace to next bounds or exit 564 | // bool insideNow = boundingDist(p, ray.dir) < .1; 565 | // insideNow = true; 566 | // if (!insideNow) { 567 | // // Trace to bounds 568 | // ray.origin.xyz = p; 569 | // Intersection h = {float4(MAXFLOAT), {}}; 570 | // float b = intersectBounds(ray, h); 571 | // if (b >= hit.result.w) break; // Ray didn't intersect bounds before next surface 572 | // p += ray.dir * (b + Epsilon); // trace to next bounds 573 | // continue; 574 | // } 575 | // Total distance ray travelled (used for early termination and returning intersection distance) 576 | float totalDist = length(ray.origin.xyz-p);// + k.z*0; 577 | 578 | // Early termination if ray travelled further than the last intersection, or if out of bounds 579 | if(totalDist>hit.result.w) return; 580 | // if(p.y>TileSize * 18 & ray.dir.y>0) return; 581 | // if(oob(p, ray.dir)) return; 582 | // if(totalDist>hit.result.w || abs(p.y) > dfThickness+Epsilon) return; 583 | 584 | // Get the distance (x = dist, y = material ID) 585 | float2 dist=df(p, ray.dir); 586 | 587 | // Check if we hit a surface 588 | if (abs(dist.x) < Epsilon * 2) { 589 | // intersection 590 | // Get normals 591 | float2 e = float2(Epsilon * .1, 0.); 592 | float3 n=normalize( 593 | float3( 594 | df(p+e.xyy, ray.dir).x-df(p-e.xyy, ray.dir).x, 595 | df(p+e.yxy, ray.dir).x-df(p-e.yxy, ray.dir).x, 596 | df(p+e.yyx, ray.dir).x-df(p-e.yyx, ray.dir).x 597 | ) 598 | ); 599 | // Restore if nans appear 600 | if(any(isnan(n))) continue; 601 | 602 | // p.xz = fract(p.xz / 1); 603 | float3 c = floor(p.xzz/10)+10;//, q = hash(c); 604 | if (dist.y<0) { 605 | // c = mix(float3(1,.5,.6), float3(.6, .7, .4), (abs(dist.y)-1) / 3); 606 | c = hash(dist.yyy); 607 | c = mix(float3(1,.5,.2), float3(.8, .9, .2), c.x); 608 | dist.y = Leaf; 609 | } 610 | 611 | // c = {1,.6,.2}; 612 | 613 | // An array of materials 614 | Material m[]={ 615 | GroundMat, // Ground 616 | {0,{1,.8,0},1,0,0,0}, // Ring 617 | // {0, 1, 1}, 618 | {2,{0,0,0},0}, // Diamond 619 | {0, c, .02}, // Leaf 620 | // {0,hash(c)*.5+.5, fract(c.x / 4.), fract(x), q.z, 1, 1}, //4 MatTest 621 | {0,{1,.6,.2}}, //4 MatTest 622 | // {0,1,0}, //5 Pride flag 623 | // GroundMaterial, // ground 624 | // {0,{1,.1,.1},0.2,.8,.2,0,0.5}, // RedBrick 625 | // {0,{.1,1,.1},0.2,.8,.2,0,0.5}, //7 GreenBrick 626 | // {0, {1,1,1}, .5}, // 8 MarbleMat 627 | // {2,{0},1}, // 9 sea 628 | // {0,1,.3}, // 10 White 629 | // {1,{2,0.5,.5}}, // 11 pink light 630 | // {2, {1,0,0},0.2} // 12 red glass 631 | }; 632 | 633 | // Set the material 634 | hit.material = m[int(dist.y)]; 635 | 636 | // Flip normals if inside object 637 | if (hit.material.id == 2 && ray.inside) n = -n; 638 | 639 | // Set the intersection result 640 | hit.result=float4(n,totalDist); 641 | 642 | // hit.material = {1, i / 30}; 643 | return; 644 | } 645 | 646 | // Step along ray 647 | p+=ray.dir*dist.x*scale; 648 | } 649 | } 650 | 651 | Intersection intersectScene(thread Ray &ray, float3 k){ 652 | Intersection hit, testHit; 653 | float3 n, o, p, blue, pink; 654 | float thickness; 655 | bool l; 656 | 657 | switch (SCENEFUNC) { 658 | case GLASSSCENE: 659 | // Create an intersection result. This is the background, so no intersection needed. The normal is 660 | // the opposite of the ray dir and distance is 'far away'. Material is undefined, we'll set that later. 661 | hit = {float4(-ray.dir, MAXFLOAT), {1,mix(float3(1,.5,.3)*2, float3(0,.0,1), pow(max(0., ray.dir.y), 0.4))}}; 662 | 663 | intersectGround(ray, hit, {0,1}); // White 664 | 665 | // Back wall 666 | intersectPlane(ray, {0,0,-1,-10}, hit, {0,1,.3}); 667 | 668 | // Intersect marble 669 | n = normalize(float3(-1,3,-1)), o={17,5.01,0}, 670 | blue = {.08,.05,0}, 671 | pink = {0,.06,.04}; 672 | thickness = 1-Epsilon * 8; 673 | 674 | // Jiggled marble 675 | n = normalize(float3(1,0,0)); 676 | sphereSliceMirror(ray, hit, {2,blue}, float4(o+n*4,5), float4(n,thickness), -4); 677 | sphereSliceMirror(ray, hit, {2,pink}, float4(o+n*6,5), float4(n,thickness), -2); 678 | sphereSliceMirror(ray, hit, {2,0}, float4(o+n*2,5), float4(n,thickness), 0); 679 | sphereSliceMirror(ray, hit, {2,pink}, float4(o-n*4,5), float4(n,thickness), 2); 680 | sphereSliceMirror(ray, hit, {2,blue}, float4(o-n*8,5), float4(n,thickness), 4); 681 | 682 | // Sliced marble 683 | n = normalize(float3(-1,-.0,-.55)); 684 | sphereSliceMirror(ray, hit, {2,blue}, float4(4,5.01,0,5), float4(n,1), -4); 685 | sphereSliceMirror(ray, hit, {2,pink}, float4(2,5.01,0,5), float4(n,1), -2); 686 | sphereSliceMirror(ray, hit, {2}, float4(0,5.01,0,5), float4(n,1), 0); 687 | sphereSliceMirror(ray, hit, {2,pink}, float4(-2,5.01,0,5), float4(n,1), 2); 688 | sphereSliceMirror(ray, hit, {2,blue}, float4(-4,5.01,0,5), float4(n,1), 4); 689 | 690 | // Joined marble 691 | n = normalize(float3(3,1,.5)); 692 | sphereSliceMirror(ray, hit, {2,blue}, float4(-17,5.01,0,5), float4(n,thickness), -4); 693 | sphereSliceMirror(ray, hit, {2,pink}, float4(-17,5.01,0,5), float4(n,thickness), -2); 694 | sphereSliceMirror(ray, hit, {2,0}, float4(-17,5.01,0,5), float4(n,thickness), 0); 695 | sphereSliceMirror(ray, hit, {2,pink}, float4(-17,5.01,0,5), float4(n,thickness), 2); 696 | sphereSliceMirror(ray, hit, {2,blue}, float4(-17,5.01,0,5), float4(n,thickness), 4); 697 | 698 | // Lights 699 | 700 | // intersectSphere(ray, float4(-40, 40, 0, 10), hit, {1, 15});// blue 701 | intersectSphere(ray, float4(45, 20, 6, 4), hit, {1, {40,35,30}});// blue 702 | intersectSphere(ray, float4(-20, 30, 5, 5), hit, {1, {10,15,20}});// blue 703 | break; 704 | 705 | case FORESTSCENE: 706 | 707 | hit = {float4(-ray.dir, MAXFLOAT), {1,mix(float3(.8,.8,.5), float3(1,.7,.3)*2, pow(max(0., ray.dir.y), 0.5))/4}}; 708 | 709 | // Ground 710 | intersectGround(ray, hit, {0,{.27,.16,.1},.1}); 711 | 712 | // Intersect sun mask 713 | float3 lightPosition = float3(-280,250,200)*2; 714 | 715 | testHit = hit; 716 | intersectPlane(ray, float4(normalize(lightPosition), length(lightPosition) / 3), testHit, {}); 717 | if (testHit.result.w < hit.result.w) { 718 | p = fract((ray.origin.xyz + ray.dir * testHit.result.w+150)/30)-.5; 719 | l = length(p)-.45 < 0; 720 | p = fract((ray.origin.xyz + ray.dir * testHit.result.w * 1.2-10)/5)-.5; 721 | l &= length(p)-.5 < 0; 722 | if (l) { 723 | // Intersect sun 724 | intersectSphere(ray, float4(lightPosition, 50), hit, {1, float3(150,70,20)*4}); 725 | } 726 | } 727 | 728 | // Raymarch (do this last for better performance) 729 | rayMarch(ray, hit, k); 730 | break; 731 | } 732 | 733 | return hit; 734 | } 735 | 736 | #pragma mark - Tracing 737 | //================// 738 | 739 | //float focalLength(Ray ray, float3 camPos, float3 camTarget) { 740 | // float3 n =normalize(camPos - camTarget); 741 | // return -dot(ray.origin.xyz - camPos, n) / dot(ray.dir, n); 742 | //} 743 | 744 | /// Sets the camera up. uv = screen position, k = random value 745 | Ray setupCamera(Ray ray, float2 uv, float3 k){ 746 | float3 camPos, camTarget; 747 | 748 | float zoom = 2, aperture = 0, softLens = 0, fisheye = 0, bokehShape = 0.25; 749 | 750 | float t = ray.origin.w / 12., segment = floor(mod(t, 3.)); 751 | t = fract(t); 752 | 753 | switch (SCENEFUNC) { 754 | case GLASSSCENE: 755 | // segment = 2; 756 | if (segment == 0) { 757 | // Pan 758 | camPos = float3(t * -30 + 15,12,-25); 759 | camTarget = float3(t * -40 + 20,4,0); 760 | zoom = 4; 761 | softLens = 2; 762 | fisheye = 1.7; 763 | } else if (segment == 1) { 764 | // Focus pull 765 | camPos = float3(-40, 25, 4); 766 | camTarget = float3(t * -35 + 20,4,0); 767 | zoom = 6 - fract(t) * 4; 768 | aperture = 4.0; 769 | } else { 770 | // Rotation 771 | camPos = float3(0, 7, -7); 772 | camTarget = float3(-18, 5-t*5, 0); 773 | Rotate2D(camPos.xz, -t*2 + 1); 774 | camPos.x -= 15; 775 | zoom = 1; 776 | fisheye = 1.0; 777 | } 778 | break; 779 | case FORESTSCENE: 780 | camPos = float3(-20, 33, 30); 781 | camTarget = float3(12,2,56); 782 | zoom = 2.5; 783 | fisheye = 1.5; 784 | aperture = 1.0; 785 | softLens = 1.5; 786 | break; 787 | } 788 | 789 | // Basic lens zoom first 790 | uv /= zoom; 791 | 792 | // get the ray dir 793 | ray.dir = camTarget - camPos; 794 | 795 | // f = focal length 796 | float fl = length(ray.dir), a; 797 | ray.dir = normalize(ray.dir); 798 | 799 | // This transforms k into a random point in a sphere 800 | // k = hash(k); 801 | k = pointOnSphere(k.xy) * pow(k.z, bokehShape) * (aperture + length(uv*zoom) * softLens); 802 | 803 | // Add random sphere point * aperture to camera position for DoF 804 | // ray.origin.xyz = CameraPosition + k * Aperture;// * length(uv); // Can uncomment this to create soft focus at edges only 805 | 806 | // Update the ray direction, then project back from the camera target to the camera plane 807 | // ray.dir=normalize(CameraTarget-ray.origin.xyz); 808 | // ray.origin.xyz=CameraTarget-ray.dir*f; 809 | ray.origin.xyz = camPos; 810 | // ray.origin.xyz += k * pow(1-abs(dot(normalize(k), ray.dir)), 2); 811 | ray.dir = normalize(camTarget - ray.origin.xyz); 812 | // Transform the camera to account for uv 813 | a = rsqrt(1 - ray.dir.y * ray.dir.y); 814 | float3x3 c = { 815 | float3(-ray.dir.z, 0, ray.dir.x) * a, 816 | float3(-ray.dir.x * ray.dir.y, 1 - ray.dir.y * ray.dir.y, -ray.dir.y * ray.dir.z) * a, 817 | -ray.dir 818 | }; 819 | a=length(uv); 820 | 821 | // Scaling for fisheye distortion 822 | float f = a * fisheye; 823 | 824 | // Last bit of uv transform 825 | ray.dir=normalize(c*mix(float3(uv,-1),float3(uv/a*sin(f),-cos(f)),.4)); 826 | 827 | // fl = focalLength(ray); 828 | float3 t2 = normalize(camPos - camTarget); // plane facing camera 829 | fl = -dot(ray.origin.xyz - camTarget, t2) / dot(ray.dir, t2); 830 | t2 = ray.origin.xyz + ray.dir * fl; 831 | ray.origin.xyz += k; 832 | ray.dir = normalize(t2 - ray.origin.xyz); 833 | 834 | return ray; 835 | } 836 | 837 | float3 traceRay(float2 uv, float pixelSize, float t, int rayCount) { 838 | Ray ray; 839 | 840 | int bounces; 841 | switch (SCENEFUNC) { 842 | case GLASSSCENE: 843 | bounces = 15; 844 | break; 845 | case FORESTSCENE: 846 | bounces = 4; 847 | break; 848 | } 849 | 850 | float3 lightSum=0, // Light sum, we add lights to this and return it at the end 851 | glassCol, 852 | k=hash(float3(uv + t,t)); // initial random value 853 | // k=hash(float3(t *0+ 0.1)); // initial random value 854 | // k = hash(float3(tgp, t+.1)); 855 | // k = hash(uv.yyy); 856 | 857 | // Go through rays 858 | for(int j=0; j= 2.0) break; 926 | // k = hash(floor(ray.origin.xyz*1000)); 927 | } 928 | 929 | // New hash value for next ray 930 | k=hash(k); 931 | } 932 | return lightSum; 933 | } 934 | 935 | #pragma mark YUV 936 | float3 rgbToYUV709 (float3 rgb) { 937 | return float3x3( 938 | float3(.2126, .7152, .00722), 939 | float3(-.1145, -.3854, .5), 940 | float3(.5, -.4541, -.04584) 941 | ) * rgb; 942 | } 943 | 944 | float3 yuv709ToRGB (float3 yuv) { 945 | return float3x3( 946 | float3(1.069, .1289, 1.5749), 947 | float3(1.069, -0.058, -0.468), 948 | float3(1.069, 1.984, 0) 949 | ) * yuv; 950 | } 951 | 952 | #pragma mark - Compute functions 953 | //=========================// 954 | // 955 | kernel void k( 956 | texture2d o [[texture(0)]], 957 | texture2d i [[texture(1)]], 958 | constant float&t[[buffer(0)]], 959 | constant int &rays[[buffer(1)]], 960 | constant uint2 &gridOffset[[buffer(2), function_constant(INCREMENTALRENDER)]], 961 | uint2 g[[thread_position_in_grid]] 962 | ){ 963 | 964 | float brightness = 1, 965 | gamma = 1./2.2, 966 | contrast = 1.0, 967 | contrastBias = 0.5, 968 | blackPoint = 0.0, 969 | whitePoint = 1.0, 970 | warmth = 1, 971 | saturation = 1.0; 972 | 973 | float2 shadowTint = {0,0}, 974 | highlightTint = {0,0}, 975 | tint = {0,0}; 976 | 977 | switch (SCENEFUNC) { 978 | case GLASSSCENE: 979 | gamma = .6; 980 | contrastBias = 0.4; 981 | saturation = 1.2; 982 | shadowTint = {-.1,.4}; 983 | highlightTint = {-.1,-.8}; 984 | tint = {-.2,-.05}; 985 | break; 986 | case FORESTSCENE: 987 | whitePoint = 2.0; 988 | gamma = .5; 989 | contrast = 1.2; 990 | contrastBias = 0.3; 991 | saturation = 0.5; 992 | warmth = 0.7; 993 | tint = {-0.3,0.1}; 994 | shadowTint = {-0.3,-0.2}; 995 | highlightTint = {1,1}; 996 | break; 997 | } 998 | float2 r=float2(o.get_width(),o.get_height()); 999 | float2 u; 1000 | if (INCREMENTALRENDER) { 1001 | u = (float2((g + gridOffset)*2)-r)/float2(r.y, -r.y); 1002 | } else { 1003 | u = (float2((g)*2)-r)/float2(r.y, -r.y); 1004 | } 1005 | 1006 | float3 p = traceRay( 1007 | u, 1008 | 5 / r.x, // AA 1009 | fmod(t, M_PI_F * 20.0), 1010 | rays 1011 | ); 1012 | p = p*pow(saturate(2.2-length(u)),.75) / rays; 1013 | p = pow(p, gamma); 1014 | 1015 | p = rgbToYUV709(p); 1016 | 1017 | p.z *= brightness; 1018 | p.z = (p.z-contrastBias) * contrast + contrastBias; 1019 | p.z = saturate(p.z / whitePoint) / (1 - blackPoint) + blackPoint; 1020 | 1021 | p.xy = pow(abs(p.xy),warmth) * sign(p.xy) * saturation; 1022 | 1023 | p.xy += mix(shadowTint, highlightTint, pow(max(0., p.z), 1.3)) + tint; 1024 | 1025 | p = yuv709ToRGB(p); 1026 | 1027 | if (SCENEFUNC == FORESTSCENE) { 1028 | float4 l = i.read(g + gridOffset); 1029 | l += float4(p, 1); 1030 | o.write(l, g + gridOffset); 1031 | } else { 1032 | // Standard output 1033 | o.write(float4(p, 1), g); 1034 | } 1035 | 1036 | } 1037 | 1038 | kernel void displayIntegrated ( 1039 | texture2d i [[texture(0)]], 1040 | texture2d o [[texture(1)]], 1041 | uint2 g[[thread_position_in_grid]] 1042 | ){ 1043 | float4 p = i.read(g); 1044 | p.xyz /= p.a; 1045 | p.a = 1; 1046 | o.write(p, g); 1047 | } 1048 | -------------------------------------------------------------------------------- /MetalBench/UI/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // MetalBench 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | @EnvironmentObject var renderer: Renderer 12 | 13 | var body: some View { 14 | ZStack(alignment: .bottom) { 15 | PreviewView() 16 | .frame(width: 1280, height: 720, alignment: .center) 17 | StatusOverlay() 18 | } 19 | .frame(width: 1280, height: 720, alignment: .center) 20 | } 21 | } 22 | 23 | struct ContentView_Previews: PreviewProvider { 24 | static let renderer = Renderer() 25 | 26 | static var previews: some View { 27 | ContentView() 28 | .environmentObject(renderer) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MetalBench/UI/Modifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Modifiers.swift 3 | // MetalBench 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UITextModifier: ViewModifier { 11 | func body(content: Content) -> some View { 12 | content 13 | .foregroundColor(Color.white) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MetalBench/UI/PreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewView.swift 3 | // MetalBench 4 | //‪ 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import SwiftUI 9 | import MetalKit 10 | 11 | struct PreviewView: NSViewRepresentable { 12 | typealias NSViewType = MTKView 13 | 14 | @EnvironmentObject var renderer: Renderer 15 | 16 | func makeCoordinator() -> PreviewCoordinator { 17 | PreviewCoordinator(self) 18 | } 19 | 20 | func makeNSView(context: NSViewRepresentableContext) -> MTKView { 21 | let mtkView = MTKView() 22 | mtkView.delegate = context.coordinator 23 | mtkView.device = renderer.dev 24 | mtkView.autoResizeDrawable = false 25 | mtkView.drawableSize = CGSize(width: 1280, height: 720) 26 | mtkView.framebufferOnly = false 27 | 28 | return mtkView 29 | } 30 | 31 | func updateNSView(_ nsView: MTKView, context: NSViewRepresentableContext) { 32 | // Check view is set 33 | if context.coordinator.view == nil { 34 | context.coordinator.view = nsView 35 | } 36 | 37 | // Check for device change 38 | let activeDev = context.coordinator.parent.renderer.dev 39 | if activeDev.registryID != context.coordinator.metalDevice.registryID { 40 | // New device, update the view and coordinator 41 | context.coordinator.metalDevice = activeDev 42 | nsView.device = activeDev 43 | context.coordinator.configure() 44 | context.coordinator.parent.renderer.resetStats() 45 | } 46 | 47 | // Check for scene change 48 | let activeScene = context.coordinator.parent.renderer.selectedScene 49 | if activeScene != context.coordinator.currentScene { 50 | context.coordinator.configure() 51 | context.coordinator.parent.renderer.resetStats() 52 | } 53 | } 54 | 55 | } 56 | 57 | class PreviewCoordinator : NSObject, MTKViewDelegate { 58 | var parent: PreviewView 59 | var view: MTKView? 60 | var metalDevice: MTLDevice 61 | var queue: MTLCommandQueue! 62 | var computePS: MTLComputePipelineState? 63 | var displayPS: MTLComputePipelineState? 64 | 65 | var currentScene = 0 { 66 | didSet { 67 | isRealtime = parent.renderer.sceneList[currentScene].isRealtime 68 | } 69 | } 70 | var isRealtime = true { 71 | didSet { 72 | // Either display-driven rendering or frame-available driven depending on scene 73 | view?.isPaused = !isRealtime 74 | view?.enableSetNeedsDisplay = !isRealtime 75 | 76 | if !isRealtime { 77 | // Create the storage texture for incremental rendering 78 | let desciptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba32Float, width: 1280, height: 720, mipmapped: false) 79 | desciptor.usage = [.shaderRead, .shaderWrite] 80 | desciptor.storageMode = .private 81 | incrementalTexture = metalDevice.makeTexture(descriptor: desciptor) 82 | } 83 | } 84 | } 85 | var endIncrementalRendering = false 86 | var incrementalTexture: MTLTexture? 87 | 88 | // Incremental renders tiled, 32x2 tiles 89 | var incrementalTile = SIMD2(0, 0) 90 | 91 | var viewConfigured = false 92 | 93 | var rayCountBuffer: MTLBuffer? 94 | var gridOffsetBuffer: MTLBuffer? 95 | var timeBuffer: MTLBuffer? 96 | var startTime = Date() 97 | 98 | let targetFPS = 30.0 99 | 100 | init(_ parent: PreviewView) { 101 | self.parent = parent 102 | self.metalDevice = parent.renderer.dev 103 | super.init() 104 | } 105 | 106 | /// Prepares rendering 107 | func configure() { 108 | endIncrementalRendering = true 109 | guard let library = metalDevice.makeDefaultLibrary() else { 110 | print("Failed to create libary") 111 | return 112 | } 113 | 114 | let constants = MTLFunctionConstantValues() 115 | var value = Int32(parent.renderer.selectedScene) 116 | constants.setConstantValue(&value, type: MTLDataType.int, index: 0) 117 | 118 | let kernel: MTLFunction 119 | do { 120 | kernel = try library.makeFunction(name: "k", constantValues: constants) 121 | } catch let e { 122 | print("Failed to create kernel") 123 | print(e.localizedDescription) 124 | return 125 | } 126 | 127 | do { 128 | computePS = try metalDevice.makeComputePipelineState(function: kernel) 129 | } catch let e { 130 | print ("Failed to make compute pipeline") 131 | print(e.localizedDescription) 132 | return 133 | } 134 | 135 | currentScene = parent.renderer.selectedScene 136 | 137 | queue = metalDevice.makeCommandQueue() 138 | 139 | // Set up the buffers to contain time and resolution 140 | startTime = Date() 141 | timeBuffer = metalDevice.makeBuffer(length: MemoryLayout.size, options: []) 142 | rayCountBuffer = metalDevice.makeBuffer(length: MemoryLayout.size, options: []) 143 | gridOffsetBuffer = metalDevice.makeBuffer(length: MemoryLayout>.size, options: []) 144 | updateRayCount() 145 | 146 | viewConfigured = true 147 | 148 | if !isRealtime { 149 | // Create a display pipeline 150 | let displayKernel: MTLFunction 151 | do { 152 | displayKernel = try library.makeFunction(name: "displayIntegrated", constantValues: constants) 153 | } catch let e { 154 | print("Failed to create kernel") 155 | print(e.localizedDescription) 156 | return 157 | } 158 | 159 | do { 160 | displayPS = try metalDevice.makeComputePipelineState(function: displayKernel) 161 | } catch { 162 | print ("Failed to make display pipeline") 163 | return 164 | } 165 | endIncrementalRendering = false 166 | incrementalTile = SIMD2(0, 0) 167 | drawIncremental() 168 | } 169 | } 170 | 171 | func updateTime() { 172 | guard let buffer = self.timeBuffer else { return } 173 | 174 | var t = Float(Date().timeIntervalSince(startTime) as Double) 175 | 176 | let bufferPointer = buffer.contents() 177 | memcpy(bufferPointer, &t, MemoryLayout.size) 178 | } 179 | 180 | func updateRayCount() { 181 | guard let buffer = self.rayCountBuffer else { return } 182 | 183 | let bufferPointer = buffer.contents() 184 | var rays = isRealtime ? parent.renderer.currentRayCount : 1 185 | memcpy(bufferPointer, &rays, MemoryLayout.size) 186 | } 187 | 188 | func updateGridOffset() { 189 | guard let buffer = self.gridOffsetBuffer else { return } 190 | 191 | let bufferPointer = buffer.contents() 192 | memcpy(bufferPointer, &incrementalTile, MemoryLayout.size) 193 | } 194 | 195 | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { 196 | } 197 | 198 | /// Renders incrementally into a backing texture, and marks the view as needing an update 199 | func drawIncremental() { 200 | updateTime() 201 | updateGridOffset() 202 | 203 | let commandBuffer = queue.makeCommandBuffer() 204 | 205 | guard let pipe = computePS, let buffer = commandBuffer, let cmdEncoder = buffer.makeComputeCommandEncoder() else { return } 206 | 207 | buffer.addCompletedHandler { cmdBuffer in 208 | // Calculate GPU side frame duration 209 | let start = cmdBuffer.gpuStartTime 210 | let end = cmdBuffer.gpuEndTime 211 | let duration = end - start 212 | 213 | // Update state for UI update 214 | DispatchQueue.main.async { 215 | self.parent.renderer.addFrameDuration(duration) 216 | self.view?.setNeedsDisplay(NSRect(origin: .zero, size: CGSize(width: 1280, height: 720))) 217 | } 218 | 219 | // Draw again if not ended 220 | if !self.endIncrementalRendering { self.drawIncremental() } 221 | } 222 | #warning("Crash on change GPU in forest due to pipe associated with different GPU") 223 | cmdEncoder.setComputePipelineState(pipe) 224 | 225 | cmdEncoder.setTexture(incrementalTexture, index: 0) 226 | cmdEncoder.setTexture(incrementalTexture, index: 1) 227 | cmdEncoder.setBuffer(timeBuffer!, offset: 0, index: 0) 228 | cmdEncoder.setBuffer(rayCountBuffer!, offset: 0, index: 1) 229 | cmdEncoder.setBuffer(gridOffsetBuffer!, offset: 0, index: 2) 230 | 231 | let threadGroupCount = MTLSizeMake(8, 8, 1) 232 | let threadGroups = MTLSizeMake((1280 / 32) / threadGroupCount.width, (720 / 1) / threadGroupCount.height, 1) 233 | cmdEncoder.dispatchThreadgroups(threadGroups, threadsPerThreadgroup: threadGroupCount) 234 | cmdEncoder.endEncoding() 235 | 236 | buffer.commit() 237 | 238 | incrementalTile.x += 1280 / 32 239 | if incrementalTile.x >= 1280 { incrementalTile.x = 0 } 240 | } 241 | 242 | func draw(in view: MTKView) { 243 | if !viewConfigured { configure() } 244 | 245 | guard let drawable = view.currentDrawable else { 246 | return 247 | } 248 | 249 | updateTime() 250 | updateRayCount() 251 | 252 | let commandBuffer = queue.makeCommandBuffer() 253 | 254 | guard let pipe = isRealtime ? computePS : displayPS, let buffer = commandBuffer, let cmdEncoder = buffer.makeComputeCommandEncoder() else { return } 255 | 256 | if isRealtime { 257 | buffer.addCompletedHandler { cmdBuffer in 258 | // Calculate GPU side frame duration 259 | let start = cmdBuffer.gpuStartTime 260 | let end = cmdBuffer.gpuEndTime 261 | let duration = end - start 262 | 263 | // Update state for UI update 264 | DispatchQueue.main.async { 265 | self.parent.renderer.addFrameDuration(duration) 266 | } 267 | } 268 | } 269 | 270 | cmdEncoder.setComputePipelineState(pipe) 271 | 272 | if isRealtime { 273 | cmdEncoder.setTexture(drawable.texture, index: 0) 274 | cmdEncoder.setBuffer(timeBuffer!, offset: 0, index: 0) 275 | cmdEncoder.setBuffer(rayCountBuffer!, offset: 0, index: 1) 276 | } else { 277 | cmdEncoder.setTexture(incrementalTexture, index: 0) 278 | cmdEncoder.setTexture(drawable.texture, index: 1) 279 | } 280 | 281 | let threadGroupCount = MTLSizeMake(8, 8, 1) 282 | let threadGroups = MTLSizeMake(drawable.texture.width / threadGroupCount.width, drawable.texture.height / threadGroupCount.height, 1) 283 | cmdEncoder.dispatchThreadgroups(threadGroups, threadsPerThreadgroup: threadGroupCount) 284 | cmdEncoder.endEncoding() 285 | 286 | buffer.present(drawable) 287 | buffer.commit() 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /MetalBench/UI/StatusOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusOverlay.swift 3 | // MetalBench 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StatusOverlay: View { 11 | @EnvironmentObject var renderer: Renderer 12 | 13 | var body: some View { 14 | ZStack { 15 | VStack { 16 | HStack { 17 | Picker("GPU", selection: $renderer.selectedGPU) { 18 | ForEach(0.. 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MetalBenchTests/MetalBenchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalBenchTests.swift 3 | // MetalBenchTests 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import XCTest 9 | @testable import MetalBench 10 | 11 | class MetalBenchTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /MetalBenchUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MetalBenchUITests/MetalBenchUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetalBenchUITests.swift 3 | // MetalBenchUITests 4 | // 5 | // Created by Alia on 30/07/2020. 6 | // 7 | 8 | import XCTest 9 | 10 | class MetalBenchUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MetalBench 2 | 3 | A GPU compute benchmarking tool for macOS / Metal 4 | 5 | ## About 6 | 7 | MetalBench contains several 3D scenes and utilises GPU compute shaders to render them using path tracing. Some scenes are rendered realtime, others are offline with incremental display update. 8 | 9 | The app uses accurate GPU draw timing to calculate a Rays per Second value. 10 | 11 | ![MetalBench running the Glass benchmark](Screenshots/Glass.jpg) 12 | 13 | ## How to use 14 | 15 | Select a GPU and scene, and observe the Average Rays Per Second value at the bottom right. It's best to wait several minutes, as some scenes contain several views which affect rendering speed, and some GPUs become thermally constrained after a short time and will slow down. 16 | 17 | ![MetalBench running the Forest Floor benchmark](Screenshots/Forest.jpg) -------------------------------------------------------------------------------- /Screenshots/Forest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alia-Traces/MetalBench/c7143a06ca1d0772bc54498295797f4f4b0278b2/Screenshots/Forest.jpg -------------------------------------------------------------------------------- /Screenshots/Glass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Alia-Traces/MetalBench/c7143a06ca1d0772bc54498295797f4f4b0278b2/Screenshots/Glass.jpg --------------------------------------------------------------------------------