├── .gitignore ├── Demo └── AnodizeDemo │ ├── AnodizeDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved │ └── AnodizeDemo │ ├── AnodizeDemo.entitlements │ ├── AnodizeDemoApp.swift │ ├── Anodized.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── MyCoolKernel.metal │ └── Preview Content │ └── Preview Assets.xcassets │ └── Contents.json ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── Anodize │ ├── Anodize.swift │ ├── MTLComputePipelineReflection+codegen.swift │ └── MTLDataType+swiftName.swift └── AnodizeUtil │ ├── AnodizeError.swift │ ├── GPUArray.swift │ ├── GPUBufferProvider.swift │ ├── MTLComputeCommandEncoder+Ext.swift │ ├── MTLDevice+Ext.swift │ └── MutableGPUArray.swift ├── inputfiles.png ├── outputfiles.png ├── runscript.png └── sandbox.png /.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 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | 64 | .air 65 | .metallib 66 | .DS_Store 67 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 29D86DF82D691785008455B8 /* AnodizeUtil in Frameworks */ = {isa = PBXBuildFile; productRef = 29D86DF72D691785008455B8 /* AnodizeUtil */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXFileReference section */ 14 | 29D86DE42D691750008455B8 /* AnodizeDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AnodizeDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 15 | /* End PBXFileReference section */ 16 | 17 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 18 | 29D86DE62D691750008455B8 /* AnodizeDemo */ = { 19 | isa = PBXFileSystemSynchronizedRootGroup; 20 | path = AnodizeDemo; 21 | sourceTree = ""; 22 | }; 23 | /* End PBXFileSystemSynchronizedRootGroup section */ 24 | 25 | /* Begin PBXFrameworksBuildPhase section */ 26 | 29D86DE12D691750008455B8 /* Frameworks */ = { 27 | isa = PBXFrameworksBuildPhase; 28 | buildActionMask = 2147483647; 29 | files = ( 30 | 29D86DF82D691785008455B8 /* AnodizeUtil in Frameworks */, 31 | ); 32 | runOnlyForDeploymentPostprocessing = 0; 33 | }; 34 | /* End PBXFrameworksBuildPhase section */ 35 | 36 | /* Begin PBXGroup section */ 37 | 29D86DDB2D691750008455B8 = { 38 | isa = PBXGroup; 39 | children = ( 40 | 29D86DE62D691750008455B8 /* AnodizeDemo */, 41 | 29D86DE52D691750008455B8 /* Products */, 42 | ); 43 | sourceTree = ""; 44 | }; 45 | 29D86DE52D691750008455B8 /* Products */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | 29D86DE42D691750008455B8 /* AnodizeDemo.app */, 49 | ); 50 | name = Products; 51 | sourceTree = ""; 52 | }; 53 | /* End PBXGroup section */ 54 | 55 | /* Begin PBXNativeTarget section */ 56 | 29D86DE32D691750008455B8 /* AnodizeDemo */ = { 57 | isa = PBXNativeTarget; 58 | buildConfigurationList = 29D86DF32D691751008455B8 /* Build configuration list for PBXNativeTarget "AnodizeDemo" */; 59 | buildPhases = ( 60 | 2907F24D2D6F9CFD00B48EB0 /* ShellScript */, 61 | 29D86DE02D691750008455B8 /* Sources */, 62 | 29D86DE12D691750008455B8 /* Frameworks */, 63 | 29D86DE22D691750008455B8 /* Resources */, 64 | ); 65 | buildRules = ( 66 | ); 67 | dependencies = ( 68 | ); 69 | fileSystemSynchronizedGroups = ( 70 | 29D86DE62D691750008455B8 /* AnodizeDemo */, 71 | ); 72 | name = AnodizeDemo; 73 | packageProductDependencies = ( 74 | 29D86DF72D691785008455B8 /* AnodizeUtil */, 75 | ); 76 | productName = AnodizeDemo; 77 | productReference = 29D86DE42D691750008455B8 /* AnodizeDemo.app */; 78 | productType = "com.apple.product-type.application"; 79 | }; 80 | /* End PBXNativeTarget section */ 81 | 82 | /* Begin PBXProject section */ 83 | 29D86DDC2D691750008455B8 /* Project object */ = { 84 | isa = PBXProject; 85 | attributes = { 86 | BuildIndependentTargetsInParallel = 1; 87 | LastSwiftUpdateCheck = 1620; 88 | LastUpgradeCheck = 1620; 89 | TargetAttributes = { 90 | 29D86DE32D691750008455B8 = { 91 | CreatedOnToolsVersion = 16.2; 92 | }; 93 | }; 94 | }; 95 | buildConfigurationList = 29D86DDF2D691750008455B8 /* Build configuration list for PBXProject "AnodizeDemo" */; 96 | developmentRegion = en; 97 | hasScannedForEncodings = 0; 98 | knownRegions = ( 99 | en, 100 | Base, 101 | ); 102 | mainGroup = 29D86DDB2D691750008455B8; 103 | minimizedProjectReferenceProxies = 1; 104 | packageReferences = ( 105 | 29D86DF62D691785008455B8 /* XCLocalSwiftPackageReference "../../../Anodize" */, 106 | ); 107 | preferredProjectObjectVersion = 77; 108 | productRefGroup = 29D86DE52D691750008455B8 /* Products */; 109 | projectDirPath = ""; 110 | projectRoot = ""; 111 | targets = ( 112 | 29D86DE32D691750008455B8 /* AnodizeDemo */, 113 | ); 114 | }; 115 | /* End PBXProject section */ 116 | 117 | /* Begin PBXResourcesBuildPhase section */ 118 | 29D86DE22D691750008455B8 /* Resources */ = { 119 | isa = PBXResourcesBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXResourcesBuildPhase section */ 126 | 127 | /* Begin PBXShellScriptBuildPhase section */ 128 | 2907F24D2D6F9CFD00B48EB0 /* ShellScript */ = { 129 | isa = PBXShellScriptBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | ); 133 | inputFileListPaths = ( 134 | ); 135 | inputPaths = ( 136 | "$(SRCROOT)/AnodizeDemo/MyCoolKernel.metal", 137 | ); 138 | outputFileListPaths = ( 139 | ); 140 | outputPaths = ( 141 | "$(SRCROOT)/AnodizeDemo/Anodized.swift", 142 | ); 143 | runOnlyForDeploymentPostprocessing = 0; 144 | shellPath = /bin/sh; 145 | shellScript = "swift run Anodize $SRCROOT/AnodizeDemo/MyCoolKernel.metal -o $SRCROOT/AnodizeDemo/Anodized.swift\n"; 146 | }; 147 | /* End PBXShellScriptBuildPhase section */ 148 | 149 | /* Begin PBXSourcesBuildPhase section */ 150 | 29D86DE02D691750008455B8 /* Sources */ = { 151 | isa = PBXSourcesBuildPhase; 152 | buildActionMask = 2147483647; 153 | files = ( 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXSourcesBuildPhase section */ 158 | 159 | /* Begin XCBuildConfiguration section */ 160 | 29D86DF12D691751008455B8 /* Debug */ = { 161 | isa = XCBuildConfiguration; 162 | buildSettings = { 163 | ALWAYS_SEARCH_USER_PATHS = NO; 164 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 165 | CLANG_ANALYZER_NONNULL = YES; 166 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 167 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 168 | CLANG_ENABLE_MODULES = YES; 169 | CLANG_ENABLE_OBJC_ARC = YES; 170 | CLANG_ENABLE_OBJC_WEAK = YES; 171 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 172 | CLANG_WARN_BOOL_CONVERSION = YES; 173 | CLANG_WARN_COMMA = YES; 174 | CLANG_WARN_CONSTANT_CONVERSION = YES; 175 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 176 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 177 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 178 | CLANG_WARN_EMPTY_BODY = YES; 179 | CLANG_WARN_ENUM_CONVERSION = YES; 180 | CLANG_WARN_INFINITE_RECURSION = YES; 181 | CLANG_WARN_INT_CONVERSION = YES; 182 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 183 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 184 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 185 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 186 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 187 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 188 | CLANG_WARN_STRICT_PROTOTYPES = YES; 189 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 190 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 191 | CLANG_WARN_UNREACHABLE_CODE = YES; 192 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 193 | COPY_PHASE_STRIP = NO; 194 | DEBUG_INFORMATION_FORMAT = dwarf; 195 | ENABLE_STRICT_OBJC_MSGSEND = YES; 196 | ENABLE_TESTABILITY = YES; 197 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 198 | GCC_C_LANGUAGE_STANDARD = gnu17; 199 | GCC_DYNAMIC_NO_PIC = NO; 200 | GCC_NO_COMMON_BLOCKS = YES; 201 | GCC_OPTIMIZATION_LEVEL = 0; 202 | GCC_PREPROCESSOR_DEFINITIONS = ( 203 | "DEBUG=1", 204 | "$(inherited)", 205 | ); 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 213 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 214 | MTL_FAST_MATH = YES; 215 | ONLY_ACTIVE_ARCH = YES; 216 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 217 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 218 | }; 219 | name = Debug; 220 | }; 221 | 29D86DF22D691751008455B8 /* Release */ = { 222 | isa = XCBuildConfiguration; 223 | buildSettings = { 224 | ALWAYS_SEARCH_USER_PATHS = NO; 225 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 226 | CLANG_ANALYZER_NONNULL = YES; 227 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 228 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_ENABLE_OBJC_WEAK = YES; 232 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 233 | CLANG_WARN_BOOL_CONVERSION = YES; 234 | CLANG_WARN_COMMA = YES; 235 | CLANG_WARN_CONSTANT_CONVERSION = YES; 236 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 237 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 238 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 239 | CLANG_WARN_EMPTY_BODY = YES; 240 | CLANG_WARN_ENUM_CONVERSION = YES; 241 | CLANG_WARN_INFINITE_RECURSION = YES; 242 | CLANG_WARN_INT_CONVERSION = YES; 243 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 244 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 245 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 246 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 247 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 248 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 249 | CLANG_WARN_STRICT_PROTOTYPES = YES; 250 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 251 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 252 | CLANG_WARN_UNREACHABLE_CODE = YES; 253 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 254 | COPY_PHASE_STRIP = NO; 255 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 256 | ENABLE_NS_ASSERTIONS = NO; 257 | ENABLE_STRICT_OBJC_MSGSEND = YES; 258 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 259 | GCC_C_LANGUAGE_STANDARD = gnu17; 260 | GCC_NO_COMMON_BLOCKS = YES; 261 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 262 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 263 | GCC_WARN_UNDECLARED_SELECTOR = YES; 264 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 265 | GCC_WARN_UNUSED_FUNCTION = YES; 266 | GCC_WARN_UNUSED_VARIABLE = YES; 267 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 268 | MTL_ENABLE_DEBUG_INFO = NO; 269 | MTL_FAST_MATH = YES; 270 | SWIFT_COMPILATION_MODE = wholemodule; 271 | }; 272 | name = Release; 273 | }; 274 | 29D86DF42D691751008455B8 /* Debug */ = { 275 | isa = XCBuildConfiguration; 276 | buildSettings = { 277 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 278 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 279 | CODE_SIGN_ENTITLEMENTS = AnodizeDemo/AnodizeDemo.entitlements; 280 | CODE_SIGN_STYLE = Automatic; 281 | CURRENT_PROJECT_VERSION = 1; 282 | DEVELOPMENT_ASSET_PATHS = "\"AnodizeDemo/Preview Content\""; 283 | DEVELOPMENT_TEAM = PEJFWJLG2S; 284 | ENABLE_HARDENED_RUNTIME = YES; 285 | ENABLE_PREVIEWS = YES; 286 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 287 | GENERATE_INFOPLIST_FILE = YES; 288 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 289 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 290 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 291 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 292 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 293 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 294 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 295 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 296 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 297 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 298 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 299 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 300 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 301 | MACOSX_DEPLOYMENT_TARGET = 15.2; 302 | MARKETING_VERSION = 1.0; 303 | PRODUCT_BUNDLE_IDENTIFIER = com.audulus.AnodizeDemo; 304 | PRODUCT_NAME = "$(TARGET_NAME)"; 305 | SDKROOT = auto; 306 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 307 | SWIFT_EMIT_LOC_STRINGS = YES; 308 | SWIFT_VERSION = 5.0; 309 | TARGETED_DEVICE_FAMILY = "1,2,7"; 310 | XROS_DEPLOYMENT_TARGET = 2.2; 311 | }; 312 | name = Debug; 313 | }; 314 | 29D86DF52D691751008455B8 /* Release */ = { 315 | isa = XCBuildConfiguration; 316 | buildSettings = { 317 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 318 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 319 | CODE_SIGN_ENTITLEMENTS = AnodizeDemo/AnodizeDemo.entitlements; 320 | CODE_SIGN_STYLE = Automatic; 321 | CURRENT_PROJECT_VERSION = 1; 322 | DEVELOPMENT_ASSET_PATHS = "\"AnodizeDemo/Preview Content\""; 323 | DEVELOPMENT_TEAM = PEJFWJLG2S; 324 | ENABLE_HARDENED_RUNTIME = YES; 325 | ENABLE_PREVIEWS = YES; 326 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 327 | GENERATE_INFOPLIST_FILE = YES; 328 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; 329 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; 330 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; 331 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; 332 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; 333 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; 334 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; 335 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; 336 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 337 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 338 | IPHONEOS_DEPLOYMENT_TARGET = 18.2; 339 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; 340 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; 341 | MACOSX_DEPLOYMENT_TARGET = 15.2; 342 | MARKETING_VERSION = 1.0; 343 | PRODUCT_BUNDLE_IDENTIFIER = com.audulus.AnodizeDemo; 344 | PRODUCT_NAME = "$(TARGET_NAME)"; 345 | SDKROOT = auto; 346 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; 347 | SWIFT_EMIT_LOC_STRINGS = YES; 348 | SWIFT_VERSION = 5.0; 349 | TARGETED_DEVICE_FAMILY = "1,2,7"; 350 | XROS_DEPLOYMENT_TARGET = 2.2; 351 | }; 352 | name = Release; 353 | }; 354 | /* End XCBuildConfiguration section */ 355 | 356 | /* Begin XCConfigurationList section */ 357 | 29D86DDF2D691750008455B8 /* Build configuration list for PBXProject "AnodizeDemo" */ = { 358 | isa = XCConfigurationList; 359 | buildConfigurations = ( 360 | 29D86DF12D691751008455B8 /* Debug */, 361 | 29D86DF22D691751008455B8 /* Release */, 362 | ); 363 | defaultConfigurationIsVisible = 0; 364 | defaultConfigurationName = Release; 365 | }; 366 | 29D86DF32D691751008455B8 /* Build configuration list for PBXNativeTarget "AnodizeDemo" */ = { 367 | isa = XCConfigurationList; 368 | buildConfigurations = ( 369 | 29D86DF42D691751008455B8 /* Debug */, 370 | 29D86DF52D691751008455B8 /* Release */, 371 | ); 372 | defaultConfigurationIsVisible = 0; 373 | defaultConfigurationName = Release; 374 | }; 375 | /* End XCConfigurationList section */ 376 | 377 | /* Begin XCLocalSwiftPackageReference section */ 378 | 29D86DF62D691785008455B8 /* XCLocalSwiftPackageReference "../../../Anodize" */ = { 379 | isa = XCLocalSwiftPackageReference; 380 | relativePath = ../../../Anodize; 381 | }; 382 | /* End XCLocalSwiftPackageReference section */ 383 | 384 | /* Begin XCSwiftPackageProductDependency section */ 385 | 29D86DF72D691785008455B8 /* AnodizeUtil */ = { 386 | isa = XCSwiftPackageProductDependency; 387 | productName = AnodizeUtil; 388 | }; 389 | /* End XCSwiftPackageProductDependency section */ 390 | }; 391 | rootObject = 29D86DDC2D691750008455B8 /* Project object */; 392 | } 393 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "c420bb3248ccd53ab97c546108c92ca296dd521f9acbdf76500bb6a05824c38b", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-argument-parser", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-argument-parser", 8 | "state" : { 9 | "revision" : "41982a3656a71c768319979febd796c6fd111d5c", 10 | "version" : "1.5.0" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/AnodizeDemo.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 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/AnodizeDemoApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnodizeDemoApp.swift 3 | // AnodizeDemo 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | import SwiftUI 9 | import AnodizeUtil 10 | 11 | func runMyKernel() { 12 | 13 | do { 14 | let device = MTLCreateSystemDefaultDevice()! 15 | let kernel = MyCoolKernel(device: device) 16 | 17 | let array = try! MutableGPUArray(data: [1,2,3]) 18 | 19 | let queue = device.makeCommandQueue()! 20 | let buf = queue.makeCommandBuffer()! 21 | 22 | try kernel 23 | .begin(buf) 24 | .buffer(array) 25 | .value(bytes: 1) 26 | .dispatch(threads: 3, threadsPerThreadgroup: 1) 27 | .end() 28 | 29 | buf.commit() 30 | buf.waitUntilCompleted() 31 | 32 | print("array: \(array.array)") 33 | 34 | } catch { 35 | print("⚠️ error: \(error)") 36 | } 37 | } 38 | 39 | @main 40 | struct AnodizeDemoApp: App { 41 | var body: some Scene { 42 | WindowGroup { 43 | ContentView() 44 | .onAppear { 45 | runMyKernel() 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/Anodized.swift: -------------------------------------------------------------------------------- 1 | // This file is generated by Anodize. DO NOT EDIT. 2 | import Metal 3 | import AnodizeUtil 4 | import simd 5 | class MyCoolKernel { 6 | private var pipeline: MTLComputePipelineState 7 | init(device: MTLDevice) { self.pipeline = device.makeComputePipeline(name: "MyCoolKernel") } 8 | func begin(_ buf: MTLCommandBuffer) throws -> BindingWrapper { 9 | guard let enc = buf.makeComputeCommandEncoder() else { throw AnodizeError.metalError("couldn't create MTLComputeCommandEncoder") } 10 | enc.label = "MyCoolKernel" 11 | enc.setComputePipelineState(pipeline) 12 | return .init(enc: enc) 13 | } 14 | struct BindingWrapper { 15 | let enc: MTLComputeCommandEncoder 16 | func bind_buffer(_ buffer: MTLBuffer) -> Self { 17 | enc.setBuffer(buffer, index: 0) 18 | return self 19 | } 20 | func buffer(_ array: any MutableGPUBufferProvider) -> Self { 21 | enc.setBuffer(array, index: 0) 22 | return self 23 | } 24 | func bind_value(_ buffer: MTLBuffer) -> Self { 25 | enc.setBuffer(buffer, index: 1) 26 | return self 27 | } 28 | func value(bytes value: Float) -> Self { 29 | enc.setBytes(value, index: 1) 30 | return self 31 | } 32 | func value(_ array: any GPUBufferProvider) -> Self { 33 | enc.setBuffer(array, index: 1) 34 | return self 35 | } 36 | func wait(_ fence: MTLFence) -> Self { 37 | enc.waitForFence(fence) 38 | return self 39 | } 40 | func update(_ fence: MTLFence) -> Self { 41 | enc.updateFence(fence) 42 | return self 43 | } 44 | func dispatch(threadgroups: MTLSize, threadsPerThreadgroup: MTLSize) -> Self { 45 | enc.dispatchThreadgroups(threadgroups, threadsPerThreadgroup: threadsPerThreadgroup) 46 | return self 47 | } 48 | func dispatch(threadgroups: Int, threadsPerThreadgroup: Int) -> Self { 49 | enc.dispatchThreadgroups(MTLSizeMake(threadgroups, 1, 1), threadsPerThreadgroup: MTLSizeMake(threadsPerThreadgroup, 1, 1)) 50 | return self 51 | } 52 | func dispatch(threads: Int, threadsPerThreadgroup: Int) -> Self { 53 | enc.dispatchThreadgroups(MTLSizeMake(Int(ceil(Double(threads)/Double(threadsPerThreadgroup))), 1, 1), threadsPerThreadgroup: MTLSizeMake(threadsPerThreadgroup, 1, 1)) 54 | return self 55 | } 56 | func end() { enc.endEncoding() } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/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 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | }, 30 | { 31 | "idiom" : "mac", 32 | "scale" : "1x", 33 | "size" : "16x16" 34 | }, 35 | { 36 | "idiom" : "mac", 37 | "scale" : "2x", 38 | "size" : "16x16" 39 | }, 40 | { 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "32x32" 44 | }, 45 | { 46 | "idiom" : "mac", 47 | "scale" : "2x", 48 | "size" : "32x32" 49 | }, 50 | { 51 | "idiom" : "mac", 52 | "scale" : "1x", 53 | "size" : "128x128" 54 | }, 55 | { 56 | "idiom" : "mac", 57 | "scale" : "2x", 58 | "size" : "128x128" 59 | }, 60 | { 61 | "idiom" : "mac", 62 | "scale" : "1x", 63 | "size" : "256x256" 64 | }, 65 | { 66 | "idiom" : "mac", 67 | "scale" : "2x", 68 | "size" : "256x256" 69 | }, 70 | { 71 | "idiom" : "mac", 72 | "scale" : "1x", 73 | "size" : "512x512" 74 | }, 75 | { 76 | "idiom" : "mac", 77 | "scale" : "2x", 78 | "size" : "512x512" 79 | } 80 | ], 81 | "info" : { 82 | "author" : "xcode", 83 | "version" : 1 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // AnodizeDemo 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundStyle(.tint) 16 | Text("Hello, world!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | #Preview { 23 | ContentView() 24 | } 25 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/MyCoolKernel.metal: -------------------------------------------------------------------------------- 1 | // 2 | // MyCoolKernel.metal 3 | // AnodizeDemo 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | kernel void MyCoolKernel(device float* buffer, 12 | constant float& value, 13 | uint tid [[thread_position_in_grid]]) { 14 | buffer[tid] += value; 15 | } 16 | -------------------------------------------------------------------------------- /Demo/AnodizeDemo/AnodizeDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Audulus LLC 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 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "c6dbea4fa7a7aad53293b18c088f6ed1765be1a0e12eb0ddd6e525f6aa8468a6", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-argument-parser", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-argument-parser", 8 | "state" : { 9 | "revision" : "41982a3656a71c768319979febd796c6fd111d5c", 10 | "version" : "1.5.0" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Anodize", 8 | platforms: [.macOS(.v15)], 9 | products: [ 10 | .executable(name: "Anodize", targets: ["Anodize"]), 11 | .library( 12 | name: "AnodizeUtil", 13 | targets: ["AnodizeUtil"]) 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package, defining a module or a test suite. 20 | // Targets can depend on other targets in this package and products from dependencies. 21 | .executableTarget( 22 | name: "Anodize", 23 | dependencies: [ 24 | .product(name: "ArgumentParser", package: "swift-argument-parser") 25 | ]), 26 | .target( 27 | name: "AnodizeUtil") 28 | ] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anodize 2 | 3 | Type safety for Metal. 🤘 4 | 5 | Used in [Sculptura](https://sculptura.app/) 6 | 7 | Suppose we've got the following MSL kernel: 8 | 9 | ```Metal 10 | kernel void MyCoolKernel(device float* buffer, 11 | constant float& value, 12 | uint tid [[thread_position_in_grid]]) { 13 | buffer[tid] += value; 14 | } 15 | ``` 16 | 17 | Running `swift run Anodize MyCoolKernel.metal -o Anodized.swift` generates the file `Anodized.swift` which contains wrappers for our kernel(s). 18 | 19 | And then call the kernel from Swift with type safety, without worrying about binding indices. 😎 20 | 21 | ```Swift 22 | 23 | let device = MTLCreateSystemDefaultDevice()! 24 | let kernel = MyCoolKernel(device: device) 25 | 26 | let array = try MutableGPUArray(data: [1,2,3]) 27 | 28 | let queue = device.makeCommandQueue()! 29 | let buf = queue.makeCommandBuffer()! 30 | 31 | try kernel 32 | .begin(buf) 33 | .buffer(array) 34 | .value(bytes: 1) 35 | .dispatch(threads: 3, threadsPerThreadgroup: 1) 36 | .end() 37 | 38 | buf.commit() 39 | buf.waitUntilCompleted() 40 | 41 | print("array: \(array.array)") // prints [2.0, 3.0, 4.0] 42 | 43 | ``` 44 | 45 | ### Xcode Integration 46 | 47 | You can run Anodize on every build when you change your metal files. It's actually pretty quick, because it doesn't have to run the compiler backend and it compiles in parallel. Sculptura has about 60 kernels and it takes about 2.5s on my Mac Mini M2. 48 | 49 | First, unforunately, you need to disable script sandboxing because Anodize uses Metal: 50 | 51 | script sandboxing 52 | 53 | Then create a Run Script build phase. Be sure to move it before the Compile Sources build phase: 54 | 55 | ![run script build phase](runscript.png) 56 | 57 | Set the `Input Files` to your metal files you are compiling, and `Output Files` to the generated file: 58 | 59 | input files 60 | 61 | output files 62 | 63 | See the [demo project](./Demo/AnodizeDemo/) for an example. 64 | 65 | ### Notes 66 | 67 | - Type safety is limited by Metal's reflection API. For user-defined types, we can only check the size is what is expected (this seems reasonably good). 68 | - You can adopt Anodize gradually in your codebase (as I'm doing in Sculptura). 69 | - You don't have to use `GPUArray`, you can make another class that conforms to `GPUBufferProvider`. 70 | - You can pass a raw `MTLBuffer` but you'll lose type safety (at least the binding indices will be correct). 71 | - Vertex and fragment functions aren't yet supported. 72 | -------------------------------------------------------------------------------- /Sources/Anodize/Anodize.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import ArgumentParser 4 | import Metal 5 | 6 | func shell(_ args: [String]) -> Int32 { 7 | let task = Process() 8 | task.launchPath = "/usr/bin/env" 9 | task.arguments = args 10 | task.launch() 11 | task.waitUntilExit() 12 | return task.terminationStatus 13 | } 14 | 15 | /// Runs multiple shell commands in parallel, limited by available CPU cores 16 | func runCommandsInParallel(commands: [[String]]) async { 17 | await withTaskGroup(of: Void.self) { group in 18 | for command in commands { 19 | group.addTask { 20 | if shell(command) != 0 { 21 | print("⚠️ failed to compile metal file") 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | func computePipelineReflection(function: MTLFunction) -> MTLComputePipelineReflection { 29 | 30 | let device = MTLCreateSystemDefaultDevice()! 31 | let computeDesc = MTLComputePipelineDescriptor() 32 | computeDesc.computeFunction = function 33 | return try! device.makeComputePipelineState(descriptor: computeDesc, options: [.bindingInfo, .bufferTypeInfo]).1! 34 | } 35 | 36 | func replaceExtension(path: String, ext: String) -> String { 37 | URL(filePath: path).deletingPathExtension().appendingPathExtension(ext).lastPathComponent 38 | } 39 | 40 | @main 41 | struct Anodize: AsyncParsableCommand { 42 | 43 | @Argument(help: "A list of input file paths.") 44 | var inputFiles: [String] 45 | 46 | @Option(name: [.short, .customLong("output")], help: "File to write generated wrapper code.") 47 | var outputFile: String 48 | 49 | mutating func run() async throws { 50 | // print("args: \(args)") 51 | 52 | let mgr = FileManager.default 53 | guard let device = MTLCreateSystemDefaultDevice() else { 54 | print("⚠️ failed to create metal device") 55 | return 56 | } 57 | 58 | let library: MTLLibrary 59 | 60 | if let file = inputFiles.first, file.hasSuffix(".metallib") { 61 | library = try! device.makeLibrary(URL: URL(filePath: file)) 62 | } else { 63 | 64 | await runCommandsInParallel(commands: inputFiles.map { file in 65 | ["xcrun", "-sdk", "macosx", "metal", "-c", file, "-o", replaceExtension(path: file, ext: "air")] 66 | }) 67 | 68 | let airFiles = inputFiles.map { replaceExtension(path: $0, ext: "air") } 69 | 70 | // print("airfiles: \(airFiles)") 71 | 72 | if shell(["xcrun", "-sdk", "macosx", "metallib", "-o", "anodize.metallib"] + airFiles) != 0 { 73 | print("⚠️ failed to link metal files") 74 | return 75 | } 76 | 77 | for file in airFiles { try! mgr.removeItem(atPath: file) } 78 | 79 | library = try! device.makeLibrary(URL: URL(filePath: "anodize.metallib")) 80 | } 81 | 82 | var contents = "" 83 | contents += "// This file is generated by Anodize. DO NOT EDIT.\n" 84 | contents += "import Metal\n" 85 | contents += "import AnodizeUtil\n" 86 | contents += "import simd\n" 87 | 88 | var count = 0 89 | for name in library.functionNames.sorted() { 90 | let function = library.makeFunction(name: name) 91 | 92 | if let function, function.functionType == .kernel { 93 | // print("found kernel function: \(name)") 94 | 95 | let reflection = computePipelineReflection(function: function) 96 | 97 | contents += reflection.kernelWrapper(name: name, functionName: name) 98 | count += 1 99 | } 100 | } 101 | 102 | print("🤘 anodized \(count) kernel functions") 103 | 104 | try! contents.write(to: URL(filePath: outputFile), atomically: true, encoding: .utf8) 105 | 106 | if mgr.fileExists(atPath: "anodize.metallib") { 107 | try! mgr.removeItem(atPath: "anodize.metallib") 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/Anodize/MTLComputePipelineReflection+codegen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTLComputePipelineReflection+codegen.swift 3 | // Anodize 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | import Metal 9 | 10 | extension MTLComputePipelineReflection { 11 | 12 | func kernelWrapper(name: String, functionName: String) -> String { 13 | 14 | var result = "final class " + name + " : @unchecked Sendable {\n" 15 | 16 | result += " private var pipeline: MTLComputePipelineState\n" 17 | result += " init(device: MTLDevice) { self.pipeline = device.makeComputePipeline(name: \"\(functionName)\") }\n" 18 | result += " func begin(_ buf: MTLCommandBuffer) throws -> BindingWrapper {\n" 19 | result += " guard let enc = buf.makeComputeCommandEncoder() else { throw AnodizeError.metalError(\"couldn't create MTLComputeCommandEncoder\") }\n" 20 | result += " enc.label = \"\(functionName)\"\n" 21 | result += " enc.setComputePipelineState(pipeline)\n" 22 | result += " return BindingWrapper(enc: enc)\n" 23 | result += " }\n" 24 | result += " struct BindingWrapper {\n" 25 | result += " let enc: MTLComputeCommandEncoder\n" 26 | 27 | for binding in bindings { 28 | 29 | switch binding.type { 30 | case .buffer: 31 | result += " func \(binding.name)(buffer: MTLBuffer, offset: Int = 0) -> Self {\n" 32 | result += " enc.setBuffer(buffer, offset: offset, index: \(binding.index))\n" 33 | result += " return self\n" 34 | result += " }\n" 35 | 36 | let bufferBinding = binding as! MTLBufferBinding 37 | 38 | if binding.access == .readOnly { 39 | if let swiftName = bufferBinding.bufferDataType.swiftName { 40 | result += " func \(binding.name)(bytes value: \(swiftName)) -> Self {\n" 41 | result += " enc.setBytes(value, index: \(binding.index))\n" 42 | result += " return self\n" 43 | result += " }\n" 44 | } else { 45 | result += " func \(binding.name)(bytes value: T) -> Self {\n" 46 | result += " assert(MemoryLayout.size == \(bufferBinding.bufferDataSize))\n" 47 | result += " enc.setBytes(value, index: \(binding.index))\n" 48 | result += " return self\n" 49 | result += " }\n" 50 | } 51 | } 52 | 53 | if let swiftName = bufferBinding.bufferDataType.swiftName { 54 | 55 | switch binding.access { 56 | case .readOnly: 57 | result += " func \(binding.name)(_ array: any GPUBufferProvider<\(swiftName)>, offset: Int = 0) -> Self {\n" 58 | result += " enc.setBuffer(array, index: \(binding.index), offset: offset)\n" 59 | result += " return self\n" 60 | result += " }\n" 61 | case .readWrite, .writeOnly: 62 | result += " func \(binding.name)(_ array: any MutableGPUBufferProvider<\(swiftName)>, offset: Int = 0) -> Self {\n" 63 | result += " enc.setBuffer(array, index: \(binding.index), offset: offset)\n" 64 | result += " return self\n" 65 | result += " }\n" 66 | default: 67 | fatalError() 68 | } 69 | 70 | } else { 71 | 72 | switch binding.access { 73 | case .readOnly: 74 | result += " func \(binding.name)(_ array: any GPUBufferProvider, offset: Int = 0) -> Self {\n" 75 | result += " assert(MemoryLayout.size == \(bufferBinding.bufferDataSize))\n" 76 | result += " enc.setBuffer(array, index: \(binding.index), offset: offset)\n" 77 | result += " return self\n" 78 | result += " }\n" 79 | case .readWrite, .writeOnly: 80 | result += " func \(binding.name)(_ array: any MutableGPUBufferProvider, offset: Int = 0) -> Self {\n" 81 | result += " assert(MemoryLayout.size == \(bufferBinding.bufferDataSize))\n" 82 | result += " enc.setBuffer(array, index: \(binding.index), offset: offset)\n" 83 | result += " return self\n" 84 | result += " }\n" 85 | default: 86 | fatalError() 87 | } 88 | } 89 | 90 | case .texture: 91 | result += " func \(binding.name)(_ texture: MTLTexture) -> Self {\n" 92 | result += " enc.setTexture(texture, index: \(binding.index))\n" 93 | result += " return self\n" 94 | result += " }\n" 95 | 96 | case .imageblock: 97 | // Nothing to do for imageblocks, I think. 98 | break 99 | 100 | default: 101 | print("unknown binding type for function \(functionName)") 102 | } 103 | } 104 | 105 | result += " func wait(_ fence: MTLFence) -> Self {\n" 106 | result += " enc.waitForFence(fence)\n" 107 | result += " return self\n" 108 | result += " }\n" 109 | 110 | result += " func update(_ fence: MTLFence) -> Self {\n" 111 | result += " enc.updateFence(fence)\n" 112 | result += " return self\n" 113 | result += " }\n" 114 | 115 | result += " func dispatch(threadgroups: MTLSize, threadsPerThreadgroup: MTLSize) -> Self {\n" 116 | result += " enc.dispatchThreadgroups(threadgroups, threadsPerThreadgroup: threadsPerThreadgroup)\n" 117 | result += " return self\n" 118 | result += " }\n" 119 | 120 | result += " func dispatch(threadgroups: Int, threadsPerThreadgroup: Int) -> Self {\n" 121 | result += " enc.dispatchThreadgroups(MTLSizeMake(threadgroups, 1, 1), threadsPerThreadgroup: MTLSizeMake(threadsPerThreadgroup, 1, 1))\n" 122 | result += " return self\n" 123 | result += " }\n" 124 | result += " func dispatch(threads: Int, threadsPerThreadgroup: Int) -> Self {\n" 125 | result += " enc.dispatchThreadgroups(MTLSizeMake(Int(ceil(Double(threads)/Double(threadsPerThreadgroup))), 1, 1), threadsPerThreadgroup: MTLSizeMake(threadsPerThreadgroup, 1, 1))\n" 126 | result += " return self\n" 127 | result += " }\n" 128 | result += " func end() { enc.endEncoding() } \n" 129 | 130 | result += " }\n" 131 | 132 | result += "}\n" 133 | 134 | return result 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/Anodize/MTLDataType+swiftName.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTLDataType+swiftName.swift 3 | // Anodize 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | import Metal 9 | 10 | extension MTLDataType { 11 | 12 | var swiftName: String? { 13 | switch self { 14 | 15 | case .none: 16 | return nil 17 | case .struct: 18 | return nil 19 | case .array: 20 | return nil 21 | case .float: 22 | return "Float" 23 | case .float2: 24 | return "SIMD2" 25 | case .float3: 26 | return "SIMD3" 27 | case .float4: 28 | return "SIMD4" 29 | case .float2x2: 30 | return "float2x2" 31 | case .float2x3: 32 | return "float2x3" 33 | case .float2x4: 34 | return "float2x4" 35 | case .float3x2: 36 | return "float3x2" 37 | case .float3x3: 38 | return "float3x3" 39 | case .float3x4: 40 | return "float3x4" 41 | case .float4x2: 42 | return "float4x2" 43 | case .float4x3: 44 | return "float4x3" 45 | case .float4x4: 46 | return "float4x4" 47 | case .half: 48 | return "Float16" 49 | case .half2: 50 | return "SIMD2" 51 | case .half3: 52 | return "SIMD3" 53 | case .half4: 54 | return "SIMD4" 55 | case .half2x2: 56 | return "simd_half2x2" 57 | case .half2x3: 58 | return "simd_half2x3" 59 | case .half2x4: 60 | return "simd_half2x4" 61 | case .half3x2: 62 | return "simd_half3x2" 63 | case .half3x3: 64 | return "simd_half3x3" 65 | case .half3x4: 66 | return "simd_half3x4" 67 | case .half4x2: 68 | return "simd_half4x2" 69 | case .half4x3: 70 | return "simd_half4x3" 71 | case .half4x4: 72 | return "simd_half4x4" 73 | case .int: 74 | return "Int32" 75 | case .int2: 76 | return "SIMD2" 77 | case .int3: 78 | return "SIMD3" 79 | case .int4: 80 | return "SIMD4" 81 | case .uint: 82 | return "UInt32" 83 | case .uint2: 84 | return "SIMD2" 85 | case .uint3: 86 | return "SIMD3" 87 | case .uint4: 88 | return "SIMD4" 89 | case .short: 90 | return "Int16" 91 | case .short2: 92 | return "SIMD2" 93 | case .short3: 94 | return "SIMD3" 95 | case .short4: 96 | return "SIMD4" 97 | case .ushort: 98 | return "UInt16" 99 | case .ushort2: 100 | return "SIMD2" 101 | case .ushort3: 102 | return "SIMD3" 103 | case .ushort4: 104 | return "SIMD4" 105 | case .char: 106 | return "Int8" 107 | case .char2: 108 | return "SIMD2" 109 | case .char3: 110 | return "SIMD3" 111 | case .char4: 112 | return "SIMD4" 113 | case .uchar: 114 | return "UInt8" 115 | case .uchar2: 116 | return "SIMD2" 117 | case .uchar3: 118 | return "SIMD3" 119 | case .uchar4: 120 | return "SIMD4" 121 | case .bool: 122 | return "Bool" 123 | case .bool2: 124 | return "SIMD2" 125 | case .bool3: 126 | return "SIMD3" 127 | case .bool4: 128 | return "SIMD4" 129 | default: 130 | return nil 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Sources/AnodizeUtil/AnodizeError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnodizeError.swift 3 | // Anodize 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | import Foundation 9 | import Metal 10 | 11 | public enum AnodizeError: Error, Equatable { 12 | case bufferOOM 13 | case logicError(String) 14 | case metalError(String) 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AnodizeUtil/GPUArray.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Metal 4 | 5 | public class GPUArray: GPUBufferProvider, @unchecked Sendable { 6 | 7 | public var buffer: MTLBuffer 8 | 9 | @usableFromInline 10 | internal var mutablePointer: UnsafeMutableBufferPointer 11 | 12 | public init(data: [Element], label: String? = nil, heap: MTLHeap? = nil) throws { 13 | 14 | // If empty, just create a one element buffer. 15 | let count = data.count > 0 ? data.count : 1 16 | 17 | if let heap { 18 | guard let buffer = data.withUnsafeBytes({ ptr in 19 | heap.makeBuffer(length: count * MemoryLayout.size, options: .storageModeShared) 20 | }) else { 21 | throw AnodizeError.bufferOOM 22 | } 23 | self.buffer = buffer 24 | let raw = UnsafeMutableRawBufferPointer(start: buffer.contents(), count: buffer.length) 25 | self.mutablePointer = raw.bindMemory(to: Element.self) 26 | 27 | for (ix, elem) in data.enumerated() { 28 | self.mutablePointer[ix] = elem 29 | } 30 | 31 | } else { 32 | let device = MTLCreateSystemDefaultDevice()! 33 | 34 | guard let buffer = data.withUnsafeBytes({ ptr in 35 | device.makeBuffer(bytes: ptr.baseAddress!, length: count * MemoryLayout.size, options: .storageModeShared) 36 | }) else { 37 | throw AnodizeError.bufferOOM 38 | } 39 | self.buffer = buffer 40 | let raw = UnsafeMutableRawBufferPointer(start: buffer.contents(), count: buffer.length) 41 | self.mutablePointer = raw.bindMemory(to: Element.self) 42 | } 43 | 44 | self.buffer.label = label 45 | } 46 | 47 | internal init(length: Int, options: MTLResourceOptions, label: String? = nil, heap: MTLHeap? = nil) throws { 48 | if length == 0 { 49 | throw AnodizeError.logicError("Empty GPUArray") 50 | } 51 | 52 | if let heap { 53 | guard let buffer = heap.makeBuffer(length: length * MemoryLayout.size, options: options) else { 54 | throw AnodizeError.bufferOOM 55 | } 56 | self.buffer = buffer 57 | } else { 58 | let device = MTLCreateSystemDefaultDevice()! 59 | 60 | guard let buffer = device.makeBuffer(length: length * MemoryLayout.size, options: options) else { 61 | throw AnodizeError.bufferOOM 62 | } 63 | self.buffer = buffer 64 | } 65 | 66 | let raw = UnsafeMutableRawBufferPointer(start: buffer.contents(), count: buffer.length) 67 | self.mutablePointer = raw.bindMemory(to: Element.self) 68 | self.buffer.label = label 69 | } 70 | 71 | public init(buffer: MTLBuffer) { 72 | self.buffer = buffer 73 | let raw = UnsafeMutableRawBufferPointer(start: buffer.contents(), count: buffer.length) 74 | self.mutablePointer = raw.bindMemory(to: Element.self) 75 | } 76 | 77 | @inline(__always) @inlinable 78 | public subscript(index: Int) -> Element { 79 | assert(index < mutablePointer.count) 80 | return self.mutablePointer[index] 81 | } 82 | 83 | @inline(__always) @inlinable 84 | public subscript(index: UInt32) -> Element { 85 | assert(index < mutablePointer.count) 86 | return self.mutablePointer[Int(index)] 87 | } 88 | 89 | public var capacity: Int { 90 | mutablePointer.count 91 | } 92 | 93 | public var data: Data { 94 | Data(bytes: buffer.contents(), count: buffer.length) 95 | } 96 | 97 | public var array: [Element] { 98 | .init(mutablePointer) 99 | } 100 | 101 | public var offset: Int { 0 } 102 | 103 | public var pointer: UnsafePointer { 104 | .init(mutablePointer.baseAddress!) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/AnodizeUtil/GPUBufferProvider.swift: -------------------------------------------------------------------------------- 1 | 2 | import Metal 3 | 4 | public protocol GPUBufferProvider { 5 | 6 | associatedtype Element 7 | 8 | var buffer: MTLBuffer { get } 9 | var offset: Int { get } 10 | var capacity: Int { get } 11 | } 12 | 13 | public protocol MutableGPUBufferProvider: GPUBufferProvider { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AnodizeUtil/MTLComputeCommandEncoder+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTLComputeCommandEncoder+Ext.swift 3 | // Anodize 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | import Foundation 9 | import Metal 10 | 11 | public extension MTLComputeCommandEncoder { 12 | func setBytes(_ value: T, index: Int) { 13 | var copy = value 14 | withUnsafeMutableBytes(of: ©) { ptr in 15 | setBytes(ptr.baseAddress!, length: MemoryLayout.size, index: index) 16 | } 17 | } 18 | 19 | func setBytes(_ value: T, index: Int32) { 20 | var copy = value 21 | withUnsafeMutableBytes(of: ©) { ptr in 22 | setBytes(ptr.baseAddress!, length: MemoryLayout.size, index: Int(index)) 23 | } 24 | } 25 | 26 | func setBytes >(_ value: T, index: Index) { 27 | var copy = value 28 | withUnsafeMutableBytes(of: ©) { ptr in 29 | setBytes(ptr.baseAddress!, length: MemoryLayout.size, index: Int(index.rawValue)) 30 | } 31 | } 32 | 33 | func setBuffer(_ buffer: MTLBuffer, index: Int32) { 34 | setBuffer(buffer, offset: 0, index: Int(index)) 35 | } 36 | 37 | func setBuffer(_ buffer: some GPUBufferProvider, index: Int32, offset: Int = 0) { 38 | setBuffer(buffer.buffer, offset: buffer.offset + offset * MemoryLayout.stride, index: Int(index)) 39 | } 40 | 41 | func setBuffer >(_ buffer: some GPUBufferProvider, 42 | index: Index, 43 | offset: Int = 0) { 44 | setBuffer(buffer.buffer, 45 | offset: buffer.offset + offset * MemoryLayout.stride, 46 | index: Int(index.rawValue)) 47 | } 48 | 49 | func setTexture(_ texture: MTLTexture?, index: Int32) { 50 | setTexture(texture, index: Int(index)) 51 | } 52 | 53 | func setTexture >(_ texture: MTLTexture?, index: Index) { 54 | setTexture(texture, index: Int(index.rawValue)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/AnodizeUtil/MTLDevice+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTLDevice+Ext.swift 3 | // Anodize 4 | // 5 | // Created by Taylor Holliday on 2/21/25. 6 | // 7 | 8 | import Metal 9 | 10 | public extension MTLDevice { 11 | func makeBuffer(_ array: [T]) -> MTLBuffer? { 12 | array.withUnsafeBytes { ptr in 13 | makeBuffer(bytes: ptr.baseAddress!, length: ptr.count) 14 | } 15 | } 16 | 17 | func makeComputePipeline(name: String) -> MTLComputePipelineState { 18 | let lib = makeDefaultLibrary()! 19 | 20 | let function = lib.makeFunction(name: name)! 21 | let computeDesc = MTLComputePipelineDescriptor() 22 | computeDesc.computeFunction = function 23 | computeDesc.label = name 24 | return try! makeComputePipelineState(descriptor: computeDesc, options: []).0 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/AnodizeUtil/MutableGPUArray.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | import Metal 4 | 5 | public final class MutableGPUArray: GPUArray, MutableGPUBufferProvider, @unchecked Sendable { 6 | 7 | public override init( 8 | length: Int, 9 | options: MTLResourceOptions = [.storageModeShared], 10 | label: String? = nil, 11 | heap: MTLHeap? = nil 12 | ) throws { 13 | try super.init(length: length, options: options, heap: heap) 14 | } 15 | 16 | public override init(data: [Element], label: String? = nil, heap: MTLHeap? = nil) throws { 17 | try super.init(data: data, label: label, heap: heap) 18 | } 19 | 20 | @inline(__always) @inlinable 21 | public override subscript(index: Int) -> Element { 22 | get { 23 | assert(index < mutablePointer.count) 24 | return self.mutablePointer[index] 25 | } 26 | set { 27 | assert(index < mutablePointer.count) 28 | self.mutablePointer[index] = newValue 29 | } 30 | } 31 | 32 | public var baseAddress: UnsafeMutablePointer { 33 | mutablePointer.baseAddress! 34 | } 35 | 36 | /// Ensure we have enough capacity. 37 | public func reserveCapacity(_ newCapacity: Int) -> Bool { 38 | 39 | if newCapacity > capacity { 40 | var sz = capacity 41 | while sz < newCapacity { 42 | sz *= 2 43 | } 44 | guard let newBuffer = buffer.device.makeBuffer(length: sz * MemoryLayout.size) else { 45 | return false 46 | } 47 | let raw = UnsafeMutableRawBufferPointer(start: newBuffer.contents(), count: newBuffer.length) 48 | let newPointer = raw.bindMemory(to: Element.self) 49 | 50 | for i in 0..