What is the Metal?

Back in 2014 at WWDC, Apple introduced Metal, a new platform for game developers that can provide 3D graphics with 10x better rendering performance and support for familiar game engines and companies.

What is the Metal shader language?

Metal Shader language is a programming language used to write [3D graphics rendering logic] and [parallel computing core logic] in iOS development. To write Metal framework App, you need to use Metal shader language program. The Metal shader language is used in conjunction with the Metal framework, which manages the running of the Metal shader language and optional compilation precursors. The Metal shader language uses Clang and LVVM for compilation processing, and the compiler has better control over the efficiency of code execution on the GPU.

The Metal grammar

Metal data type

  • Bool Fabric type, true/false
  • Char Is a signed 8-bit integer
  • Unsigned char/uchar An unsigned 8-bit integer
  • Short is a signed 16-bit integer
  • Unsigned short/ USHORT An unsigned 16-bit integer
  • Half 16bit floating point number
  • Float 32bit Floating point number
  • Size_t 64bit unsigned integer
  • The void type represents an empty collection

Note: Metal supports suffixes for literal types such as 0.5f,0.5F,0.5h, 0.5h

Texture types

Texture type is a handle that points to a 1d / 2D / 3D texture data. Describe the type of texture object in a function; Texture types have three access permissions:

  • Sample: Read and write
  • Read: read only
  • Write: write only

Rules of Use:

  • texture1d<T,access, a=access::sample>
  • texture2d<T,access, a=access::sample>
  • texture3d<T,access, a=access::sample>

T: data type that sets the color type to read from or write to a texture. T can be half,float,short,int, etc.

Code examples:

void foo (texture2d<float> imgA [[ texture(0) ]] ,
texture2d<float, access::read> imgB [[ texture(1) ]],
 texture2d<float, access::write> imgC [[ texture(2) ]])
{ 
... 
}
Copy the code
  • Void: no return value
  • Foo: the function name
  • Texture2d imgA [[texture(0)]] Defaults to sample;
  • Texture2d

    imgB [[texture(1)]] uses read
    ,>
  • Texture2d

    imgC [[texture(2)]] uses write
    ,>

Sampler type

The receiver type determines how a texture is sampled. There is an object in the Metal framework that corresponds to a sampler for the shader language

  • Enum Class coord{normalized,pixel} Specifies whether the texture coordinates need to be normalized when sampling in a texture
  • Enum class filter{nearest, Linear} texture sampling filter, zoom/zoom filter
  • Enum class min_filter{nearest,linear
  • Enum class mag_filter{nearest,linear} Sets the zoom filter mode of a texture sample

Set addressing modes for textures S, T and R

  • enum class s_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat};
  • enum class t_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat};
  • enum class r_address{clamp_to_zero,clamp_to_edge,repeat,mirrored_repeat};

Enum class address{CLAMp_to_zero, Clamp_to_edge,repeat,mirrored_repeat}

Enum class mip_filter{none,nearest,linear}

Note: Initializing the sampler in the Metal program must be declared using a CONSTExpr modifier

constexpr sampler s(coord::pixel,address::clamp_to_zero,filter::linear)
Copy the code

Function modifier

  • Kernel: indicates that this function is a parallel computing coloring function, which can be assigned to execute in 1d / 2D / 3D threads;
  • Vertex: indicates that this function is a vertex shader function that will execute once for each vertex in the vertex data stream and then generate data output for each vertex to draw the pipeline;
  • Fragment: indicates that this function is a fragment function that executes once for each fragment in the fragment metadata stream and its association and then outputs the color data for each fragment into the draw pipeline.

Note: for functions decorated with kernel, the return value type must be void

Only graph shader functions can be modified by vertex and fragment. For graph shader functions, the return value type identifies whether it evaluates for vertices or pixels. The graph shader function can return void, but this means that the function does not output data to the draw pipeline, which is a meaningless operation.

Note: A function modified by a function modifier may not call a function modified by a function modifier, otherwise it will fail.

