The purpose of this case is to understand the two storage methods of vertex data and their differences and application scenarios

In Metal Entry Level 02: The setVertexBytes(_:length:index: 🙂 method is used when passing vertices. The main reason is that when drawing a triangle, only three vertices are required, so the data can be stored in an array. The data is stored in the CPU. So when we have a lot of vertex data, how do we store and pass it?

The setVertexBytes(_:length:index:) method is described as follows in the official documentation of apple. For one-time data smaller than 4KB (4096 bytes), setVertexBytes(_:length:index:) is used. If the data length exceeds 4KB or the vertex data needs to be used for many times, an MTLBuffer object needs to be created. The purpose of the buffer is to store the vertex data in the vertex cache. The GPU can directly access the cache to obtain the vertex data. And the buffer cache data needs to be passed to the vertex shader using the setVertexBuffer(_:offset:index:) method.

Here is a case to illustrate the use of MTLBuffer. The overall effect of the case is shown in the figure

The overall execution process is as follows

Compared to the Metal Entry Level 02: Loading triangles case, there are two main changes

  • Render loop class: storage and transfer of large amounts of vertex data
  • Metal file: vertex coordinates need to be normalized to handle other view controllers and bridge between OC and C. No changes are made to the H file, please refer to the complete code at the end of this article

Render loop class

It is mainly to modify the storage and transfer of large quantities of vertex data, involving the following two functions

  • InitWithMetalKitView function: generates a large amount of vertex data and stores the data in buffer
  • DrawInMTKView proxy method: Vertex data is passed to the vertex shader throughsetVertexBuffer(_:offset:index:)methods

InitWithMetalKitView function

This function mainly initializes GPU devices and loads Metal, as shown in the figure

It is mainly divided into the following two steps

  • Initializes the device
  • The loadMetal function loads metal

Initialize Device through the MTKView object view passed in the view controller to initialize the Render object, using the view to obtain the permission to use the GPU

_device = mtkView.device;

Copy the code

LoadMetal function This function is mainly used to initialize the preparation of metal. Loading metal can be divided into steps

  • View sets the pixel format to draw the texture
mtkView.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;

Copy the code
  • Load. Metal files & vertex shader and chip shader functions

  • Create a render pipe and name & Set the vertices and pieces of the render pipe function & Set the component format for the color data

  • The above steps are basically necessary for drawing with Metal. They are described in the Introductory Metal example 02: Loading triangles

  • Get vertex data

    • throughgenerateVertexDataThe function generates a large volume of vertex data
    • The reason for using buffer is that the size of vertex data exceeds 4KB. If you use array storage, it will cost a lot of performance to transfer vertex data from CPU to GPU. Therefore, Apple official documents suggest that you need to create a buffer when there is a large amount of vertex dataMTLBufferObject to store data
    • Copy vertex data to the vertex cache through the cachecontentContent property access pointer
    • Calculate the total number of vertices by dividing the length of the vertex data by the size of the individual vertices
NSData *vertexData = [CJLRenderer generateVertexData]; / / create a vertex buffer that can be read by the GPU _vertexBuffer = [_device newBufferWithLength: vertexData. Length options:MTLResourceStorageModeShared]; // copy vertex data to vertex buffer /* memcpy(void * DST, const void * SRC, size_t n); DST: destination -- where to read SRC: source content -- where to read source data n: */ memcpy(_vertexbuffer.contents, vertexdata.bytes, vertexdata.length); // Count vertices = vertexdata. length/sizeof(CJLVertex);Copy the code
  • Create a commandQueue commandQueue using device
_commandQueue = [_device newCommandQueue];

Copy the code

DrawInMTKView proxy method

Set the view’s delegate object to render in the viewDidload function, which handles the view’s delegate method in the custom render loop class. The view’s default frame rate is consistent with the screen refresh rate, so whenever the screen is refreshed, MTKViewDelegate’s drawInMTKView ‘drawing method is called to render the graphics. The flow chart of drawInMTKView method is shown below

It is mainly divided into the following parts

  • Create command cache & name
  • Create render descriptor with renderPassDescriptor & determine if render descriptor is null
    • When the render descriptor is not empty, proceed
    • When the render descriptor is empty, jump directly to commit
  • Create commandEncoder commandEncoder
  • Set the viewport
  • Sets the render pipeline state
  • To transfer data
  • Drawing graphics
  • End the commandEncoder work
  • Render to screen by presentDrawable
  • Submit commandBuffer to the GPU

The above steps, except the data transfer part, are all necessary steps for graphic rendering, which are explained in the cases mentioned at the beginning of the article. The data transfer part is emphasized below

In init initialization, vertex data is stored using buffer, so vertex data is passed using the corresponding buffer transfer function setVertexBuffer. ViewpoertSize data can still be passed using setVertexBytes. So buffer and bytes passing can be mixed

function parameter
setVertexBytes:length:atIndex Parameter 1-bytes: points to the memory pointer passed to the shader
Parameter 2-length: The memory size of the data to be passed
Parameter 3-index: indicates the corresponding Index
setVertexBuffer:offset:atIndex Parameter 1-buffer: Buffer object to which data is to be passed
Parameter 2-offset: offset from the beginning byte of the buffer, usually passed 0
Parameter 3-atIndex: indicates the corresponding index
/ / we call - [MTLRenderCommandEncoder setVertexBuffer: offset: atIndex:] from our OC code for sending data to preload MTLBuffer to our Metal / * in the vertex shader function This call takes 3 arguments: 1) buffer - the buffer object containing the data to be passed and 2) offset - they are offset from the beginning byte of the buffer to indicate what the "vertex pointer" points to. In this case, we're passing 0, so the data is being passed down in the first place. Offset 3) index - An integer index corresponding to the index of the buffer property qualifier in our "vertexShader" function. Note that this parameter and - [MTLRenderCommandEncoder setVertexBytes: length: atIndex:] "index" parameter is the same. */ / set _vertexBuffer to the vertex buffer. Stored in the buffer [commandEncoder setVertexBuffer: _vertexBuffer offset: 0 atIndex: CJLVertexInputIndexVertices]; CommandEncoder setVertexBytes:&_viewportSize commandEncoder setVertexBytes:&_viewportSize length:sizeof(_viewportSize) atIndex:CJLVertexInputIndexViewportSize];Copy the code

