J.Doe
J.Doe

Reputation: 1552

Metal shader types vertex and buffer best practice demonstrated in "ShaderTypes.h"

Here is the full shadertypes.h file. I am going to break it down into parts that I cant understand.

#ifndef ShaderTypes_h
#define ShaderTypes_h

//Part 1 Compiler flags
#ifdef __METAL_VERSION__
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
#define NSInteger metal::int32_t
#else
#import <Foundation/Foundation.h>
#endif

#include <simd/simd.h>

//Part 2 buffer index
typedef NS_ENUM(NSInteger, BufferIndex)
{
    BufferIndexMeshPositions = 0,
    BufferIndexMeshGenerics  = 1,
    BufferIndexUniforms      = 2
};

//Part 3 vertex attribute and position
typedef NS_ENUM(NSInteger, VertexAttribute)
{
    VertexAttributePosition  = 0,
    VertexAttributeTexcoord  = 1,
};

//Part 4 texture index color
typedef NS_ENUM(NSInteger, TextureIndex)
{
    TextureIndexColor    = 0,
};

//Part 5 uniforms
typedef struct
{
    matrix_float4x4 projectionMatrix;
    matrix_float4x4 modelViewMatrix;
} Uniforms;

#endif /* ShaderTypes_h */

Part 1

#ifdef __METAL_VERSION__
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
#define NSInteger metal::int32_t
#else
#import <Foundation/Foundation.h>
#endif
//A bunch of definitions go after this
#endif

My confusion is mostly why we are doing all of this code. It seems to be checking if the user has metal but then what is this NS_ENUM it is defining? Why is it defining a metal variable? While is foundation conditionally imported?

Part 2

typedef NS_ENUM(NSInteger, BufferIndex)
{
    BufferIndexMeshPositions = 0,
    BufferIndexMeshGenerics  = 1,
    BufferIndexUniforms      = 2
};

Not exactly sure what this is doing especially since I cant find anyone referencing them explicitly anywhere.

Part 3

typedef NS_ENUM(NSInteger, VertexAttribute)
{
    VertexAttributePosition  = 0,
    VertexAttributeTexcoord  = 1,
};

This one has a bit more of a clear usage because it is referenced in the .metal file

typedef struct
{
    float3 position [[attribute(VertexAttributePosition)]];
    float2 texCoord [[attribute(VertexAttributeTexcoord)]];
} Vertex;

as well as in the attribute section of the vertex descriptor code

    mtlVertexDescriptor.attributes[VertexAttribute.position.rawValue].format = MTLVertexFormat.float3
mtlVertexDescriptor.attributes[VertexAttribute.position.rawValue].offset = 0
mtlVertexDescriptor.attributes[VertexAttribute.position.rawValue].bufferIndex = BufferIndex.meshPositions.rawValue
...

Somehow it appears to be keeping track of indexes of the various elements rather like the buffer.

Part 4

This one I sort of get as it is referenced in the render here

renderEncoder.setFragmentTexture(colorMap, index: TextureIndex.color.rawValue)

as well as the shader here

fragment float4 fragmentShader(..., texture2d<half> colorMap     [[ texture(TextureIndexColor) ]]){

I sort of get this one (Minus the NSEnum part) however I dont get how it is good practice to do this for just one thing.

Part 5

This one is actually the only one I understand it looks like its a struct for all the uniform components which makes a lot of sense as it is storing the actual types of the uniforms allowing the shader to be exposed to the struct as well as the renderer.


I guess ultimately I am wondering why this approach was taken and why it is a suggested best practice by Apple. I suppose it sort of makes sense to do things this way except for the fact that metal appears to play better with objective-c even though it looks like swift.

Upvotes: 2

Views: 2233

Answers (1)

Ken Thomases
Ken Thomases

Reputation: 90521

This is a header that's meant to be shared by Metal shader code and app code (either Objective-C or, via bridging, Swift). Having to share it between those two languages requires a bit of care.

The #ifdef __METAL_VERSION__ test determines which language it's being compiled in. For Metal, a couple of macros are defined. For Objective-C, it imports Foundation. (Foundation can't, of course, be imported when compiling Metal code.)

In their (Objective-)C headers, Apple routinely declare enums using the NS_ENUM() macro. That macro is defined within the Foundation headers, so it can be used when this header is being compiled into app code. It's not defined in the Metal headers, so this header needs to define it for itself if it wants to use it (as it does). The Foundation version of the macro is useful because it detects compiler capabilities to use typed enums if available and conventional enums if not. In this header, Apple is implementing that same macro for the Metal shader language. Since there's no question whether the compiler supports typed enums (it does), there's only one definition of the macro which takes advantage of that feature.

Later in the header, they plan on using the NSInteger type. That's defined by the Foundation headers for app code. Since they want to use the same code for Metal but that type isn't defined there, they need to define it in this header. They make a couple of weird choices, though. First, they use a macro instead of a typedef. Second, they define it to be equivalent to int32_t (in the metal namespace). That's weird because the app code is going to be 64-bit (Metal isn't available to 32-bit apps) causing NSInteger to be a 64-bit integer type (equivalent to int64_t). So, the two worlds are going to disagree about the size of NSInteger and therefore all of the enums based on it. That's kind of bad, but probably doesn't cause a real issue given the ways these enums are actually used.

It would probably have been better to simply base the enums on int, which is 32-bit in all Apple environments.

Parts 2, 3, and 4 are all similar. It's generally good practice to use "symbolic" constants rather than just magic numbers (i.e. integer literals within the code). It is less error-prone and improves readability. These parts are simply defining some such symbolic constants for use by the Metal code and app code to share. The fact that some of these names aren't used in the particular sample project you're examining suggests that Apple uses this same header for multiple sample projects, or something like that.

Upvotes: 9

Related Questions