Skip to content

Implement bindless buffers on metal#9081

Open
mate-h wants to merge 4 commits intogfx-rs:trunkfrom
mate-h:m/bindless-buffer-metal
Open

Implement bindless buffers on metal#9081
mate-h wants to merge 4 commits intogfx-rs:trunkfrom
mate-h:m/bindless-buffer-metal

Conversation

@mate-h
Copy link

@mate-h mate-h commented Feb 20, 2026

Connections
Resolves #6741

Description
Problem: Metal on macOS did not support bindless storage buffers.

In wgpu-hal, encode buffer binding arrays using MTLArgumentEncoder. Metal expects encoded pointers, not raw resource IDs. Creates an argument buffer from an encoder, encode each buffer, and register it in resources_to_use.

In naga MSL writer, emit device T* for buffer binding array elements (which are pointers to device memory) instead of the constant layout used for textures and samplers. For struct members behind binding arrays, emit -> instead of . because the elements are pointers.

In the adapter, expose the BUFFER_BINDING_ARRAY feature and raise max_buffers_per_stage on supporting devices, with special handling for MTLCaptureDevice. This is a separate fix for Xcode frame capture, when some functions fail to invoke, in these cases fallback to feature table based limits. I referenced the metal feature table PDF.

Testing

Tested with Bevy Solari on metal / macOS which makes use of bindless storage buffers and raytracing acceleration structures.

Screenshot 2026-02-20 at 12 06 13 AM

Squash

Checklist

  • Run cargo fmt.
  • Run taplo format.
  • Run cargo clippy --tests. If applicable, add:
    • --target wasm32-unknown-unknown
  • Run cargo xtask test to run tests.
  • If this contains user-facing changes, add a CHANGELOG.md entry.

@inner-daemons inner-daemons self-assigned this Feb 20, 2026
@inner-daemons
Copy link
Collaborator

I'm excited to see this and plan on checking out the code. Thanks a ton for working on it!

Copy link
Contributor

@Vecvec Vecvec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of things from a read through.

Copy link
Collaborator

@inner-daemons inner-daemons left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so lots of thoughts here, mostly questions. Overall looks good. More comments would be great.

Big thing though is snapshot tests. I can't really review this change until I know what it generates and know that what it generates is correct. There might even be existing tests that you can just add METAL as a target to.

Thanks for putting in the work. I look forward to using this myself, and I'd like to get this landed soon!


- Use autogenerated `objc2` bindings internally, which should resolve a lot of leaks and unsoundness. By @madsmtm in [#5641](https://github.com/gfx-rs/wgpu/pull/5641).
- Implements ray-tracing acceleration structures for metal backend. By @lichtso in [#8071](https://github.com/gfx-rs/wgpu/pull/8071).
- Added support for bindless storage buffers (buffer binding arrays) on Metal. By @mate-h in [#9081](https://github.com/gfx-rs/wgpu/pull/9081).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny nit but maybe put this under "new features" rather than "changes"?

crate::TypeInner::Struct { .. } => {
write!(
out,
"device {ARGUMENT_BUFFER_WRAPPER_STRUCT}<device {base_tyname}*>*"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining this special case

crate::Expression::FunctionArgument(_) => return None,
// There are no other expressions that produce pointer values.
_ => unreachable!(),
_ => return None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

{
Some(device.hasUnifiedMemory())
// On Xcode frame capture this call causes a crash, fall back to feature table.
if Self::is_capture_mtl_device(device) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this might belong in another PR but its fine here IMO. Anyway, why Apple6 specifically? And tbh, it'd be fine to just report None here if we don't know for sure

);
features.set(
F::BUFFER_BINDING_ARRAY,
self.msl_version >= MTLLanguageVersion::Version3_0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find any reason why this shouldn't be 2.0? Though I guess you copied the above so that should also be changed.

Also, I believe this might impact Features::PARTIALLY_BOUND_BINDING_ARRAY?

Comment on lines +369 to +376
wgt::BufferBindingType::Uniform => MTLResourceUsage::Read,
wgt::BufferBindingType::Storage { read_only } => {
if *read_only {
MTLResourceUsage::Read
} else {
MTLResourceUsage::Read | MTLResourceUsage::Write
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
wgt::BufferBindingType::Uniform => MTLResourceUsage::Read,
wgt::BufferBindingType::Storage { read_only } => {
if *read_only {
MTLResourceUsage::Read
} else {
MTLResourceUsage::Read | MTLResourceUsage::Write
}
}
wgt::BufferBindingType::Uniform | wgt::BufferBindingType::Storage { read_only: true }=> MTLResourceUsage::Read,
wgt::BufferBindingType::Storage { read_only: false }=> MTLResourceUsage::Write,

Comment on lines +384 to +391
wgt::BufferBindingType::Uniform => MTLBindingAccess::ReadOnly,
wgt::BufferBindingType::Storage { read_only } => {
if *read_only {
MTLBindingAccess::ReadOnly
} else {
MTLBindingAccess::ReadWrite
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
wgt::BufferBindingType::Uniform => MTLBindingAccess::ReadOnly,
wgt::BufferBindingType::Storage { read_only } => {
if *read_only {
MTLBindingAccess::ReadOnly
} else {
MTLBindingAccess::ReadWrite
}
}
wgt::BufferBindingType::Uniform | wgt::BufferBindingType::Storage { read_only: true } => MTLBindingAccess::ReadOnly,
wgt::BufferBindingType::Storage { read_only: false } => MTLBindingAccess::ReadWrite,

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will definitely need some snapshot tests here. You can read through naga/tests/in/wgsl to get a good idea of how to create these.

Comment on lines +972 to +973
let buffers = &desc.buffers[entry.resource_index as usize..]
[..count as usize];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let buffers = &desc.buffers[entry.resource_index as usize..]
[..count as usize];
let buffers = &desc.buffers[entry.resource_index as usize..(entry.resource_index + count) as usize];

Comment on lines +975 to +995
let argument_desc = conv::pointer_array_argument_descriptor(
count,
conv::map_binding_access(&ty),
);

let encoder = device
.newArgumentEncoderWithArguments(&NSArray::from_retained_slice(
&[argument_desc],
))
.unwrap();
let aligned_length = wgt::math::align_to(
encoder.encodedLength(),
encoder.alignment(),
);
argument_buffer = device
.newBufferWithLength_options(
aligned_length,
MTLResourceOptions::HazardTrackingModeUntracked
| MTLResourceOptions::StorageModeShared,
)
.unwrap();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire section of code (and everything aroudn it) needs more comments. I don't fully understand why we're creating buffers, not putting them anywhere, etc.

@inner-daemons inner-daemons removed their assignment Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support binding_array of Storage Buffers on Metal

3 participants