Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metal: Fix crash when uniform set is empty for classic binding mode #100766

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

stuartcarnie
Copy link
Contributor

@stuartcarnie stuartcarnie commented Dec 23, 2024

Closes #100764

Resolves a CRASH_BAD_INDEX error when binding uniform sets that are empty, specifically for A12 devices and earlier, which use classic ABI vs argument buffers.

Resolves a Metal CPU API validation error when binding "empty" particle buffers, that were incorrectly sized. Metal doesn't support defining a struct with an empty array.

Overview

An example of this in GLSL is:

struct ParticleEmission {
  vec3 velocity;
  uint flags;
};

layout(set = 1, binding = 2, std430) restrict buffer SourceEmission {
  int particle_count;
  uint pad0;
  uint pad1;
  uint pad2;
  ParticleEmission data[];
}
src_particles;

The minimum size of SourceEmission is 16 bytes when binding a buffer for backends that support empty arrays.

However, when this is converted to Metal, note the data member is an array of size 1:

struct ParticleEmission {
  float3 velocity;
  uint flags;
};

struct SourceEmission {
  int particle_count;
  uint pad0;
  uint pad1;
  uint pad2;
  ParticleEmission data[1];
};

This means that the minimum size of the struct when binding an "empty" array is 32 bytes. This doesn't cause an issue when Metal API validation is disabled, but this feature is often enabled when running code from Xcode, which iPhone developers are likely to do.

@@ -1082,7 +1083,7 @@

UniformSet const &set = p_shader->sets[index];

for (uint32_t i = 0; i < uniforms.size(); i++) {
for (uint32_t i = 0; i < std::min(uniforms.size(), set.uniforms.size()); i++) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
for (uint32_t i = 0; i < std::min(uniforms.size(), set.uniforms.size()); i++) {
for (uint32_t i = 0; i < MIN(uniforms.size(), set.uniforms.size()); i++) {

This should use our math library instead of std

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was having trouble, as NSObjCRuntime.h defines them. I've #undef'd them, so it uses Godot's constexpr functions now.

Copy link
Member

Choose a reason for hiding this comment

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

That's weird, as core/typedefs.h which defines our MIN and MAX also undef's the pre-existing ones.

Maybe you need an explicit core/typedefs.h include here? It's kind of taken for granted usually as most Godot headers include it one way or another, but maybe this specific file of the Metal driver doesn't as it's relatively self-contained?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, I'd have to include typedefs.h before the system libraries, and then NSObjCRuntime.h re-defines it again. Quite a nuisance!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what it looks like:

#if !defined(MIN)
    #define __NSMIN_IMPL__(A,B,L) ({ __typeof__(A) __NSX_PASTE__(__a,L) = (A); __typeof__(B) __NSX_PASTE__(__b,L) = (B); (__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__a,L) : __NSX_PASTE__(__b,L); })
    #define MIN(A,B) __NSMIN_IMPL__(A,B,__COUNTER__)
#endif

Copy link
Member

@Calinou Calinou left a comment

Choose a reason for hiding this comment

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

Code looks good to me.

Comment on lines +544 to +546
// For rendering devices that do not support empty arrays (like C++),
// we need to size the buffer with at least 1 element.
particles->unused_emission_storage_buffer = RD::get_singleton()->storage_buffer_create(sizeof(ParticleEmissionBuffer));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm creating an empty buffer with enough data for one element, which is required for backends, like Metal, that don't support empty arrays in struct definitions, similar to C++.

}
}

void ParticlesStorage::_particles_ensure_unused_trail_buffer(Particles *particles) {
if (particles->unused_trail_storage_buffer.is_null()) {
particles->unused_trail_storage_buffer = RD::get_singleton()->storage_buffer_create(sizeof(uint32_t) * 4);
particles->unused_trail_storage_buffer = RD::get_singleton()->storage_buffer_create(16 * sizeof(float)); // Size of mat4.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was incorrectly sized, so was causing validation errors when using Metal's slot-based binding API (A12 devices)

@stuartcarnie
Copy link
Contributor Author

@TCROC can you try this, as it should resolve your issues

@TCROC
Copy link
Contributor

TCROC commented Dec 24, 2024

@TCROC can you try this, as it should resolve your issues

@stuartcarnie Yep. I'll merge it into my fork now and let you know how it goes :).

@TCROC
Copy link
Contributor

TCROC commented Dec 24, 2024

@stuartcarnie I can confirm, this PR appears to fix the issue! Thank you for the quick response and patch! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Metal: Error compiling compute shader on iPhone A12
6 participants