Tips on Implementing Vulkan & Metal Layer

This article was written in 2018, some of the reference might be outdated and a lot has been improved in the API, but the insights should still apply.

This is a record of pitfalls I had encountered and things I wish I had paid more attention when I am implementing the Vulkan and Metal layer for Android and iOS mobile devices, for a rendering framework that have already been running on Windows, PS4 and Switch.

1. Always look at the spec

Iterally check the spec the first time we call a function from the API, don’t make any assumption.

In Vulkan, all create info’s pNext must be initialized as null. This removes a lot of crashes that the validation layer can not tell. The spec has written this very clearly under every create info struct description, so just follow the spec.

D3D12 Vulkan Metal
void DrawIndexedInstanced(
[in] UINT IndexCountPerInstance,
[in] UINT InstanceCount,
[in] UINT StartIndexLocation,
[in] INT BaseVertexLocation,
[in] UINT StartInstanceLocation);
void vkCmdDrawIndexed(
VkCommandBuffer commandBuffer,
uint32_t indexCount,
uint32_t instanceCount,
uint32_t firstIndex,
int32_t vertexOffset,
uint32_t firstInstance);
func drawIndexedPrimitives(
primitiveType: MTLPrimitiveType,
indexCount: Int,
indexType: MTLIndexType,
indexBuffer: MTLBuffer,
indexBufferOffset: Int,
instanceCount: Int,
baseVertex: Int,
baseInstance: Int)

To specify which index to begin with drawing, D3D12(StartIndexLocation) and Vulkan(firstIndex) simply use an offset, Metal(indexBufferOffset) uses a byte offset. Metal is the different one here. By checking the Metal spec, it is stated that indexBufferOffset is the Byte offset within index buffer to start reading indices from, so comparing to the other two APIs, it is StartIndexLocation/firstIndex * 2 or 4 (16 bit or 32 bit index).

This bug is easy to spot and solve, but avoidable at the first place if we can just go to the spec and have a simple look before calling the function. The name of the variable also gives some hints, instead of index it uses offset.

2. Identify Differences at the very beginning

Identifing similarity between APIs at the very beginning makes implementation of new layers more efficient. But the deal breaker is identifying differences, then we can know where to slow down and pay extra attention.

Also identify the differences between porting a game and implementing a graphics layer in an engine. Almost all “Vulkan is easier than you think” presentation we found online will not come into recuse. Although Metal comes with many code sample, obviously sample code will not help much too as well. Sometime maybe we need a rewrite in order to make one feature on a specific API portable in all other APIs. For example some APIs require the information of render targets in order to start recording command into command list. And such as Metal only provides shader reflection data at pipeline object construction time, so if our engine depends on shader reflection we might need to find another way to pregenerate those, maybe have a proxy runtime as the shader compiler.

3. Test on multiple devices

The open source nature of Vulkan means there is possibility that gpu manufacturers miss or wrongly implement certain feature. Especially this is the era when every month there might be more than ten new mobile devices out on the market. And new manufacturers are emerging as well.

My real life experience: a driver bug on Adreno gpu decided to turn my constant buffer into null in a compute shader, which works totally fine on other devices.

4. We are developing APPS as well

Implementing graphics layer for mobile platforms is not like implementing for game consoles. Some elements are coupled to the front-end of the app, so we need to know how app development works as well. Noticeably, on iOS:

5. Unknown gpu error usually means shader/call parameter error

With explicit APIs validation layers are very important during development, because there are too many places we can make mistakes. Unknown gpu error could not be caught by the validation layer usually means the problematic logic solely running on the gpu. Those error messages give no information and usually causes a crash or hang. Here is a simple list of what I have experienced:

When having unknown gpu error, removing instructions in shader one by one so the frame capture debugger works would help a lot.

My Feeling

D3D12

They are the first into the market of low level explicit graphics API. So I think there is some less intuitive way to achieve certain features that have been improved to simpler method on Vulkan and Metal. For example, all different kinds of buffers in D3D12 are translated to almost one single type of “Storage Buffer” in Vulkan

Vulkan

Most things make sense. But just as any other open source API or library, it is a bit troublesome to setup everything, such as validation, frame capture, debugging, shader compilatio,n because they usually don’t come in one package. The extension system is especially awesome. New features that hardware vendor experimenting or implemented can be distributed through extension. For example, raytracing and mesh shader are first distributed in Vulkan with Nvidia’s extension.

Metal

I think Metal tries to position itself half way between high and low level. As always Apple likes to create their own ecosystem, for example dumping domain and hull shader and replaces it with some special forms of vertex shader. And dumping geometry shader completely and recommend using compute shader instead. Most of the decisions themselves are actually good and make sense, I think partly because Apple usually introduces the same feature slower than the other APIs and they are more flexible with backward compatibility. And their debugging and profiling tools are easy to use, also very convenient when everything comes in one package.

© 2018 Norris Chiu; Theme by mattgraham