├── Screenshot.png ├── Wireframe_Screenshot.png ├── ios_metal_bezier_renderer.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── LICENSE ├── metal_bezier ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── ViewController.swift ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── AppDelegate.swift ├── bezier_shaders.metal ├── MetalBezierView.swift └── PageAlignedArray.swift └── README.md /Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/ios_metal_bezier_renderer/master/Screenshot.png -------------------------------------------------------------------------------- /Wireframe_Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijn/ios_metal_bezier_renderer/master/Wireframe_Screenshot.png -------------------------------------------------------------------------------- /ios_metal_bezier_renderer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 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 | -------------------------------------------------------------------------------- /metal_bezier/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /metal_bezier/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /metal_bezier/ViewController.swift: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2016 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 | import UIKit 23 | import MetalKit 24 | 25 | class ViewController: UIViewController { 26 | 27 | @IBOutlet var metalView : MTKView? 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | // Do any additional setup after loading the view, typically from a nib. 32 | } 33 | 34 | override func didReceiveMemoryWarning() { 35 | super.didReceiveMemoryWarning() 36 | // Dispose of any resources that can be recreated. 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /metal_bezier/Base.lproj/LaunchScreen.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 | -------------------------------------------------------------------------------- /metal_bezier/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2016 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 | 23 | import UIKit 24 | 25 | @UIApplicationMain 26 | class AppDelegate: UIResponder, UIApplicationDelegate { 27 | 28 | var window: UIWindow? 29 | 30 | 31 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 32 | // Override point for customization after application launch. 33 | return true 34 | } 35 | 36 | func applicationWillResignActive(_ application: UIApplication) { 37 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 38 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 39 | } 40 | 41 | func applicationDidEnterBackground(_ application: UIApplication) { 42 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 43 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 44 | } 45 | 46 | func applicationWillEnterForeground(_ application: UIApplication) { 47 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 48 | } 49 | 50 | func applicationDidBecomeActive(_ application: UIApplication) { 51 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 52 | } 53 | 54 | func applicationWillTerminate(_ application: UIApplication) { 55 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 56 | } 57 | 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /metal_bezier/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # High-performance Bezier Curves in the GPU with Metal 2 | Lately I've been working on a project to achieve high-performance 2D graphics on iOS using Metal. It's interesting in that tasks that used to be supported by the fixed hardware in the older graphics processors have, in recent years, become a bit more challenging and less of a natural fit for today's modern GPU hardware. 3 | 4 | In this project I spent some time playing with rendering Bezier curves completely in the GPU and measuring the kind of performance that can be achieved. Other OpenGL implementations I was able to find online appeared to be cheating: They were actually calculating the vertices on the CPU and feeding the calculated data to the GPU, so all the GPU was doing was actually drawing the triangles to the screen. This is surprising given that the GPU appears to be very well suited to perform these kinds of calculations. Here is what the output of my test program looks like: 5 | 6 | ![iPad Screenshot with thousands of Bezier curves rendered in realtime](Screenshot.png) 7 | 8 | In this implementation the Bezier curve is fully calculated in the vertex shader, which achieves very high performance. The test program renders several thousand Bezier curves animating at 60FPS with perfect smoothness (and with 4x MSAA). 9 | 10 | ## Visualizing the triangles 11 | As with any GPU-based rendering, we use triangles to actually render our curves, which significantly complicates the vertex shader and adds cost. We could use simple lines and just have each vertex shader calculate a single point, but then all we'd get is a fixed-width curve. Instead, we compute triangles out of the curve and render those, which allows us to choose our thickness. Depending on the length of the curve and on the screen resolution, it appears that a couple of hundred triangles tend to be sufficient to achieve a nice smooth curve on an iPad's screen. 12 | 13 | Here is a wireframe image showing a single curve, to illustrate how the triangles are used to express the curve: 14 | 15 | ![Visualizing the triangles](Wireframe_Screenshot.png) 16 | 17 | ## A note about performance 18 | While the code does perform well enough for most application, the performance is actually a bit unimpressive. For example, in terms of triangles/sec, the iPad Pro appears to be achieving around 2000 curves consisting of 200 triangles each at around 40FPS (around 25ms/frame). That equates a roughly 16M triangles/sec fillrate, which is **significantly** below 19 | the expected performance for that device. 20 | 21 | It is possible that the vertex shader computational load is the bottleneck, but Instruments and the GPU profiler inside Xcode are telling a different story, and are indicating that most of the time is being spent in the fragment shader (which in this implementation should be doing absolutely nothing). 22 | 23 | Another interesting data point is the fact that while increasing the number of curves dramatically increases the per-frame GPU render time, merely increasing the number of triangles *per curve* has a much smaller impact on overall performance. 24 | 25 | For example, on the same iPad Pro, doubling the number of triangles-per-curve from 200 to 400 only increases the per-frame time to around 31ms. That's a 24% increase after we basically doubled the GPU's workload! 26 | 27 | By way of comparison, if we double the number of curves instead (keeping the per-curve triangle count at 200), performance drops to around 47ms per frame. 28 | -------------------------------------------------------------------------------- /metal_bezier/bezier_shaders.metal: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2016 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 | 23 | 24 | #include 25 | #include 26 | 27 | 28 | using namespace metal; 29 | 30 | struct GlobalParameters 31 | { 32 | uint elementsPerInstance; 33 | }; 34 | 35 | // BezierParameters represent a per-curve buffer specifying curve parameters. Note that 36 | // even though the vertex shader is obviously called per-vertex, it actually uses the same 37 | // BezierParameters instance (identified through the instance_id) for all vertexes in a given 38 | // curve. 39 | struct BezierParameters 40 | { 41 | float2 a; 42 | float2 b; 43 | float2 p1; 44 | float2 p2; 45 | float lineThickness; 46 | float4 color; 47 | // The following vectors are used internally on the CPU-side. Since we're sharing memory 48 | // I do not believe this matters much performance-wise, and storing it in this struct 49 | // makes the code simpler on the CPU side. Though it is a recipe for disaster if someone 50 | // changes things without updating this. 51 | float2 unused[4]; 52 | float unused2; 53 | }; 54 | 55 | struct VertexOut { 56 | float4 pos[[position]]; 57 | float4 color; 58 | }; 59 | 60 | vertex VertexOut bezier_vertex(constant BezierParameters *allParams[[buffer(0)]], 61 | constant GlobalParameters& globalParams[[buffer(1)]], 62 | uint vertexId [[vertex_id]], 63 | uint instanceId [[instance_id]]) 64 | { 65 | // TO DO: Is there no way to ask Metal to give us vertexes per instances? 66 | float t = (float) vertexId / globalParams.elementsPerInstance; 67 | 68 | BezierParameters params = allParams[instanceId]; 69 | 70 | // This is a little trick to avoid conditional code. We need to determine which side of the 71 | // triangle we are processing, so as to calculate the correct "side" of the curve, so we just 72 | // check for odd vs. even vertexId values to determine that: 73 | float lineWidth = (1 - (((float) (vertexId % 2)) * 2.0)) * params.lineThickness; 74 | 75 | float2 a = params.a; 76 | float2 b = params.b; 77 | 78 | // We premultiply several values though I doubt it actually does anything performance-wise: 79 | float2 p1 = params.p1 * 3.0; 80 | float2 p2 = params.p2 * 3.0; 81 | 82 | float nt = 1.0f - t; 83 | 84 | float nt_2 = nt * nt; 85 | float nt_3 = nt_2 * nt; 86 | 87 | float t_2 = t * t; 88 | float t_3 = t_2 * t; 89 | 90 | // Calculate a single point in this Bezier curve: 91 | float2 point = a * nt_3 + p1 * nt_2 * t + p2 * nt * t_2 + b * t_3; 92 | 93 | // Calculate the tangent so we can produce a triangle (to achieve a line width greater than 1): 94 | float2 tangent = -3.0 * a * nt_2 + p1 * (1.0 - 4.0 * t + 3.0 * t_2) + p2 * (2.0 * t - 3.0 * t_2) + 3 * b * t_2; 95 | 96 | tangent = normalize(float2(-tangent.y, tangent.x)); 97 | 98 | VertexOut vo; 99 | 100 | // Combine the point with the tangent and lineWidth to achieve a properly oriented 101 | // triangle for this point in the curve: 102 | vo.pos.xy = point + (tangent * (lineWidth / 2.0f)); 103 | vo.pos.zw = float2(0, 1); 104 | vo.color = params.color; 105 | 106 | return vo; 107 | } 108 | 109 | fragment half4 bezier_fragment(VertexOut params[[stage_in]]) 110 | { 111 | return half4(params.color); 112 | } 113 | 114 | -------------------------------------------------------------------------------- /metal_bezier/MetalBezierView.swift: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2016 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 | 23 | 24 | import UIKit 25 | import MetalKit 26 | 27 | struct BezierParameters 28 | { 29 | // Set coordinateRange to <1.0 to restrict the drawing area 30 | static let coordinateRange : Float = 1.0 31 | 32 | var a : vector_float2 = vector_float2() 33 | var b : vector_float2 = vector_float2() 34 | var p1 : vector_float2 = vector_float2() 35 | var p2 : vector_float2 = vector_float2() 36 | 37 | // This will define line width for all curves: 38 | var lineWidth : Float = 0.10 39 | 40 | var color : vector_float4 = vector_float4() 41 | 42 | private var aMotionVec : vector_float2 = vector_float2() 43 | private var bMotionVec : vector_float2 = vector_float2() 44 | private var p1MotionVec : vector_float2 = vector_float2() 45 | private var p2MotionVec : vector_float2 = vector_float2() 46 | 47 | // This sets the animation speed for the curves: 48 | private var animationSpeed : Float = 0.01 49 | 50 | private var unused : Float = 0.01 51 | 52 | 53 | private func makeRandSpeed() -> Float { 54 | return ((Float(arc4random_uniform(2000000)) / 1000000.0) - 1.0) * animationSpeed 55 | } 56 | 57 | private func makeRandCoord() -> Float { 58 | return (Float(arc4random_uniform(1000000)) / 500000.0 - 1.0) * BezierParameters.coordinateRange 59 | } 60 | 61 | init() { 62 | // Set a random color for this curve: 63 | color = vector_float4(x: Float(arc4random_uniform(1000)) / 1000.0, 64 | y: Float(arc4random_uniform(1000)) / 1000.0, 65 | z: Float(arc4random_uniform(1000)) / 1000.0, 66 | w: 1.0) 67 | 68 | // Start this curve out at a random position and shape: 69 | a.x = makeRandCoord() 70 | a.y = makeRandCoord() 71 | 72 | b.x = makeRandCoord() 73 | b.y = makeRandCoord() 74 | 75 | p1.x = makeRandCoord() 76 | p1.y = makeRandCoord() 77 | 78 | p2.x = makeRandCoord() 79 | p2.y = makeRandCoord() 80 | 81 | // Initialize random motion vectors: 82 | aMotionVec.x = makeRandSpeed() 83 | aMotionVec.y = makeRandSpeed() 84 | 85 | bMotionVec.x = makeRandSpeed() 86 | bMotionVec.y = makeRandSpeed() 87 | 88 | p1MotionVec.x = makeRandSpeed() 89 | p1MotionVec.y = makeRandSpeed() 90 | 91 | p2MotionVec.x = makeRandSpeed() 92 | p2MotionVec.y = makeRandSpeed() 93 | } 94 | 95 | private func animateVector(vector : vector_float2, motionVec : inout vector_float2) -> vector_float2 { 96 | if vector.x >= BezierParameters.coordinateRange || vector.x <= -BezierParameters.coordinateRange { 97 | motionVec.x = -motionVec.x 98 | } 99 | if vector.y >= BezierParameters.coordinateRange || vector.y <= -BezierParameters.coordinateRange { 100 | motionVec.y = -motionVec.y 101 | } 102 | 103 | return vector_float2(x: vector.x + motionVec.x, y: vector.y + motionVec.y) 104 | } 105 | 106 | mutating func animate() { 107 | a = animateVector(vector: a, motionVec: &aMotionVec) 108 | b = animateVector(vector: b, motionVec: &bMotionVec) 109 | p1 = animateVector(vector: p1, motionVec: &p1MotionVec) 110 | p2 = animateVector(vector: p2, motionVec: &p2MotionVec) 111 | } 112 | 113 | init(a : vector_float2, b: vector_float2, p1 : vector_float2, p2 : vector_float2) { 114 | self.a = a 115 | self.b = b 116 | self.p1 = p1 117 | self.p2 = p2 118 | } 119 | } 120 | 121 | struct GlobalParameters { 122 | var elementsPerInstance : UInt 123 | } 124 | 125 | class MetalBezierView: MTKView { 126 | private var commandQueue: MTLCommandQueue! = nil 127 | private var library: MTLLibrary! = nil 128 | private var pipelineDescriptor = MTLRenderPipelineDescriptor() 129 | private var pipelineState : MTLRenderPipelineState! = nil 130 | private var vertexBuffer : MTLBuffer! = nil 131 | 132 | var indices : [UInt16] = [UInt16]() 133 | var indicesBuffer : MTLBuffer? 134 | 135 | var globalParamBuffer : MTLBuffer? 136 | 137 | // This is where we store all curve parameters (including their current positions during 138 | // animation). We use the PageAlignedContiguousArray to directly store and manipulate 139 | // them in shared memory. 140 | var params : PageAlignedContiguousArray = PageAlignedContiguousArray(repeating: BezierParameters(), count: 2000) 141 | var paramBuffer : MTLBuffer? 142 | 143 | override init(frame frameRect: CGRect, device: MTLDevice?) 144 | { 145 | super.init(frame: frameRect, device: device) 146 | configureWithDevice(device!) 147 | } 148 | 149 | required init(coder: NSCoder) 150 | { 151 | super.init(coder: coder) 152 | configureWithDevice(MTLCreateSystemDefaultDevice()!) 153 | } 154 | 155 | private func configureWithDevice(_ device : MTLDevice) { 156 | self.clearColor = MTLClearColor.init(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) 157 | self.autoresizingMask = [.flexibleWidth, .flexibleHeight] 158 | self.framebufferOnly = true 159 | self.colorPixelFormat = .bgra8Unorm 160 | 161 | // Run with 4x MSAA: 162 | self.sampleCount = 4 163 | 164 | self.preferredFramesPerSecond = 60 165 | 166 | self.device = device 167 | } 168 | 169 | override var device: MTLDevice! { 170 | didSet { 171 | super.device = device 172 | commandQueue = (self.device?.makeCommandQueue())! 173 | 174 | library = device?.newDefaultLibrary() 175 | pipelineDescriptor.vertexFunction = library?.makeFunction(name: "bezier_vertex") 176 | pipelineDescriptor.fragmentFunction = library?.makeFunction(name: "bezier_fragment") 177 | pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm 178 | 179 | // Run with 4x MSAA: 180 | pipelineDescriptor.sampleCount = 4 181 | 182 | do { 183 | try pipelineState = device?.makeRenderPipelineState(descriptor: pipelineDescriptor) 184 | } 185 | catch { 186 | 187 | } 188 | for (index, _) in params.enumerated() { 189 | params[index] = BezierParameters() 190 | } 191 | 192 | paramBuffer = device.makeBufferWithPageAlignedArray(params) 193 | 194 | var currentIndex : UInt16 = 0 195 | 196 | // Set how many "elements" are to be used for each curve. Normally we would 197 | // calculate this per curve, but since we're using the indexed primitives 198 | // approach, we need a fixed number of vertices per curve. Note that this is 199 | // the number of triangles, not vertexes: 200 | var globalParams = GlobalParameters(elementsPerInstance: 200) 201 | globalParamBuffer = (self.device?.makeBuffer(bytes: &globalParams, 202 | length: MemoryLayout.size, 203 | options: .storageModeShared)) 204 | 205 | repeat { 206 | indices.append(currentIndex) 207 | indices.append(currentIndex + 1) 208 | indices.append(currentIndex + 2) 209 | currentIndex += 1 210 | } while indices.count < Int(globalParams.elementsPerInstance * 3) 211 | 212 | let indicesDataSize = MemoryLayout.size * indices.count 213 | indicesBuffer = (self.device?.makeBuffer(bytes: indices, 214 | length: indicesDataSize, 215 | options: .storageModeShared)) 216 | } 217 | } 218 | 219 | override func draw(_ rect: CGRect) { 220 | 221 | // Animate all of our curves. No need to reload this into the GPU for each frame. 222 | // The params are in shared memory so our modifications will be automatically visible 223 | // to the vertex shader. 224 | for (index, _) in params.enumerated() { 225 | params[index].animate() 226 | } 227 | 228 | let commandBuffer = commandQueue!.makeCommandBuffer() 229 | 230 | let renderPassDescriptor = self.currentRenderPassDescriptor 231 | 232 | if renderPassDescriptor == nil { 233 | return 234 | } 235 | 236 | 237 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor!) 238 | 239 | renderEncoder.setRenderPipelineState(pipelineState) 240 | 241 | renderEncoder.setVertexBuffer(paramBuffer, offset: 0, at: 0) 242 | renderEncoder.setVertexBuffer(globalParamBuffer, offset: 0, at: 1) 243 | 244 | // Enable this to see the actual triangles instead of a solid curve: 245 | //renderEncoder.setTriangleFillMode(.lines) 246 | 247 | renderEncoder.drawIndexedPrimitives(type: .triangle, indexCount: indices.count, indexType: .uint16, indexBuffer: indicesBuffer!, indexBufferOffset: 0, instanceCount: params.count) 248 | 249 | renderEncoder.endEncoding() 250 | 251 | commandBuffer.present(self.currentDrawable!) 252 | commandBuffer.commit() 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /metal_bezier/PageAlignedArray.swift: -------------------------------------------------------------------------------- 1 | //MIT License 2 | // 3 | //Copyright (c) 2016 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 | 23 | import CoreFoundation 24 | import Metal 25 | 26 | class PageAlignedArrayImpl { 27 | var space: Int 28 | var ptr: UnsafeMutablePointer 29 | 30 | static private func alignedAlloc(count: Int) -> UnsafeMutablePointer { 31 | var newAddr:UnsafeMutableRawPointer? 32 | let alignment : Int = Int(getpagesize()) 33 | var size : Int 34 | 35 | if count == 0 { 36 | size = MemoryLayout.stride / Int(getpagesize()) 37 | 38 | if MemoryLayout.stride % Int(getpagesize()) != 0 { 39 | size += Int(getpagesize()) 40 | } 41 | } else { 42 | size = Int(count * MemoryLayout.stride) 43 | } 44 | 45 | posix_memalign(&newAddr, alignment, size) 46 | 47 | return newAddr!.assumingMemoryBound(to: T.self) 48 | } 49 | 50 | static private func freeAlignedAlloc(addr : UnsafeMutablePointer) { 51 | free(addr) 52 | } 53 | 54 | 55 | init(count: Int = 0, ptr: UnsafeMutablePointer? = nil) { 56 | self.count = count 57 | self.space = Int(getpagesize()) / MemoryLayout.stride 58 | 59 | self.ptr = PageAlignedArrayImpl.alignedAlloc(count: count) 60 | 61 | if ptr != nil { 62 | self.ptr.initialize(from: ptr!, count: count) 63 | } 64 | } 65 | 66 | var count : Int { 67 | didSet { 68 | if space <= count { 69 | let newSpace = count * 2 70 | let newPtr = PageAlignedArrayImpl.alignedAlloc(count: newSpace) 71 | 72 | newPtr.moveInitialize(from: ptr, count: oldValue) 73 | 74 | PageAlignedArrayImpl.freeAlignedAlloc(addr: ptr) 75 | ptr = newPtr 76 | space = newSpace 77 | } 78 | } 79 | } 80 | 81 | func copy() -> PageAlignedArrayImpl { 82 | return PageAlignedArrayImpl(count: count, ptr: ptr) 83 | } 84 | 85 | deinit { 86 | ptr.deinitialize(count: count) 87 | PageAlignedArrayImpl.freeAlignedAlloc(addr: ptr) 88 | } 89 | } 90 | 91 | struct PageAlignedContiguousArray: RangeReplaceableCollection { 92 | private var impl: PageAlignedArrayImpl = PageAlignedArrayImpl(count: 0) 93 | 94 | /// Replaces the specified subrange of elements with the given collection. 95 | /// 96 | /// This method has the effect of removing the specified range of elements 97 | /// from the collection and inserting the new elements at the same location. 98 | /// The number of new elements need not match the number of elements being 99 | /// removed. 100 | /// 101 | /// In this example, three elements in the middle of an array of integers are 102 | /// replaced by the five elements of a `Repeated` instance. 103 | /// 104 | /// var nums = [10, 20, 30, 40, 50] 105 | /// nums.replaceSubrange(1...3, with: repeatElement(1, count: 5)) 106 | /// print(nums) 107 | /// // Prints "[10, 1, 1, 1, 1, 1, 50]" 108 | /// 109 | /// If you pass a zero-length range as the `subrange` parameter, this method 110 | /// inserts the elements of `newElements` at `subrange.startIndex`. Calling 111 | /// the `insert(contentsOf:at:)` method instead is preferred. 112 | /// 113 | /// Likewise, if you pass a zero-length collection as the `newElements` 114 | /// parameter, this method removes the elements in the given subrange 115 | /// without replacement. Calling the `removeSubrange(_:)` method instead is 116 | /// preferred. 117 | /// 118 | /// Calling this method may invalidate any existing indices for use with this 119 | /// collection. 120 | /// 121 | /// - Parameters: 122 | /// - subrange: The subrange of the collection to replace. The bounds of 123 | /// the range must be valid indices of the collection. 124 | /// - newElements: The new elements to add to the collection. 125 | /// 126 | /// - Complexity: O(*m*), where *m* is the combined length of the collection 127 | /// and `newElements`. If the call to `replaceSubrange` simply appends the 128 | /// contents of `newElements` to the collection, the complexity is O(*n*), 129 | /// where *n* is the length of `newElements`. 130 | public mutating func replaceSubrange(_ subrange: Range, with newElements: C) where C : Collection, C.Iterator.Element == T { 131 | let newCount = newElements.count as! Int 132 | let oldCount = self.count 133 | let eraseCount = subrange.count 134 | 135 | let growth = newCount - eraseCount 136 | impl.count = oldCount + growth 137 | 138 | let elements = impl.ptr 139 | let oldTailIndex = subrange.upperBound 140 | let oldTailStart = elements + oldTailIndex 141 | let newTailIndex = oldTailIndex + growth 142 | let newTailStart = oldTailStart + growth 143 | let tailCount = oldCount - subrange.upperBound 144 | 145 | if growth > 0 { 146 | // Slide the tail part of the buffer forwards, in reverse order 147 | // so as not to self-clobber. 148 | newTailStart.moveInitialize(from: oldTailStart, count: tailCount) 149 | 150 | // Assign over the original subRange 151 | var i = newElements.startIndex 152 | for j in CountableRange(subrange) { 153 | elements[j] = newElements[i] 154 | newElements.formIndex(after: &i) 155 | } 156 | // Initialize the hole left by sliding the tail forward 157 | for j in oldTailIndex.. shrinkage { // If the tail length exceeds the shrinkage 180 | 181 | // Assign over the rest of the replaced range with the first 182 | // part of the tail. 183 | newTailStart.moveAssign(from: oldTailStart, count: shrinkage) 184 | 185 | // Slide the rest of the tail back 186 | oldTailStart.moveInitialize( 187 | from: oldTailStart + shrinkage, count: tailCount - shrinkage) 188 | } 189 | else { // Tail fits within erased elements 190 | // Assign over the start of the replaced range with the tail 191 | newTailStart.moveAssign(from: oldTailStart, count: tailCount) 192 | 193 | // Destroy elements remaining after the tail in subRange 194 | (newTailStart + tailCount).deinitialize( 195 | count: shrinkage - tailCount) 196 | } 197 | } 198 | } 199 | 200 | /// Returns the position immediately after the given index. 201 | /// 202 | /// - Parameter i: A valid index of the collection. `i` must be less than 203 | /// `endIndex`. 204 | /// - Returns: The index value immediately after `i`. 205 | public func index(after i: Int) -> Int { 206 | return i + 1 207 | } 208 | 209 | var buffer : UnsafeMutablePointer { 210 | return impl.ptr 211 | } 212 | 213 | var bufferLength : Int { 214 | return impl.space * MemoryLayout.stride 215 | } 216 | 217 | 218 | var count: Int { 219 | return impl.count 220 | } 221 | 222 | subscript(index: Int) -> T { 223 | get { 224 | assert (index < count, "Array index out of range") 225 | return impl.ptr[index] 226 | } 227 | mutating set { 228 | assert (index < count, "Array index out of range") 229 | impl.ptr[index] = newValue 230 | } 231 | } 232 | 233 | var description: String { 234 | return String(format: "Aligned buffer: %x", impl.ptr) 235 | } 236 | 237 | typealias Index = Int 238 | 239 | var startIndex: Index { 240 | return 0 241 | } 242 | 243 | var endIndex: Index { 244 | return count 245 | } 246 | 247 | typealias Generator = AnyIterator 248 | 249 | func generate() -> Generator { 250 | var index = 0 251 | return AnyIterator { 252 | if index < self.count { 253 | index += 1 254 | return self[index] 255 | } else { 256 | return nil 257 | } 258 | } 259 | } 260 | } 261 | 262 | extension PageAlignedContiguousArray : ExpressibleByArrayLiteral { 263 | public init(arrayLiteral elements: T...) { 264 | self.init() 265 | for element in elements { 266 | append(element) 267 | } 268 | } 269 | } 270 | 271 | extension MTLDevice { 272 | func makeBufferWithPageAlignedArray(_ array: PageAlignedContiguousArray) -> MTLBuffer? { 273 | let pageSize = UInt(getpagesize()) 274 | let pageSizeBitmask = UInt(getpagesize()) - 1 275 | 276 | var calculatedBufferLength = UInt(array.bufferLength) 277 | if (calculatedBufferLength & pageSizeBitmask) != 0 { 278 | // WARNING: I BELIEVE this is safe to do. Metal wants a fully page aligned buffer length consisting of a 279 | // page aligned pointer and a page-aligned length. If the length is not page aligned, I round it up to the 280 | // next page size here. I figure it would't actually read those extra bytes. Then again why is this 281 | // requirement there in the first place? 282 | calculatedBufferLength &= ~(pageSize - 1) 283 | calculatedBufferLength += pageSize 284 | } 285 | return self.makeBuffer(bytesNoCopy: array.buffer, length: Int(calculatedBufferLength), options: .storageModeShared, deallocator: nil) 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /ios_metal_bezier_renderer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5130CC661DCEEC5200FA29CC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5130CC651DCEEC5200FA29CC /* AppDelegate.swift */; }; 11 | 5130CC681DCEEC5200FA29CC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5130CC671DCEEC5200FA29CC /* ViewController.swift */; }; 12 | 5130CC6B1DCEEC5200FA29CC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5130CC691DCEEC5200FA29CC /* Main.storyboard */; }; 13 | 5130CC6D1DCEEC5200FA29CC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5130CC6C1DCEEC5200FA29CC /* Assets.xcassets */; }; 14 | 5130CC701DCEEC5200FA29CC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5130CC6E1DCEEC5200FA29CC /* LaunchScreen.storyboard */; }; 15 | 5130CC941DCEEC8400FA29CC /* MetalBezierView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5130CC931DCEEC8400FA29CC /* MetalBezierView.swift */; }; 16 | 5130CC961DCEED4F00FA29CC /* bezier_shaders.metal in Sources */ = {isa = PBXBuildFile; fileRef = 5130CC951DCEED4F00FA29CC /* bezier_shaders.metal */; }; 17 | 5130CC981DD10BC000FA29CC /* PageAlignedArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5130CC971DD10BC000FA29CC /* PageAlignedArray.swift */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXContainerItemProxy section */ 21 | 5130CC771DCEEC5200FA29CC /* PBXContainerItemProxy */ = { 22 | isa = PBXContainerItemProxy; 23 | containerPortal = 5130CC5A1DCEEC5200FA29CC /* Project object */; 24 | proxyType = 1; 25 | remoteGlobalIDString = 5130CC611DCEEC5200FA29CC; 26 | remoteInfo = metal_bezier; 27 | }; 28 | 5130CC821DCEEC5200FA29CC /* PBXContainerItemProxy */ = { 29 | isa = PBXContainerItemProxy; 30 | containerPortal = 5130CC5A1DCEEC5200FA29CC /* Project object */; 31 | proxyType = 1; 32 | remoteGlobalIDString = 5130CC611DCEEC5200FA29CC; 33 | remoteInfo = metal_bezier; 34 | }; 35 | /* End PBXContainerItemProxy section */ 36 | 37 | /* Begin PBXFileReference section */ 38 | 5130CC621DCEEC5200FA29CC /* ios_metal_bezier_renderer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ios_metal_bezier_renderer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 5130CC651DCEEC5200FA29CC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 40 | 5130CC671DCEEC5200FA29CC /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 41 | 5130CC6A1DCEEC5200FA29CC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 5130CC6C1DCEEC5200FA29CC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 5130CC6F1DCEEC5200FA29CC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 5130CC711DCEEC5200FA29CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | 5130CC761DCEEC5200FA29CC /* ios_metal_bezier_rendererTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ios_metal_bezier_rendererTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 5130CC811DCEEC5200FA29CC /* ios_metal_bezier_rendererUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ios_metal_bezier_rendererUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 5130CC931DCEEC8400FA29CC /* MetalBezierView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetalBezierView.swift; sourceTree = ""; }; 48 | 5130CC951DCEED4F00FA29CC /* bezier_shaders.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = bezier_shaders.metal; sourceTree = ""; }; 49 | 5130CC971DD10BC000FA29CC /* PageAlignedArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageAlignedArray.swift; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 5130CC5F1DCEEC5200FA29CC /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | 5130CC731DCEEC5200FA29CC /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | 5130CC7E1DCEEC5200FA29CC /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | 5130CC591DCEEC5200FA29CC = { 78 | isa = PBXGroup; 79 | children = ( 80 | 5130CC641DCEEC5200FA29CC /* ios_metal_bezier_renderer */, 81 | 5130CC631DCEEC5200FA29CC /* Products */, 82 | ); 83 | sourceTree = ""; 84 | }; 85 | 5130CC631DCEEC5200FA29CC /* Products */ = { 86 | isa = PBXGroup; 87 | children = ( 88 | 5130CC621DCEEC5200FA29CC /* ios_metal_bezier_renderer.app */, 89 | 5130CC761DCEEC5200FA29CC /* ios_metal_bezier_rendererTests.xctest */, 90 | 5130CC811DCEEC5200FA29CC /* ios_metal_bezier_rendererUITests.xctest */, 91 | ); 92 | name = Products; 93 | sourceTree = ""; 94 | }; 95 | 5130CC641DCEEC5200FA29CC /* ios_metal_bezier_renderer */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | 5130CC931DCEEC8400FA29CC /* MetalBezierView.swift */, 99 | 5130CC951DCEED4F00FA29CC /* bezier_shaders.metal */, 100 | 5130CC971DD10BC000FA29CC /* PageAlignedArray.swift */, 101 | 5130CC651DCEEC5200FA29CC /* AppDelegate.swift */, 102 | 5130CC671DCEEC5200FA29CC /* ViewController.swift */, 103 | 5130CC691DCEEC5200FA29CC /* Main.storyboard */, 104 | 5130CC6C1DCEEC5200FA29CC /* Assets.xcassets */, 105 | 5130CC6E1DCEEC5200FA29CC /* LaunchScreen.storyboard */, 106 | 5130CC711DCEEC5200FA29CC /* Info.plist */, 107 | ); 108 | name = ios_metal_bezier_renderer; 109 | path = metal_bezier; 110 | sourceTree = ""; 111 | }; 112 | /* End PBXGroup section */ 113 | 114 | /* Begin PBXNativeTarget section */ 115 | 5130CC611DCEEC5200FA29CC /* ios_metal_bezier_renderer */ = { 116 | isa = PBXNativeTarget; 117 | buildConfigurationList = 5130CC8A1DCEEC5300FA29CC /* Build configuration list for PBXNativeTarget "ios_metal_bezier_renderer" */; 118 | buildPhases = ( 119 | 5130CC5E1DCEEC5200FA29CC /* Sources */, 120 | 5130CC5F1DCEEC5200FA29CC /* Frameworks */, 121 | 5130CC601DCEEC5200FA29CC /* Resources */, 122 | ); 123 | buildRules = ( 124 | ); 125 | dependencies = ( 126 | ); 127 | name = ios_metal_bezier_renderer; 128 | productName = metal_bezier; 129 | productReference = 5130CC621DCEEC5200FA29CC /* ios_metal_bezier_renderer.app */; 130 | productType = "com.apple.product-type.application"; 131 | }; 132 | 5130CC751DCEEC5200FA29CC /* ios_metal_bezier_rendererTests */ = { 133 | isa = PBXNativeTarget; 134 | buildConfigurationList = 5130CC8D1DCEEC5300FA29CC /* Build configuration list for PBXNativeTarget "ios_metal_bezier_rendererTests" */; 135 | buildPhases = ( 136 | 5130CC721DCEEC5200FA29CC /* Sources */, 137 | 5130CC731DCEEC5200FA29CC /* Frameworks */, 138 | 5130CC741DCEEC5200FA29CC /* Resources */, 139 | ); 140 | buildRules = ( 141 | ); 142 | dependencies = ( 143 | 5130CC781DCEEC5200FA29CC /* PBXTargetDependency */, 144 | ); 145 | name = ios_metal_bezier_rendererTests; 146 | productName = metal_bezierTests; 147 | productReference = 5130CC761DCEEC5200FA29CC /* ios_metal_bezier_rendererTests.xctest */; 148 | productType = "com.apple.product-type.bundle.unit-test"; 149 | }; 150 | 5130CC801DCEEC5200FA29CC /* ios_metal_bezier_rendererUITests */ = { 151 | isa = PBXNativeTarget; 152 | buildConfigurationList = 5130CC901DCEEC5300FA29CC /* Build configuration list for PBXNativeTarget "ios_metal_bezier_rendererUITests" */; 153 | buildPhases = ( 154 | 5130CC7D1DCEEC5200FA29CC /* Sources */, 155 | 5130CC7E1DCEEC5200FA29CC /* Frameworks */, 156 | 5130CC7F1DCEEC5200FA29CC /* Resources */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | 5130CC831DCEEC5200FA29CC /* PBXTargetDependency */, 162 | ); 163 | name = ios_metal_bezier_rendererUITests; 164 | productName = metal_bezierUITests; 165 | productReference = 5130CC811DCEEC5200FA29CC /* ios_metal_bezier_rendererUITests.xctest */; 166 | productType = "com.apple.product-type.bundle.ui-testing"; 167 | }; 168 | /* End PBXNativeTarget section */ 169 | 170 | /* Begin PBXProject section */ 171 | 5130CC5A1DCEEC5200FA29CC /* Project object */ = { 172 | isa = PBXProject; 173 | attributes = { 174 | LastSwiftUpdateCheck = 0810; 175 | LastUpgradeCheck = 0810; 176 | ORGANIZATIONNAME = "Eldad Eilam"; 177 | TargetAttributes = { 178 | 5130CC611DCEEC5200FA29CC = { 179 | CreatedOnToolsVersion = 8.1; 180 | DevelopmentTeam = E54FNFV6AU; 181 | ProvisioningStyle = Automatic; 182 | }; 183 | 5130CC751DCEEC5200FA29CC = { 184 | CreatedOnToolsVersion = 8.1; 185 | ProvisioningStyle = Automatic; 186 | TestTargetID = 5130CC611DCEEC5200FA29CC; 187 | }; 188 | 5130CC801DCEEC5200FA29CC = { 189 | CreatedOnToolsVersion = 8.1; 190 | ProvisioningStyle = Automatic; 191 | TestTargetID = 5130CC611DCEEC5200FA29CC; 192 | }; 193 | }; 194 | }; 195 | buildConfigurationList = 5130CC5D1DCEEC5200FA29CC /* Build configuration list for PBXProject "ios_metal_bezier_renderer" */; 196 | compatibilityVersion = "Xcode 3.2"; 197 | developmentRegion = English; 198 | hasScannedForEncodings = 0; 199 | knownRegions = ( 200 | en, 201 | Base, 202 | ); 203 | mainGroup = 5130CC591DCEEC5200FA29CC; 204 | productRefGroup = 5130CC631DCEEC5200FA29CC /* Products */; 205 | projectDirPath = ""; 206 | projectRoot = ""; 207 | targets = ( 208 | 5130CC611DCEEC5200FA29CC /* ios_metal_bezier_renderer */, 209 | 5130CC751DCEEC5200FA29CC /* ios_metal_bezier_rendererTests */, 210 | 5130CC801DCEEC5200FA29CC /* ios_metal_bezier_rendererUITests */, 211 | ); 212 | }; 213 | /* End PBXProject section */ 214 | 215 | /* Begin PBXResourcesBuildPhase section */ 216 | 5130CC601DCEEC5200FA29CC /* Resources */ = { 217 | isa = PBXResourcesBuildPhase; 218 | buildActionMask = 2147483647; 219 | files = ( 220 | 5130CC701DCEEC5200FA29CC /* LaunchScreen.storyboard in Resources */, 221 | 5130CC6D1DCEEC5200FA29CC /* Assets.xcassets in Resources */, 222 | 5130CC6B1DCEEC5200FA29CC /* Main.storyboard in Resources */, 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | }; 226 | 5130CC741DCEEC5200FA29CC /* Resources */ = { 227 | isa = PBXResourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | 5130CC7F1DCEEC5200FA29CC /* Resources */ = { 234 | isa = PBXResourcesBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | ); 238 | runOnlyForDeploymentPostprocessing = 0; 239 | }; 240 | /* End PBXResourcesBuildPhase section */ 241 | 242 | /* Begin PBXSourcesBuildPhase section */ 243 | 5130CC5E1DCEEC5200FA29CC /* Sources */ = { 244 | isa = PBXSourcesBuildPhase; 245 | buildActionMask = 2147483647; 246 | files = ( 247 | 5130CC941DCEEC8400FA29CC /* MetalBezierView.swift in Sources */, 248 | 5130CC681DCEEC5200FA29CC /* ViewController.swift in Sources */, 249 | 5130CC661DCEEC5200FA29CC /* AppDelegate.swift in Sources */, 250 | 5130CC961DCEED4F00FA29CC /* bezier_shaders.metal in Sources */, 251 | 5130CC981DD10BC000FA29CC /* PageAlignedArray.swift in Sources */, 252 | ); 253 | runOnlyForDeploymentPostprocessing = 0; 254 | }; 255 | 5130CC721DCEEC5200FA29CC /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | 5130CC7D1DCEEC5200FA29CC /* Sources */ = { 263 | isa = PBXSourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | ); 267 | runOnlyForDeploymentPostprocessing = 0; 268 | }; 269 | /* End PBXSourcesBuildPhase section */ 270 | 271 | /* Begin PBXTargetDependency section */ 272 | 5130CC781DCEEC5200FA29CC /* PBXTargetDependency */ = { 273 | isa = PBXTargetDependency; 274 | target = 5130CC611DCEEC5200FA29CC /* ios_metal_bezier_renderer */; 275 | targetProxy = 5130CC771DCEEC5200FA29CC /* PBXContainerItemProxy */; 276 | }; 277 | 5130CC831DCEEC5200FA29CC /* PBXTargetDependency */ = { 278 | isa = PBXTargetDependency; 279 | target = 5130CC611DCEEC5200FA29CC /* ios_metal_bezier_renderer */; 280 | targetProxy = 5130CC821DCEEC5200FA29CC /* PBXContainerItemProxy */; 281 | }; 282 | /* End PBXTargetDependency section */ 283 | 284 | /* Begin PBXVariantGroup section */ 285 | 5130CC691DCEEC5200FA29CC /* Main.storyboard */ = { 286 | isa = PBXVariantGroup; 287 | children = ( 288 | 5130CC6A1DCEEC5200FA29CC /* Base */, 289 | ); 290 | name = Main.storyboard; 291 | sourceTree = ""; 292 | }; 293 | 5130CC6E1DCEEC5200FA29CC /* LaunchScreen.storyboard */ = { 294 | isa = PBXVariantGroup; 295 | children = ( 296 | 5130CC6F1DCEEC5200FA29CC /* Base */, 297 | ); 298 | name = LaunchScreen.storyboard; 299 | sourceTree = ""; 300 | }; 301 | /* End PBXVariantGroup section */ 302 | 303 | /* Begin XCBuildConfiguration section */ 304 | 5130CC881DCEEC5300FA29CC /* Debug */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | ALWAYS_SEARCH_USER_PATHS = NO; 308 | CLANG_ANALYZER_NONNULL = YES; 309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 310 | CLANG_CXX_LIBRARY = "libc++"; 311 | CLANG_ENABLE_MODULES = YES; 312 | CLANG_ENABLE_OBJC_ARC = YES; 313 | CLANG_WARN_BOOL_CONVERSION = YES; 314 | CLANG_WARN_CONSTANT_CONVERSION = YES; 315 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 316 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 317 | CLANG_WARN_EMPTY_BODY = YES; 318 | CLANG_WARN_ENUM_CONVERSION = YES; 319 | CLANG_WARN_INFINITE_RECURSION = YES; 320 | CLANG_WARN_INT_CONVERSION = YES; 321 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 322 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 323 | CLANG_WARN_UNREACHABLE_CODE = YES; 324 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 325 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 326 | COPY_PHASE_STRIP = NO; 327 | DEBUG_INFORMATION_FORMAT = dwarf; 328 | ENABLE_STRICT_OBJC_MSGSEND = YES; 329 | ENABLE_TESTABILITY = YES; 330 | GCC_C_LANGUAGE_STANDARD = gnu99; 331 | GCC_DYNAMIC_NO_PIC = NO; 332 | GCC_NO_COMMON_BLOCKS = YES; 333 | GCC_OPTIMIZATION_LEVEL = 0; 334 | GCC_PREPROCESSOR_DEFINITIONS = ( 335 | "DEBUG=1", 336 | "$(inherited)", 337 | ); 338 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 339 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 340 | GCC_WARN_UNDECLARED_SELECTOR = YES; 341 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 342 | GCC_WARN_UNUSED_FUNCTION = YES; 343 | GCC_WARN_UNUSED_VARIABLE = YES; 344 | IPHONEOS_DEPLOYMENT_TARGET = 10.1; 345 | MTL_ENABLE_DEBUG_INFO = YES; 346 | ONLY_ACTIVE_ARCH = YES; 347 | SDKROOT = iphoneos; 348 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 349 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 350 | TARGETED_DEVICE_FAMILY = "1,2"; 351 | }; 352 | name = Debug; 353 | }; 354 | 5130CC891DCEEC5300FA29CC /* Release */ = { 355 | isa = XCBuildConfiguration; 356 | buildSettings = { 357 | ALWAYS_SEARCH_USER_PATHS = NO; 358 | CLANG_ANALYZER_NONNULL = YES; 359 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 360 | CLANG_CXX_LIBRARY = "libc++"; 361 | CLANG_ENABLE_MODULES = YES; 362 | CLANG_ENABLE_OBJC_ARC = YES; 363 | CLANG_WARN_BOOL_CONVERSION = YES; 364 | CLANG_WARN_CONSTANT_CONVERSION = YES; 365 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 366 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 367 | CLANG_WARN_EMPTY_BODY = YES; 368 | CLANG_WARN_ENUM_CONVERSION = YES; 369 | CLANG_WARN_INFINITE_RECURSION = YES; 370 | CLANG_WARN_INT_CONVERSION = YES; 371 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 372 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 373 | CLANG_WARN_UNREACHABLE_CODE = YES; 374 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 375 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 376 | COPY_PHASE_STRIP = NO; 377 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 378 | ENABLE_NS_ASSERTIONS = NO; 379 | ENABLE_STRICT_OBJC_MSGSEND = YES; 380 | GCC_C_LANGUAGE_STANDARD = gnu99; 381 | GCC_NO_COMMON_BLOCKS = YES; 382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 384 | GCC_WARN_UNDECLARED_SELECTOR = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 386 | GCC_WARN_UNUSED_FUNCTION = YES; 387 | GCC_WARN_UNUSED_VARIABLE = YES; 388 | IPHONEOS_DEPLOYMENT_TARGET = 10.1; 389 | MTL_ENABLE_DEBUG_INFO = NO; 390 | SDKROOT = iphoneos; 391 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 392 | TARGETED_DEVICE_FAMILY = "1,2"; 393 | VALIDATE_PRODUCT = YES; 394 | }; 395 | name = Release; 396 | }; 397 | 5130CC8B1DCEEC5300FA29CC /* Debug */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 401 | DEVELOPMENT_TEAM = E54FNFV6AU; 402 | INFOPLIST_FILE = metal_bezier/Info.plist; 403 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 404 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 405 | PRODUCT_BUNDLE_IDENTIFIER = "com.eldade.mac-metal-tests.metal-bezier"; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SWIFT_VERSION = 3.0; 408 | }; 409 | name = Debug; 410 | }; 411 | 5130CC8C1DCEEC5300FA29CC /* Release */ = { 412 | isa = XCBuildConfiguration; 413 | buildSettings = { 414 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 415 | DEVELOPMENT_TEAM = E54FNFV6AU; 416 | INFOPLIST_FILE = metal_bezier/Info.plist; 417 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 418 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 419 | PRODUCT_BUNDLE_IDENTIFIER = "com.eldade.mac-metal-tests.metal-bezier"; 420 | PRODUCT_NAME = "$(TARGET_NAME)"; 421 | SWIFT_VERSION = 3.0; 422 | }; 423 | name = Release; 424 | }; 425 | 5130CC8E1DCEEC5300FA29CC /* Debug */ = { 426 | isa = XCBuildConfiguration; 427 | buildSettings = { 428 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 429 | BUNDLE_LOADER = "$(TEST_HOST)"; 430 | INFOPLIST_FILE = metal_bezierTests/Info.plist; 431 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 432 | PRODUCT_BUNDLE_IDENTIFIER = "com.eldade.mac-metal-tests.metal-bezierTests"; 433 | PRODUCT_NAME = "$(TARGET_NAME)"; 434 | SWIFT_VERSION = 3.0; 435 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios_metal_bezier_renderer.app/ios_metal_bezier_renderer"; 436 | }; 437 | name = Debug; 438 | }; 439 | 5130CC8F1DCEEC5300FA29CC /* Release */ = { 440 | isa = XCBuildConfiguration; 441 | buildSettings = { 442 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 443 | BUNDLE_LOADER = "$(TEST_HOST)"; 444 | INFOPLIST_FILE = metal_bezierTests/Info.plist; 445 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 446 | PRODUCT_BUNDLE_IDENTIFIER = "com.eldade.mac-metal-tests.metal-bezierTests"; 447 | PRODUCT_NAME = "$(TARGET_NAME)"; 448 | SWIFT_VERSION = 3.0; 449 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ios_metal_bezier_renderer.app/ios_metal_bezier_renderer"; 450 | }; 451 | name = Release; 452 | }; 453 | 5130CC911DCEEC5300FA29CC /* Debug */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 457 | INFOPLIST_FILE = metal_bezierUITests/Info.plist; 458 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 459 | PRODUCT_BUNDLE_IDENTIFIER = "com.eldade.mac-metal-tests.metal-bezierUITests"; 460 | PRODUCT_NAME = "$(TARGET_NAME)"; 461 | SWIFT_VERSION = 3.0; 462 | TEST_TARGET_NAME = metal_bezier; 463 | }; 464 | name = Debug; 465 | }; 466 | 5130CC921DCEEC5300FA29CC /* Release */ = { 467 | isa = XCBuildConfiguration; 468 | buildSettings = { 469 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 470 | INFOPLIST_FILE = metal_bezierUITests/Info.plist; 471 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 472 | PRODUCT_BUNDLE_IDENTIFIER = "com.eldade.mac-metal-tests.metal-bezierUITests"; 473 | PRODUCT_NAME = "$(TARGET_NAME)"; 474 | SWIFT_VERSION = 3.0; 475 | TEST_TARGET_NAME = metal_bezier; 476 | }; 477 | name = Release; 478 | }; 479 | /* End XCBuildConfiguration section */ 480 | 481 | /* Begin XCConfigurationList section */ 482 | 5130CC5D1DCEEC5200FA29CC /* Build configuration list for PBXProject "ios_metal_bezier_renderer" */ = { 483 | isa = XCConfigurationList; 484 | buildConfigurations = ( 485 | 5130CC881DCEEC5300FA29CC /* Debug */, 486 | 5130CC891DCEEC5300FA29CC /* Release */, 487 | ); 488 | defaultConfigurationIsVisible = 0; 489 | defaultConfigurationName = Release; 490 | }; 491 | 5130CC8A1DCEEC5300FA29CC /* Build configuration list for PBXNativeTarget "ios_metal_bezier_renderer" */ = { 492 | isa = XCConfigurationList; 493 | buildConfigurations = ( 494 | 5130CC8B1DCEEC5300FA29CC /* Debug */, 495 | 5130CC8C1DCEEC5300FA29CC /* Release */, 496 | ); 497 | defaultConfigurationIsVisible = 0; 498 | defaultConfigurationName = Release; 499 | }; 500 | 5130CC8D1DCEEC5300FA29CC /* Build configuration list for PBXNativeTarget "ios_metal_bezier_rendererTests" */ = { 501 | isa = XCConfigurationList; 502 | buildConfigurations = ( 503 | 5130CC8E1DCEEC5300FA29CC /* Debug */, 504 | 5130CC8F1DCEEC5300FA29CC /* Release */, 505 | ); 506 | defaultConfigurationIsVisible = 0; 507 | defaultConfigurationName = Release; 508 | }; 509 | 5130CC901DCEEC5300FA29CC /* Build configuration list for PBXNativeTarget "ios_metal_bezier_rendererUITests" */ = { 510 | isa = XCConfigurationList; 511 | buildConfigurations = ( 512 | 5130CC911DCEEC5300FA29CC /* Debug */, 513 | 5130CC921DCEEC5300FA29CC /* Release */, 514 | ); 515 | defaultConfigurationIsVisible = 0; 516 | defaultConfigurationName = Release; 517 | }; 518 | /* End XCConfigurationList section */ 519 | }; 520 | rootObject = 5130CC5A1DCEEC5200FA29CC /* Project object */; 521 | } 522 | --------------------------------------------------------------------------------