Your app can be ready to harness the power of Metal starting with just a few lines of code. Get introduced to the new MetalKit framework and learn its simple API for model and texture loading, animation control, and other common tasks. Plug into Metal Performance Shaders and access a powerful library of image-processing operations tuned for Apple hardware.
DAN OMACHI: Good morning, welcome to the second part
of our What's New in Metal presentation.
My name is Dan Omachi.
I'm an engineer in Apple's GPU software frameworks team.
Today my colleague Anna Tikhonova and I will talk
about technologies that build upon Metal,
helping you deliver great rendering experiences
on both iOS and OS X.
So this is the second of three sessions
at this years WWDC talking about Metal.
In the first session, Rob Duraf talked about developments
in Metal that have happened in the past year.
He also described some of the new features
in Metal we just released.
He also described how app thinning is a great match
for your Metal applications.
In this session, I will be up here first to talk to you
about MetalKit, convenience APIs allowing you bring
up Metal applications much more quickly.
Then Anna will come up to talk
about Metal performance shaders framework,
which offers great shaders
for common data parallel operations available
on iOS devices with an A8 processor.
And tomorrow, you will get a chance
to catch the Metal Performance Optimization Techniques session,
where they will introduce the Metal System Trace Tool
and provide you with some best practices
for shipping an efficient Metal application.
So let's get started with Metal kit.
Utility functionality for your Metal apps.
So because Metal is a low level API, there are a number
of things you need to do to get up and running.
MetalKit hopes to help with this
by providing efficient implementation
for commonly used scenarios.
This requires less effort on your part to get up
and rendering and we offer increased performance
and stability over the standard boilerplate code
that you might implement yourself.
There is less maintenance for you as the burden
of that maintenance has been shifted from you to us.
So MetalKit consists of three main components.
First is the MetalKit view.
A unified view class between iOS and OS X
for rendering your Metal scenes.
Second is the texture loader which creates texture objects
from image files on disk.
And finally, Metal kit's model I/O integration which loads
and manages mesh data from Metal rendering.
MetalKit view is the simplest way
to get Metal rendering on screen.
It's a unified class on both iOS and OS X.
It offers pretty much the same interface
on both Operating Systems, but naturally it's a subclass
of UI view for iOS and a subclass of NS view on OS X.
Its main job is to manage displayable render targets
for you, and it creates render path descriptors
for these render targets automatically.
Now, it's super flexible in terms of the ways
that it can execute your draw code.
You can use a timer-based mode
where it will execute your draw code at a regular interval
in synchronization with a display,
or you can use an event-based mode
which will trigger your draw code whenever a touch
or UI event has occurred, so you can respond to that event.
Finally, you can explicitly drive your draw code,
perhaps in an open loop on a secondary thread
at your own frame rate.
So there are two approaches to using the MetalKit view.
The simplest approach is to implement a delegate
to handle your draw and resize operations.
In this case, you would implement the draw
in view method to handle your per-frame updates including
encoding any rendering commands.
You would also implement the view will layout
with size method to handle size changes to your view.
This is where you might update your projection matrix
or change any texture sizes
to better fit your displayable area.
Now, if you have any other pieces of the view that you need
to override, you can subclass the MetalKit view.
And in this case on iOS,
you would override the draw Rect method
to handle your per-frame updates
and the layout subviews method to handle resizes.
Likewise on OS X you would handle,
you would override these two methods, the draw Rect
and set frame size method.
So here is an example of setting up a view controller
that also serves as the delegate to our view.
In the view did load method, after we received a reference
to the view, we will assign ourself as the delegate,
and particularly important on OS X is that we will need to choose
and set a Metal device.
Once we are done with that, we can configure some
of the view's properties including choosing our own
custom pixel formats for the color
and depth and stencil buffers.
We can use multisampling
by increasing the sample count property to a value above 1,
or we can set our custom clear color.
Now, here is a very basic usage of the MetalKit view
in the implementation of the per frame update.
In our drawing view method, we call the views render,
I'm sorry, current render past descriptor.
Now, the first time you access this property each frame,
the view will call into core animation
and get a drawable back,
which you can encode your rendering commands
and render to.
So we will render our final render pass,
which will show up in this drawable.
And then we will present the drawable, which is stored
in the view's current drawable property
and we will commit our command buffer.
Now, because of its importance
in structuring your per-frame updates, let me take a minute
to talk about managing these drawables.
So there are a limited pool of drawables
in this system all managed by core animation.
There are only a few of them mostly because of their size,
they take up some space.
Now, these drawables are concurrently used
through many stages of the display pipeline.
Here is roughly how it works.
First, your application encodes commands
to be rendered onto the drawable.
It sends that drawable down to the GPU
as your application encodes commands for the next frame
and the GPU renders to that drawable, and core animation
at this stage may do compositing
with other layers into that drawable.
And finally, the display can take the drawable
and slap it up onto the screen.
Now, the display can't replace it with anything
until another drawable is available.
So if any of these previous stages are taking a lot of time,
it just has to sit there for awhile.
Additionally, the display can't recycle that drawable back
up to your frame until it has something available.
So let's take a look at your application frame
with respect to these drawables.
First, you call the MetalKit views current render past
descriptor which reserves a drawable.
Then you will encode rendering commands that you want
into that drawable, and finally you will present
and commit the drawable
which will release it back to core animation.
Now, this is fine if all we are doing is rendering a single
render pass, however,
it's likely we will do other operations
such as some app logic, encoding in offscreen render pass
where we don't actually need the drawable,
or running some compute kernels for physics or whatnot.
In this case we are essentially hogging the drawable
from our future self because in a subsequent frame,
we will call this current render pass descriptor
and it will sit there waiting for a drawable
to become available, which may not be the case
because we are doing these other operations and reserving it
for much longer than we need to.
So to solve this problem,
let's put these operations before our access
to the current render pass descriptor.
Now, let me just note that this isn't a problem specific
to the MetalKit view.
You need to be aware of this issue if you roll your own view
in access core animation directly.
So this is information that's quite useful in any case.
Now, here is a more complete example
of our per-frame rendering update.
First, as I described we want to update our app's render state,
encode any offscreen passes,
do anything where we don't need the drawable.
And then we can continue as we did previously,
get the current render pass descriptor, encode our commands
for that final pass, and present and commit our command buffer.
The key point is that these two stages should be
as close together as possible.
It's a critical section where we are holding onto a resource
and we don't want to hold onto it any longer than we need to.
That's it for the view.
Let's move on to the texture loader.
It's textural loading made simple.
You give a reference and you get back a fully formed
Not only is it simple, it's fast and fully featured.
It asynchronously decodes files
and creates textures on a separate thread.
It has support for many common image file formats including
JPG, TIF and PNG and also supports the PVR
and KTX texture file formats.
What's interesting about these formats is that they store data
in a raw form that can be uploaded to your Metal Texture
without any conversion.
Additionally, you can encode data for MIT maps for any
of the other types of textures including 3D textures,
cube maps, and texture arrays.
Its usage is really simple.
First, we create a texture loader object
by supplying a device.
Then, once we have that texture loader object,
we can create many textures with it.
First, we will give a URL location of our image file,
and we can supply a number of options including how we want
to treat the sRGB information in the file or whether
or not we want to allocate memory for MIT maps
when we create this texture,
and finally we will supply a completion handler block.
Now, this block will get executed as soon
as the texture loader has finished loading the texture
and created it.
It will pass the texture handler back to you
which you can stash away for later and render
with when you need to.
That's very simple, the texture loader.
Let's move on to model I/O.
So model I/0 is a new framework introduced
with iOS 9 and OS X El Capitan.
And one of its key features is
that it can load many model file formats for you.
You can create your own importers and exporters
for proprietary formats if you need to.
Some of the cooler features here are
that you can do offline baking operations.
You can create static ambient occlusion maps,
light map generations, it also includes the Voxelization
of your meshes.
It provides you a way to focus on your rendering code
and write your shaders.
You don't have to deal with creating some parchers
to get some stuff off disk.
You have to deal less with serialization,
you just load a model file with Model I/O,
put it into some form you can render it with,
and start writing your shaders.
So what does MetalKit provide in this context?
It's utilities to efficiently use model I/O with Metal.
It offers optimized loading of model I/O meshes
into Metal buffers, the encapsulation of mesh data
within MetalKit objects, and there are a number of functions
to prepare mesh data for Metal pipelines.
Let me walk you through the process of loading a model file
with model I/O and getting it rendering on screen with Metal.
And here are the steps we will take.
So first, we will create a Metal render state pipeline
that we'll use to create our mesh, to render our mesh.
Then we will actually load the model file
by initializing the model I/O asset.
And with that asset, we will create MetalKit mesh
and sub mesh objects.
Finally, we will render those objects with Metal.
So let's focus on creating a Metal Render State Pipeline
and we will pay particular attention
to creating a vertex descriptor that will describe the layout
of vertices that we'll need our mesh to be
in to feed our pipeline.
Here is the bare bones of a vertex shader.
It uses the stage in qualifier which basically says
that our per vertex inputs, the layout for them,
will be described outside of the shader
in our objective-C code using a vertex descriptor.
It uses this vertex input structure
which is defined up here.
And the key part of this vertex input structure are these
attributes, the indices here which we will use to connect
up outside inside of our Objective-C code.
Note that these floating-point vector types define how the data
looks within the shader, not actually how the data looks
as it's being fed into the shader
from our Objective-C code.
For that, we need to create a vertex descriptor.
Now, I'm going to put this vertex input structure
up here just for reference, but let me just remind you
that it does not define the layout of the data
as it's being fed into the shader.
We are actually creating the Metal Vertex Descriptor
that is down below.
And for that, what we will do is for attribute zero,
the position will define as using three floating points,
three floating point values.
For attribute one, the color will specify that it's going
to be composed of four unsigned characters,
not the four floats above, four unsigned characters
and it will have an offset
of 12 bytes immediately following the position data.
Now, for the texture coordinates in attribute 2, we will define
that it uses two half floats.
And that it immediately follows the position and color data
with an offset of 16 bytes.
And finally, we will specify that the size
of each vertex is 20 bytes by setting the stride
to 20 for that buffer.
Now, this defines the layout of each vertex
within our array of vertices.
So now that we have got our Metal Vertex Descriptor,
we can assign it to our render state pipeline,
and with that render --
excuse me, with the render pipeline descriptor,
we can create a Metal Render State Pipeline.
So let's move on to actually loading our asset
and using model I/O for that task.
And we will actually use the vertex descriptor we just
created in the previous step,
along with a MetalKit mesh buffer object,
a mesh buffer allocator object.
I will describe a little bit more
about its importance as we continue.
So the model I/O vertex descriptor
and Metal Vertex Descriptor are very similar,
but while the model I/O vertex descriptor describes the layouts
of vertex attributes within a mesh,
the Metal Vertex Descriptor describes the layout
of vertex attributes as their input
to a render state pipeline.
Now, they are intentionally designed to look similar
as they contain attribute and buffer layout objects,
and the reason for this is it simplifies the translation
of one object to another.
Now, each attribute in a model I/O vertex descriptor has an
identifying string base name.
Model I/O assigns a default name if one does not exist
in the model file or that model file does not support
These names include position, normal, texture, coordinate,
color, et cetera, and model I/0 defines these
with the string based MDLVertex attribute constants.
There are a number of files including the Alembic file
format where you can customize those names.
Be aware if you are changing the names you will need
to access these attributes with those customized names.
So we recommend that you create a custom model I/O vertex
descriptor, because by default model I/O loads vertices
yet memory-hungry floating-point types.
This is one of the advantages of using model I/0.
You can actually load a model format and have the vertex data
in any form that you would like and use model I/0 to massage
that data into a format you can actually use.
In this case, we want to feed the pipelines
with the smallest type
that meets your precision requirements.
This would improve your vertex bandwidth efficiency;
as you are feeding each vertex to the pipeline,
you don't really want a bloated vertex.
So here is the layout we defined previously
when creating our Metal Vertex Descriptor.
Now, we will create our model I/O vertex descriptor
by calling this MTK model I/O vertex format from Metal
and we will supply our Metal Vertex Descriptor.
This builds the majority of this model I/O vertex descriptor,
yet we still need to tag each attribute with a name,
so model I/O knows what we are talking about.
So for attribute 0, we will tag it
with the vertex attribute position name.
And similarly for attribute 1 and 2, we will tag them
with a color and texture coordinate attributes.
The other thing we will do here is we will create a MetalKit
mesh buffer allocator and we will supply a Metal device.
Now, what this object does is it allows model I/0
to load vertex data directly into GPU backed memory.
Now, you don't have to use a MetalKit mesh buffer allocator,
but what that will do is it will allocate system memory
for these vertex and index buffers inside of the mesh.
And when you want to actually render it, we will need to copy
from that system memory down into the GPU backed memory.
So for efficiency, it's really desirable to use one
of these mesh buffer allocators, and here is how you use it.
Now, we are going to load our asset file.
We will supply the URL location, the model I/0 vertex descriptor
which will tell model I/0 how to lay out each vertex
and we will also supply this mesh buffer allocator
so that model I/0 can load this data directly
into GPU backed memory.
So now that we have got our asset,
let's actually create some MetalKit mesh
and some mesh objects.
So here's an example of an asset
that might be created by model I/0.
Inside of an asset, we might have camera objects,
light objects, and particularly important
to us right now are the mesh objects.
Now, MetalKit is primarily concerned
with these mesh objects.
It doesn't really deal directly with the light
and camera objects because that sort of data is very specific
to your custom shaders and your engines.
You can actually introspect into this object
or go look inside this object and grab out that camera
and light information to plug it into your shaders,
but MetalKit isn't directly involved in that process.
So what we can do is pass in this asset directly
to this mesh, meshes from asset class function
which will create an array of MetalKit meshes.
Let's take a look at what's inside this mesh object.
So first are these vertex buffers
which includes the position attributes,
the normal attributes, textural attributes, et cetera.
In our example, we only needed one array
because we interleaved all of our data.
However, you can define the layout to use multiple arrays,
and therefore you would have multiple vertex buffers.
You could define that attribute 0 would be inside
of a single array, so you would have an array of positions
in one array, an array of texture coordinates in the next,
another array with colors, and so on.
The mesh also includes a vertex descriptor
which defines this layout
and it's the same object we just created and passed
in when we initialized our asset.
And finally, the mesh contains a number of sub mesh objects.
Now, the key part of each sub mesh object is this index
buffer, which references vertices inside the
Additionally, there are a number of properties which you can use
to make a draw call with Metal.
So now that we have got our Metal kit mesh
and sub mesh objects, let's go ahead and render them.
So first we will iterate through each vertex buffer.
Now, we may have a sparse array, so we need to make sure
that there is actually something in each buffer,
but once we are sure of that, we can continue on
and set the vertex buffer in our render encoder.
Now, the vertex buffer actually has two properties,
the buffer itself, and an offset within the buffer
where your vertex data resides.
We also need to supply a buffer index telling the pipeline
exactly where the data is.
Now, we will actually render our mesh.
We will iterate through every sub mesh,
and make our draw index primitives call.
Note here that the sub mesh has all of the parameters
for this draw index parameter.
So today we posted this MetalKit essentials sample
on the WWDC 2015 site.
I encourage you to download it.
It describes a number of the techniques I described.
It uses model I/0 to load this little airplane object that's
stuffed in an OBJ file and creates a MetalKit mesh
and renders it on screen.
So you can get an idea of exactly how this is all done.
So I encourage you to check that out.
So that's it for me, my name is Dan Omachi,
I will be at the Metal Lab tomorrow if you have questions
about topics I have discussed.
I would like to welcome my colleague, Anna Tikhonova
on stage to talk about the Metal performance shaders frameworks.
ANNA TIKHONOVA: Good morning.
Thank you, Dan, for the introduction, my name is Anna
and I will be talking to you about Metal performance shaders.
Let's get started.
So first of all, what is it?
It's a framework of optimized high performance data parallel
algorithms for the GPU in Metal.
When and why would you use it?
If you are writing C code and you want
to add a common sorting algorithm
to your CPU application, you are very unlikely to implement one
from scratch unless it was out of self-interest.
You are a lot more likely to use the implementation provided
to you by the library because it's already been debugged
and optimized for you.
Likewise, if you wanted to add an image processing operation
to your CPU application, on our platform,
you would use the accelerate framework
because it uses vImage.
It's a powerful,
high-performance tuned image processing framework for --
that utilizes the CPU's vector processing.
These are just a few examples.
The point is that there is a rich environment available
for your CPU applications.
On the GPU the story is a bit different,
you simply have fewer options but we would
like to change the story.
Our goal is to enrich your Metal programming environment.
We have selected a collection of common filters
that we see often used in graphic processing pipelines
in your image processing applications and games.
These algorithms are optimized for iOS and available
in iOS 9 for the A8 processor.
The Metal performance shaders framework has two goals,
performance and ease of use.
It's designed to integrate easily
into your Metal applications.
It operates on Metal resources directly.
They are the input and the output.
We are not only giving you collection
of these high performance optimized awesome kernels --
we are giving you that, but we are also taking care of all
of the host code necessary to launch these kernels.
We take care of the decision-making process of how
to split up the work for parallel computation.
The work you have to do to take advantage of this framework
in your applications usually amounts
to only a few lines of code.
It's as simple as calling a library function.
So now that I have introduced the framework to you,
let's take a look at the available operations.
Here is a full list and let's start from the beginning.
I will cover just a few of these actually
and I will show you examples.
So first, the framework supports the histogram filter
and the histogram equalization and specification filters.
The equalization and specification filters,
they allow you to change the distribution
of color intensities in your image.
The equalization filter is a special case.
It changes the distribution
from the current distribution to the uniform one.
And the specification filter enables you
to set any distribution of your choice.
You specify the histogram that will be used in the filter.
This is an example of the equalization filter.
It increases the global contrast in the image.
Here it brings out the rainbow in the sky quite beautifully.
One thing I'd like to mention
about these filters is they are not an end in themselves.
They can be used as an intermediate step
in a more complex algorithm.
The histogram filter can be used as an intermediate step
in implementing tune mapping,
which is a technique commonly used by graphics developers
to approximate the appearance of high dynamic range.
So moving on, we also support Lancos resampling.
It's a high-quality resampling algorithm that can used
to downscale, upscale, squeeze, and stretch images.
In this example, I stretched the image vertically
and squeezed it horizontally while preserving all
of the image content.
You also support the thresholding filter.
It can be used to find image edges
if chained with a Sobel filter.
Let's take a look at an example.
This is the output of the thresholding filter,
and now it's fed into the Sobel filter to give you image edges.
And finally we support a whole range
of convolution kernels including general convolution,
where you can specify your own convolution matrix.
And we also support Gaussian blur,
box tent, and Sobel filters.
My final example will be of a Gaussian blur.
You should all be very familiar with it;
we like to use it in our UI.
What if you wanted to use a Gaussian blur
in your own application,
the Metal performance shaders framework makes it very easy.
How easy, you ask?
I'm building some anticipation here.
It's just two lines of code.
You first have to create a blur filter object, and then you have
to encode the filter to the command buffer.
And that's it [applause].
Thank you, guys.
And one thing I just wanted to point out again and just note is
that this API takes your common Metal resources as input.
Your device, your command buffer, your textures.
These are the Metal resources
that you already create in your application.
And now that I have shown you these two lines of code,
let's take a look at where they plug
in into your current Metal work flow.
So this is a graphical representation
of your command buffer.
It contains all of the commands you are going
to be submitting to the device.
You do your work as you usually would.
You render your scene by issuing draw calls
and you do your post processing effect by dispatching kernels.
Now you have decided that one
of your post processing effects is going to be a blur filter.
So this is exactly where it goes.
And don't forget that you still have to submit your commands
to the device as you normally would.
Nothing changes here.
And now, if you would like to look at the sample code
for the example I was just going through,
you could do so right now.
You can go to developer.Apple.com
and download the example called Metal performance shaders
I've mentioned before that the Metal performance shaders
framework has two goals, performance and ease of use.
I just showed you how easy it is to use.
Let's take a quick look behind the scenes
at what's giving you this performance.
For every one of these filters including the Gaussian blur
filter, we had to choose the right algorithm.
The right here means correct and it has to be the fastest.
The fastest for a particular combination of input data,
input parameters, and device GPU.
What do I mean by this?
There are multiple ways to implement Gaussian blur.
There are constant cost, log 2, linear,
and brute force algorithms.
All of these approaches have different start
up costs and overheads.
One approach may work really well for a small kernel radius
but perform very poorly on a large kernel radius.
The point is we had to implement each one of these approaches
and find out experimentally which one is going
to be the fastest for a particular combination
of input problem, input parameters, and device GPU.
And after this process, all of the kernels had to be tuned
for such parameters as your kernel radius,
your pixel format,
your underlying hardware architecture's memory hierarchy,
and such parameters as number of pixels per thread
and thread group dimensions.
This is what determines how to split up your work in parallel.
And finally, I would like to mention
that the framework also performs CPU optimizations for you.
It optimizes program loading speed.
It also reuses intermediate textures,
and finally it does some compute encoder optimization for you.
Specifically, it can defect
if you are using multiple computing coders in a row and if
so it will coalesce them.
And after we have done all of these steps for you,
that's cool, but what would this actually look like in terms
of code, for example, for an optimized Gaussian blur shader
that I just showed you?
Well, are you ready for it?
Here is the code.
So now all of you know how
to implement your own optimized Gaussian blur, right?
I bet you didn't know you were going
to learn this in this session.
Basically all joking aside, this is 49 Metal kernels,
2,000 lines of kernel code
and 821 different Metal Gaussian blur implementations
where each implementation is some combination
of these 49 Metal kernels, so it looks like a lot of work we did
and now you don't have to.
Now, let's take a look
at the Metal performance shaders framework in action.
So first, I will demonstrate the performance
of a simple textbook separable Gaussian blur implementation
that took only minutes to write in Metal.
This is probably something you would start with if you had
to implement your own blur
and you didn't have Metal performance shaders available
So now we are happily running at 60 frames per second
but we are not actually doing any work yet.
The sigma value is 0.
Let's change the sigma value to 6 and we are down to
about 8 frames per second.
Dare we go any further?
Let's try a sigma of 20.
Okay. And we are down to 3 frames per second
so that's not going to work.
Let's switch to the Metal performance
So now we are back to 60 frames per second,
not doing any work, sigma of 6.
Still 60 frames per second.
Sigma of 20.
Still 60 frames per second.
And, of course, we had to go further and really blur it,
still at 60 frames per second.
So this looks like a winner.
So your screen refresh rate is 60 hertz.
This means that we are running at 60 frames we are second,
so the performance of this optimized Gaussian blur shader
you have seen in the demo is capped at 60 frames per second.
This means that you have 16.6 milliseconds to draw your frame,
and this also includes any compositing work
that your system might need to do.
This chart shows you the execution time
of this optimized Gaussian blur filter for different values
of sigma and as you can see, the execution time is a lot less
than 16.6 milliseconds.
So this means that you still have some extra time
to do additional GPU work,
and still hit the desired 60 frames per second.
And now there are just a few more details I would
like to cover.
Sometimes you will need to work on very large images
and you will need to tile them.
And sometimes you will just need to work
on a portion of your image.
So there is a mechanism for that.
It's called source offset and destination clip Rect.
Clip rect has an origin and size.
It determines the region of the destination texture,
which is going to be updated by a filter.
The source offset only has an origin.
The size is implicit, it's determined by the clip rect,
and it is just an offset from the upper left corner
of your source texture.
They work together to give you the final image.
In the Metal performance shaders framework your source
and destination can be one in the same texture.
In this case, the clip rect
and source offset work exactly the same way.
When the source and destination are the same texture,
we call it an in place operation.
Use it to save memory.
How could you do you actually encode one
of these filters in place?
You have to use the encode to command buffer method that takes
in place texture and a fall by back copy allocator.
One thing to keep in mind here, it's not always possible
for the shaders to run in place.
It depends on your filter,
on the filter parameters and properties.
If you want this operation
to always succeed, use a copy allocator.
It will be called automatically, only in the situation where the
in place operation is not possible.
And we will create a new destination texture for you
so that the operation can proceed
out of place if necessary.
And here is an example of a simple fall back copy allocator.
This one simply creates a new destination texture
with the same pixel format and dimensions
as the source texture, very simple.
So now I have shown you an example before of an
in place operation where you only modified a portion
of your destination texture and everything outside
of the clip rect remained unchanged.
You can also do this in the copy allocator.
Just initialize your destination texture with the contexts
of your source texture.
And I would also like to mention that all
of the usual Metal resources such as your device
and your command buffer are available to you
in the copy allocator.
Now that I have covered these details,
let's jump into the summary.
I would like to say please use the Metal support frameworks,
MetalKit and Metal performance shaders, they are robust,
they are optimized, and as I have shown you, they're easy
to integrate into your Metal applications.
They will allow for faster bring up time of your applications.
Now, you can spend the time
on making application unique instead
of implementing common tasks.
And, of course, as an added benefit, there is less code
for you to write and maintain.
And come to our labs, give us feedback.
Let us know how to get started or give us questions.
Let us know if there are new utilities or shaders you would
like to see added to the support frameworks.
You can always find more information online.
We have documentation videos available, and take advantage
of the Apple Developer Forums and technical support.
For general inquiries, contact our gaming technologies
evangelist Allan Schaffer.
You can watch the past sessions online, but if you would
like to learn new Metal performance optimization
techniques, come to our talk tomorrow
at 11:00 a.m. Thank you.
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.