Accessing data in argument buffers when the first field is [[id(n)]]

I’m having trouble with binding an argument buffer. I’m using spirv_cross to produce MSL from GLSL so my shader setup is a bit unusual and I’m not sure if what I’m trying to do is supported or not.

If I set up the argument buffer like this:

Code Block
struct spvDescriptorSetBuffer0
{
    /* used by the vertex function only */
    constant ArgsTransform* transform [[id(0)]];
    /* used by the fragment function only */
    constant Args* uniform_buffer [[id(1)]];
};

The shader reads the bound values correctly. 

However, I have separate vertex and fragment shader MSL files (because I generate from GLSL using spirv_cross). The MSL for the arg buffer looks like this:

Separate vertex MSL file
Code Block
struct spvDescriptorSetBuffer0
{
    constant ArgsTransform* transform [[id(0)]];
};

Separate fragment MSL file
Code Block
struct spvDescriptorSetBuffer0
{
    constant Args* uniform_buffer [[id(1)]];
};

In this case, when the fragment shader reads uniform_buffer, which is [[id(1)]], it gets the “transform” data that was bound at index 0 (that was set by calling setBuffer(_:offset:index:), passing 0 to index.

Adding constant ArgsTransform* transform [[id(0)]]; to that struct in the fragment MSL file makes uniform_buffer return expected values.

Is this the intended behavior? The documentation for the function is here: https://developer.apple.com/documentation/metal/mtlargumentencoder/2915785-setbuffer

The index of the buffer within the argument buffer. This value corresponds to either the index ID of a Metal shading language declaration or the index field of a MTLArgumentDescriptor object.

The docs say "either" - it's not clear to me which one is supposed to take effect here. Either way, I think I am setting BOTH the "index ID of a MSL declaration" and the "index field of MTLArgumentDescriptor"

My argument descriptors for the arg buffer are here:
Code Block
<MTLArgumentDescriptorInternal: 0x122670cc0>
dataType = MTLDataTypePointer
index = 0
arrayLength = 0
access = MTLArgumentAccessReadOnly
textureType = MTLTextureType2D
constantBlockAlignment = default,
<MTLArgumentDescriptorInternal: 0x122670da0>
dataType = MTLDataTypePointer
index = 1
arrayLength = 0
access = MTLArgumentAccessReadOnly
textureType = MTLTextureType2D
constantBlockAlignment = default,

I was hoping to make this setup work because if it did, then I could treat argument buffers like vulkan descriptor sets.
Answered by Graphics and Games Engineer in 663780022
There are a few things going on here.

Using a MTLArgumentDescriptor to specify types and indices in an argument buffer is a complementary method to using [[id(n)]] to do the same thing. In general, you shouldn't use both.

  1. You can specify types and indices using the [[id(n)]] attribute qualifier and create a MTLArgumentEncoder with -[MTLFunction newArgumentEncoderWithBufferIndex:]. This method is more flexible in that it allow you to specify types and indices for complex nested structures. We have a couple of samples showing how to use this method (such as this one)

  2. You can use an MTLArgumentDescriptor to create a MTLArgumentEncoder with -[MTLDevice newArgumentEncoderWithArguments:]. This method is more flexible in that it allows you to specify types and indices in your host code (i.e. your Rust code in this case). However, you can only specify types or indices for flat (non-nested) structures this way. Also, this method is more error prone because you can mistakenly specify a type with a the argument descriptor that doesn't match the type as it's used in your shader.

So use one of these methods, not both. (There are rules so that you could to use both, but unless you really need to for some quirk of your apps's architecture, it will only make your code more confusing)

Another problem is that you're trying you use a single argument encoder for multiple arguments. This will only work if both arguments are the same type, but your spvDescriptorSetBuffer0 structures are different types (that happened to be named the same).

So create separate argument encoders for your vertex and fragment shader buffers.

It shouldn't matter that they're in separate files. Each buffer parameter to a function has a separate "id space". That is to say if you have two buffer parameters in a function both can have a buffer arguments with an id(0). Likewise if you have a vertex and a fragment shader and both can have a buffer argument with an id(0) and these are independent.

I'm not sure what's going on. Can post you the vertex and fragment shader function headers?
Full shaders here, please see the comment in the second file. Thanks for looking at this! I can post the full single-file repro but it is in rust.)

triangle.vert.metal


Code Block
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct ArgsTransform
{
float4x4 transform;
};
struct spvDescriptorSetBuffer0
{
constant ArgsTransform* uniform_buffer [[id(0)]];
};
struct main0_out
{
float4 position [[position]];
};
struct main0_in
{
float2 in_pos [[attribute(0)]];
float3 in_color [[attribute(1)]];
};
vertex main0_out main0(main0_in in [[stage_in]], constant spvDescriptorSetBuffer0& spvDescriptorSet0 [[buffer(0)]])
{
main0_out out = {};
out.position = (*spvDescriptorSet0.uniform_buffer).transform * float4(in.in_pos, 0.0, 1.0);
return out;
}


triangle.frag.metal


Code Block
#include <metal_stdlib>
#include <simd/simd.h>
using namespace metal;
struct ArgsTransform
{
float4x4 transform;
};
struct Args
{
float4 color;
};
struct spvDescriptorSetBuffer0
{
/*
If the [[id(0)]] field is commented out, uniform_buffer returns the value
that was bound to [[id(0)]] instead of [[id(1)]] and the triangle does not
draw because the color is incorrect (0 alpha)
*/
/* constant ArgsTransform* transform [[id(0)]]; */
constant Args* uniform_buffer [[id(1)]];
};
struct main0_out
{
float4 out_color [[color(0)]];
};
struct main0_in
{
};
fragment main0_out main0(main0_in in [[stage_in]], constant spvDescriptorSetBuffer0& spvDescriptorSet0 [[buffer(0)]])
{
main0_out out = {};
out.out_color = spvDescriptorSet0.uniform_buffer->color;
return out;
}


Excerpts from code


This is in rust, sorry :D.

Code Block
//
// Setup argument buffer
//
let desc0 = ArgumentDescriptor::new();
desc0.set_data_type(MTLDataType::Pointer);
desc0.set_index(0);
let desc1 = ArgumentDescriptor::new();
desc1.set_data_type(MTLDataType::Pointer);
desc1.set_index(1);
println!("args: {:#?}", [&desc0, &desc1]);
let args = Array::from_slice(&[desc0, desc1]);
let encoder = device.new_argument_encoder(&args);
let argument_buffer = device.new_buffer(encoder.encoded_length(), MTLResourceOptions::empty());
encoder.set_argument_buffer(&argument_buffer, 0);
encoder.set_buffer(0, &transform_buffer, 0);
encoder.set_buffer(1, &color_buffer, 0);


Code Block
// Bind argument buffer to vertex and fragment shaders
encoder.set_vertex_buffer(0, Some(&argument_buffer), 0);
encoder.set_fragment_buffer(0, Some(&argument_buffer), 0);
encoder.use_resource(&transform_buffer, MTLResourceUsage::Read);
encoder.use_resource(&color_buffer, MTLResourceUsage::Read);
encoder.draw_primitives(MTLPrimitiveType::Triangle, 0, 3);

Accepted Answer
There are a few things going on here.

Using a MTLArgumentDescriptor to specify types and indices in an argument buffer is a complementary method to using [[id(n)]] to do the same thing. In general, you shouldn't use both.

  1. You can specify types and indices using the [[id(n)]] attribute qualifier and create a MTLArgumentEncoder with -[MTLFunction newArgumentEncoderWithBufferIndex:]. This method is more flexible in that it allow you to specify types and indices for complex nested structures. We have a couple of samples showing how to use this method (such as this one)

  2. You can use an MTLArgumentDescriptor to create a MTLArgumentEncoder with -[MTLDevice newArgumentEncoderWithArguments:]. This method is more flexible in that it allows you to specify types and indices in your host code (i.e. your Rust code in this case). However, you can only specify types or indices for flat (non-nested) structures this way. Also, this method is more error prone because you can mistakenly specify a type with a the argument descriptor that doesn't match the type as it's used in your shader.

So use one of these methods, not both. (There are rules so that you could to use both, but unless you really need to for some quirk of your apps's architecture, it will only make your code more confusing)

Another problem is that you're trying you use a single argument encoder for multiple arguments. This will only work if both arguments are the same type, but your spvDescriptorSetBuffer0 structures are different types (that happened to be named the same).

So create separate argument encoders for your vertex and fragment shader buffers.

Thanks for the information! I was able to use an option in spirv_cross "--msl-force-active-argument-buffer-resources" to ensure the same type is emitted for the vertex and fragment files. This fixes the case mentioned above.

I think in my case, I'm better off using the second approach. I don't have nested structures in argument buffers because I need to stay compatible with vulkan and other graphics APIs. Additionally the first approach would still require me to pass the "correct" indices when setting the values in functions like MTLArgumentEncoder's setBuffer/setTexture so it still requires me to use reflection data (which I already generate offline).

I'm actually not sure if there is a way to force spirv_cross to omit the [[id(n)]] attributes. So it would be helpful to know if there are rules I need to follow. It seems like the most important thing is to include the same fields in the same order in all shader stages and in the host code. I noticed that even if I use different IDs in the vertex MSL, fragment MSL, and my host application code, as long as the fields are the same order and nothing is omitted, the triangle renders fine. (Not that I would intentionally "ship" like that :D)

Thanks again for answering, it's been very helpful!

Accessing data in argument buffers when the first field is [[id(n)]]
 
 
Q