kernel void hello1(){} kernel void hello2(){ hello1(); // This will not work. }Copy the code

Address space modifier used for a variable or parameter

  • Device: The device address space, pointing to the cache object allocated by the device memory pool. It is readable and writable. A cache object can be declared as a scalar, vector, or pointer or reference to a user-defined structure.

Note: Texture objects always allocate memory in the device address space. The device address space modifier does not have to appear in the texture type definition. The contents of a texture object cannot be accessed directly. Metal provides built-in functions to read textures

  • Threadgroup: the address space of a threadgroup, used by the parallel computing shader function to allocate memory variables that are modified by it and shared by all threads in the threadgroup. Variables allocated in the address space of a threadgroup cannot be used by the graphing shader function.
  • Constant: constant address space, pointing to a cache object that is also in the device memory pool, but is read-only.

Note: Pointers or references to a constant address space can be used as arguments to a function. Assigning a value to a variable declared as a constant causes a compilation error, as does declaring a constant without assigning an initial value.

  • Thread: Address space of a thread. Modifiable variables are available only to the current thread. A variable address space allocation declared in a graph drawing shader function or in a parallel computation shader function.

For graph shader functions, the parameter of pointer or reference type must be defined as device or constant address space; For parallel computing shaders, the parameter of pointer or reference type must be defined as device or ThreadGroup or constant address space.

Function parameters and variables

Function parameters are passed to a function and have a place to store them.

  • Device buffer/constant buffer: [[buffer(index)]], fixed notation, index is uint data, the same as below; Represents the location of the cache
  • A: [[range (index)]] Represents the position of the texture
  • Sampler: [[sampler(index)]] Represents the position of the sampler
  • Threadgroup buffer: [[threadgroup(index)]
kernel void add_vectors(
  const device float4 *inA [[ buffer(0) ]],
const device float4 *inB [[ buffer(1) ]], 
  device float4 *out [[ buffer(2) ]],
uint id [[ thread_position_in_grid ]])
{
 out[id] = inA[id] + inB[id];
}
  
Copy the code

The example above shows a simple parallel computing shader function that adds the cache inA and inB in the address space of the two devices and writes the result to out. The attribute modifier “[[buffer(index)]]” is the cache position specified by the shader function parameter. Thread_position_in_grid: Used to represent the position of the current node in a multi-threaded grid.

Built-in variable property modifier

  • [[vertex_id]] Specifies the vertex ID identifier
  • [[position]] Vertex information
  • [[point_size]] Specifies the size of the point
  • (m)] [[color] color
  • [[stage_in]] The slice input data used by the slice shader function is rasterized after output by the vertex shader function. Both the vertex shader function and the slice shader function can only have one parameter modified by stage_in. For the structure modified by stage_in, Its members must be integers or floating-point scalars or vectors.

Render a triangle using the Metal shader language

Create a data type file ccshadertypes.h shared by OC and Metal

// // ccshadertypes. h // MetalTriangle // // Created by iot_user on 2020/8/21. // Copyright © 2020 Iot.all Rights // #ifndef CCShaderTypes_h #define CCShaderTypes_h typedef enum CCVertexInputIndex{// vertex CCVertexInputIndexViewportSize CCVertexInputIndexVertices = 0, / / view size = 1,} CCVertexInputIndex; Typedef struct {// vector_float4 position; // Color vector_float4 color; } CCVertex; #endif /* CCShaderTypes_h */Copy the code

Create the iOS project and generate the. Metal file CCShaders. Metal

// // CCShaders. Metal // MetalTriangle // // Created by iot_user on 2020/8/21. // Copyright © 2020 Iot.all Rights reserved. // #include <metal_stdlib> using namespace metal; #import "ccShadertypes. h" // Typedef struct{// Vertex float4 clipSpacePosition [[position]]; // color float4 color; } RasterizerData; Vertex RasterizerData vertexShader(uint vertexID [[vertex_id]], constant CCVertex *vertices [[buffer(CCVertexInputIndexVertices)]], constant vector_uint2 *viewportSizePointer [[buffer(CCVertexInputIndexViewportSize)]]){ RasterizerData out; out.clipSpacePosition = vertices[vertexID].position; out.color = vertices[vertexID].color; return out; } // Fragment Float4 fragmentShader(RasterizerData in [[stage_in]]) {return in.color; }Copy the code

Create the CCRender file that manages Metal shader loading and rendering

CCRender.h

// // ccrender. h // MetalTriangle // // Created by iot_user on 2020/8/21. // Copyright © 2020 IOT. All rights reserved. // #import <Foundation/Foundation.h> @import MetalKit; NS_ASSUME_NONNULL_BEGIN @interface CCRender : NSObject<MTKViewDelegate> -(id)initWithMetalKitView:(MTKView *)mtkView; @end NS_ASSUME_NONNULL_ENDCopy the code

CCRender.m

// // ccrender. m // MetalTriangle // // Created by iot_user on 2020/8/21. // Copyright © 2020 Iot.All rights reserved. // #import "CCRender.h" #import "CCShaderTypes.h" @implementation CCRender { id<MTLDevice> _device; //GPU device ID <MTLCommandQueue> _commandQueue; // Command queue id<MTLRenderPipelineState> _pipelineState; // Render pipe vector_uint2_viewportsize; } -(id)initWithMetalKitView:(MTKView *) MTKView {self = [super init]; If (self) {// get GPU _device = mtkView.device; _commandQueue = [_device newCommandQueue]; Id <MTLLibrary> defaultLibrary = [_device newDefaultLibrary]; VertexFunction = [defaultLibrary newFunctionWithName:@"vertexShader"]; // Get the function id<MTLFunction> fragmentFunction = [defaultLibrary newFunctionWithName:@"fragmentShader"]; / / create the render management describe MTLRenderPipelineDescriptor * pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; pipelineDescriptor.label = @"pipelineDescriptor"; pipelineDescriptor.vertexFunction = vertexFunction; pipelineDescriptor.fragmentFunction = fragmentFunction; pipelineDescriptor.colorAttachments[0].pixelFormat = mtkView.colorPixelFormat; // create render pipe NSError *error; _pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; if (! _pipelineState) {NSLog(@"Failed to create pipeline state, error %@", error); return nil; } } return self; } -(void)drawInMTKView:(MTKView *)view{ //1. Static const CCVertex triangleVertices[] = {// RGBA color values {{0.5, -0.25, 0.0, 1.0}, {1, 0, 0, 1}}, {{0.5, 0.25, 0.0, 1.0}, {0, 1, 0, 1}}, {{- f 0.0, 0.25, 0.0, 1.0}, {0, 0, 1, 1}},}; ClearColor = MTLClearColorMake(0, 0, 0, 1.0); <MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer]; commandBuffer.label = @"commandBuffer"; / / 4. Create a render descriptor MTLRenderPassDescriptor * renderPassDescriptor = the currentRenderPassDescriptor; if (renderPassDescriptor ! RenderEncoder <MTLRenderCommandEncoder> renderEncoder = [commandBuffer) {//5 renderCommandEncoderWithDescriptor:renderPassDescriptor]; renderEncoder.label = @"renderEncoder"; MTLViewport viewPort = {0.0,0.0,_viewportSize. X,_viewportSize. Y,-1.0,1.0}; [renderEncoder setViewport:viewport]; RenderEncoder setRenderPipelineState:_pipelineState]; / / 8. Vertex data and color data into the shader [renderEncoder setVertexBytes: triangleVertices length: sizeof (triangleVertices) atIndex:CCVertexInputIndexVertices]; [renderEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize)) atIndex:CCVertexInputIndexViewportSize]; RenderEncoder drawPrimitives (MTLPrimitiveTypeTriangle) vertexStart:0 vertexCount:3]; RenderEncoder [renderEncoder endEncoding]; / / 12. Will push command buffer to the view can draw area [commandBuffer presentDrawable: the currentDrawable]; } //13. Commit the render command to the GPU [commandBuffer commit]; } -(void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size { _viewportSize.x = size.width; _viewportSize.y = size.height; } @endCopy the code

Load the render results via MTKView

// // viewController. m // MetalTriangle // // Created by iot_user on 2020/8/21. // Copyright © 2020 Iot.all Rights reserved. // #import "ViewController.h" #import "CCRender.h" @interface ViewController () { MTKView *_view; CCRender *_render; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. _view = (MTKView *)self.view; _view.device = MTLCreateSystemDefaultDevice(); if (! _view.device) { NSLog(@"Metal is not supported on this device"); return; } // _render = [[CCRender alloc] initWithMetalKitView:_view]; if (! _render) { NSLog(@"Renderer failed initialization"); return; } // initialize viewport size [_render mtkView:_view drawableSizeWillChange:_view.drawableSize]; _view.delegate = _render; } @endCopy the code

Note: Self. view in main. storyboard has been strongly cast to MTKView. You can also create a child view of MTKView to display the render results.

Results the following