Metal file

In the. Metal file, the vertex shader function needs to normalize the vertex coordinates because the vertex data is initialized using object coordinates. The normalization of vertex coordinates mainly includes the following steps:

  • Initializes the output clipping space position
  • Get xy of the current vertex coordinates: mainly because the graph is drawn in 2D and z is 0
  • Convert the incoming view size tovector_float2Two dimensional vector type
  • Vertex coordinate normalization: It is possible to separate two channels X and Y at the same time with a single line of code, perform division, and then put the result into the x and Y channels of the output, i.e. convert from a pixel space position to a clipped space position
vertex RasterizerData vertexShader(uint vertexID [[vertex_id]], constant CJLVertex *vertices [[buffer(CJLVertexInputIndexVertices)]], Constant vector_uint2 * viewportSizePointer [[buffer (CJLVertexInputIndexViewportSize)]]) {/ * processing vertex data: 2) Pass the vertex color value to the return value */ // 1) Define out RasterizerData out; Out. ClipSpacePosition = vector_float4(0.0, 0.0, 0.0, 1.0); // select * from xy; Float2 pixelSpacePosition = vertices[vertexID].position.xy; float2: vertices[vertexID].position.xy; // Convert vierportSizePointer from verctor_uint2 to vector_FLOAT2 viewportSize = vector_float2(*viewportSizePointer); // The output position of each vertex shader is in the clipping space (also known as the normalized device coordinate space (NDC)), where (-1,-1) represents the lower left corner of the viewport and (1,1) represents the upper right corner of the viewport. To convert from a position in pixel space to a position in clip space, we divide the pixel coordinates by half the size of the viewport. Out.clipspaceposition.xy = pixelSpacePosition/(viewportSize / 2.0); // Assign the input color directly to the output color. Color = vertexID; color = vertexID; color = vertexID; color = vertexID; color = vertexID; / / finish! Pass the structure to the next stage in the pipe: return out; }Copy the code

conclusion

Vertex data can be stored in two ways

  • throughAn array ofStored in theCPU, need to make use ofsetVertexBytes:length:atIndex:The function passes vertex data toGPUThis case is only suitableLess than 4 KBVertex data of
  • When vertex dataMore than 4 KB, the vertex data needs to be stored inMTLBufferObjects, that is,Vertex cacheIn, this storage modeThe GPU can read directlyIs passed to the shader functionsetVertexBuffer: offset: atIndex:function

See github-18_metal_ triangle _OC for the complete code