Skip to content

Conversation

@Eideren
Copy link
Collaborator

@Eideren Eideren commented Aug 7, 2025

PR Details

Adding in an API to read mesh data efficiently.

Why

A vertex buffer, like its name implies, contain the vertices of a mesh.
Vertex buffers do not have a standardized layout, each mesh may have its own layout and data type it uses for their vertices. Some have blend weights, or tangents, while others only have positions - they may also use different data types, for example Half4 positions, 4byte color ...

All of those permutations and subtleties make reading a mesh's vertex buffer tedious and error prone.

Index buffers are significantly easier, but still easy to mess up given that they may be defined as a 16bit or 32bit indices and that buffers may or may not use the whole data - they have offset and lengths.

There's also the lack of documentation around the fact that vertex buffers are not kept in cpu memory, they are uploaded as soon as they are deserialized. Reading it requires loading it from disk or the gpu, both of which are definitely not straightforward.

Example

Quick example for the most common method:

Model.Meshes[0].Draw.VertexBuffers[0].AsReadable(Services, out VertexBufferHelper helper, out int count);
var vertexPositions = new Vector3[count];
var textureCoordinates = new Vector2[count];
// Copies the vertex positions of this mesh into the array provided
// The position's data type will be automatically converted into a Vector3 if needed
helper.Copy<PositionSemantic, Vector3>(vertexPositions);
helper.Copy<TextureCoordinateSemantic, Vector2>(textureCoordinates); // Copies the vertex texture coordinates

The semantic generic used there informs the API as to what it should read from the vertices, but it also helps in preventing user error; they enforce the use of the right type for a given semantic - TextureCoordinateSemantic only accepts Vector2 as the span type, providing anything else would show a syntax error.

There is also a Read<> method which provides user with a direct access to the buffer and all the tools they may need to deal with the issues outlined above. When they have other needs than just a dumb copy.

There are other examples baked into the documentation for the API.

Related Issue

None

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
    • improves support for non-standard mesh layout with physics collision
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have built and run the editor to try this change out.

@Eideren Eideren marked this pull request as draft August 29, 2025 11:06
@Eideren
Copy link
Collaborator Author

Eideren commented Sep 2, 2025

I'm done, added a helper to write to the buffer directly regardless of the data type it is made up of;

Model.Meshes[0].Draw.VertexBuffers[0].AsReadable(Services, out VertexBufferHelper helper, out int count);
// Write to colors if that semantic already exist in the buffer, otherwise returns false
helper.Write<ColorSemantic, Vector4, MultColor>(new MultColor(){ Color = Color.Gray });
// Upload changes to the GPU
Model.Meshes[0].Draw.VertexBuffers[0].Buffer.Recreate(helper.DataOuter);

private struct MultColor : VertexBufferHelper.IWriter<Vector4>
{
   public Color Color;

   public unsafe void Write<TConversion, TSource>(byte* sourcePointer, int elementCount, int stride)
       where TConversion : IConversion<TSource, Vector4>, IConversion<Vector4, TSource>
       where TSource : unmanaged
   {
       for (byte* end = sourcePointer + elementCount * stride; sourcePointer < end; sourcePointer += stride)
       {
           TConversion.Convert(*(TSource*)sourcePointer, out var val);
           val *= (Vector4)Color;
           TConversion.Convert(val, out *(TSource*)sourcePointer);
       }
   }
}

if someone could take a look at this one it would be great.

@Eideren Eideren marked this pull request as ready for review September 2, 2025 15:59
Comment on lines 168 to 176
public struct UShort4(ushort x, ushort y, ushort z, ushort w)
{
public ushort X = x, Y = y, Z = z, W = w;
}

public struct Byte4(byte x, byte y, byte z, byte w)
{
public byte X = x, Y = y, Z = z, W = w;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe these special vector types are better in another namespace.

I don't think the new Stride.Graphics.Semantic is appropriate, as these types can be used for data packing apart from shader/vertex semantics.

The Stride.Mathematics would be the first option, but as they are not math types (like vectors) I've been thinking if it would make sense to have a namespace for these types of packed vectors, like it existed in XNA (and now MonoGame). Look here for an example.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a very good idea, do you think I should mirror the pixel format naming scheme for those types then ?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's an idea I had for a future PR, but if we start it here the better. I (or anyone that wants to contribute) can add the remaining packed vector types that make sense later.

But for now, I think for simple types without much packing restrictions, Byte4 is more expressive in what it stores than Rgba32 or whatever it is called in MonoGame.

Do you think it would make sense to have a Byte4 and later a R8G8B8A8 and have easy conversions between the two? Or better deciding on a single name and documenting it or letting the user discover the type she needs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I can see having those two type living in the same namespace be a bit awkward - Maybe we could introduce R8G8B8A8 inside some new Stride.Graphics.Format namespace and have its analogous type, Byte4, inside Stride.Core.Mathematics regardless - we do have some weirder types like Size3 after all.
I'm saying this but I'm definitely not convinced of this suggestion either though, I would actually rather you tell me what you would rather have and I'll just adopt it as I don't think I ultimately have a strong grasp on if any of our propositions would be preferable from a graphics programmer's perspective.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd like to separate the mathematical concepts (vectors, matrices, planes, etc.) from the data type / memory concepts. Here, Size3 would be a mathematical concept (a vector of three dimensions specifying a size. The weird thing is that it consists of just integers, so it is a size for arrays, matrices, textures, etc.), while I see Byte4 as a data/memory type (a type consisting of 4 bytes, with a very specific size, padding, etc.)

For reference, that was also what XNA (and now MonoGame) did, mathematical types in the base namespace, and the "packed vectors" were just special types for when you need to operate on the data of textures, vertex buffers, etc. That's why they were in a separate namespace.

Taking this into account, I vote to have them in a separate namespace. First, so it not polutes the Intellisense with weirdly named types, and second, so people that uses those types know that they are importing them from a specialized namespace, for specific uses.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, and another thing:
The Stride.Core.Mathematics package can be used independently, and it should contain only math-related things.
And I see some of those packed vector types as more graphics-interop-related (specially the ones mirroring pixel formats), so another reason to have them into its own namespace nested in Stride.Graphics.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done, moved them to Stride.Graphics.Interop, let me know what you think

Copy link
Contributor

@Ethereal77 Ethereal77 left a comment

Choose a reason for hiding this comment

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

Impressive! This has the potential to ease the management of indices and vertices by a lot. Nice job! 👏

Left some comments to discuss.

@Eideren
Copy link
Collaborator Author

Eideren commented Sep 8, 2025

Tried to reduce the amount of boilerplate required to introduce new types for interop but the JIT doesn't handle it well enough, SharpLab refuses to compile it:
https://sharplab.io/#v2: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

We could revisit this whenever the JIT improves.

@Ethereal77
Copy link
Contributor

Sorry for the delay. I'm quite busy lately.

I like what you are trying to do here, Eideren. I feel like it is abusing the type system a bit however 😁. I know Norbo does similar things in Bepu.

I have some comments:

  • I see you have created an interface for unsigned-normalized (UNorm) vectors, but just for those with four components. Why?

  • What about SNorm vectors or values?

  • I think value normalization is an orthogonal concept to data type. Could be decoupled somehow from the data type?
    Just thinking out loud. I'm also thinking about it, but can't think of a non-convoluted way to do it.

  • I see you have defined a Color semantic, but it is fixed to a four-component vector. Sometimes you only need to define the
    color's RGB values and no alpha. In those cases I've sometimes used a Vector3. Also, I've seen sometimes custom renderings
    that used other colorspaces with just two components (quite rare however), for example, encoding just the chrominance of
    the color (the CgCr of a YCgCr color).

  • What about packed vectors with weird bitsizes? For example, R10G10B10A2, or R11G11B10. I'm not saying we should support
    any possible combination or weird data type. Just asking out of curiosity.

As an aside, what do you use to check the JIT assembly (apart from SharpLab)? The snippet of the comment above fails because
of a limitation of SharpLab (I suppose to avoid excessive computation). Locally, it compiles fine, but I don't know what
to use to check the assembly. I used in the past an utility named JitPad, but it is unmantained and not updated since several
years ago.

@Eideren
Copy link
Collaborator Author

Eideren commented Sep 17, 2025

I see you have created an interface for unsigned-normalized (UNorm) vectors, but just for those with four components. Why?
What about SNorm vectors or values?
[...]
What about packed vectors with weird bitsizes? For example, R10G10B10A2, or R11G11B10. I'm not saying we should support any possible combination or weird data type. Just asking out of curiosity.

Right now I'm just trying to cover the most common types, UNorm4Byte is what assimp spits out when your model comes with vertex color from what I can tell.
We could cover every type, but with how much boilerplate I'm already introducing in the few conversions we have already, I would rather we keep things lean until the JIT improves.

I think value normalization is an orthogonal concept to data type. Could be decoupled somehow from the data type?
Just thinking out loud. I'm also thinking about it, but can't think of a non-convoluted way to do it.

That would be nice but the issue is that they aren't interpreted in the same way, a byte4 is clearly a integer type, whereas a UNormByte is treated like a floating point number. It's kind of a more compressed half. The user should not have to deal with that since we're automatically converting the type for them behind the scene for all the other kinds of types.

I see you have defined a Color semantic

This was supposed to reflect how stride interprets the Color semantic;

private static PixelFormat ConvertTypeToFormat(Type typeT)
{
if (typeof(Vector4) == typeT || typeof(Color4) == typeT)
return PixelFormat.R32G32B32A32_Float;
if (typeof(Vector3) == typeT || typeof(Color3) == typeT)
return PixelFormat.R32G32B32_Float;
if (typeof(Vector2) == typeT)
return PixelFormat.R32G32_Float;
if (typeof(float) == typeT)
return PixelFormat.R32_Float;
if (typeof(Color) == typeT)
return PixelFormat.R8G8B8A8_UNorm;
if (typeof(ColorBGRA) == typeT)
return PixelFormat.B8G8R8A8_UNorm;
if (typeof(Half4) == typeT)
return PixelFormat.R16G16B16A16_Float;
if (typeof(Half2) == typeT)
return PixelFormat.R16G16_Float;
if (typeof(Half) == typeT)
return PixelFormat.R16_Float;
if (typeof(Int4) == typeT)
return PixelFormat.R32G32B32A32_UInt;
if (typeof(Int3) == typeT)
return PixelFormat.R32G32B32_UInt;
if (typeof(int) == typeT)
return PixelFormat.R32_UInt;
if (typeof(uint) == typeT)
return PixelFormat.R32_UInt;
//if (typeof(Bool4) == typeT)
// return PixelFormat.R32G32B32A32_UInt;
//if (typeof(Bool) == typeT)
// return PixelFormat.R32_UInt;
throw new NotSupportedException(string.Format("Type [{0}] is not supported. You must specify an explicit DXGI.Format", typeT.Name));
}

But looks like I confused Color4 with Color, I'll correct this one tomorrow.

what do you use to check the JIT assembly (apart from SharpLab)

You can use the native dotnet one or Disasmo if you're lazy, both of them require a fairly straightforward solution, which stride's isn't, so your best bet is to create a temporary solution with the logic you want to check out.

@Eideren
Copy link
Collaborator Author

Eideren commented Sep 17, 2025

Actually, godbolt seems like it has improved since the last time I tried it. If I force inline the generic call into the caller it does significantly improve the codegen. If you're not against the example I've shown above I think I can make it work.

@Ethereal77
Copy link
Contributor

Right now I'm just trying to cover the most common types, UNorm4Byte is what assimp spits out when your model comes with vertex color from what I can tell.

But it is a 32-bit type with 4 bytes. It's just the way you can interpret those values as unsigned and normalized, not the internal representation. Look at how we do in Stride.Core.Mathematics.Color today. It's also a 32-bit 4-byte type, but internally normalizes/unnormalizes float values for some computations, and to interop with Color3 and Color4. I think Half does even more packing magic.

Right now I'm just trying to cover the most common types,

For a first version I think that's sensible. Even though it would be nice to use this for all kind of packed values (pixels, vertices, etc.), I would prefer to expand it later if needed than making a convoluted mess now just to support everything and the kitchen sink 😀

If you're not against the example I've shown above I think I can make it work.

In your example, I assume SingleVector is just an example of what would be those "packed types"? Like UNormByte4?

I ask because I think I'm not getting why you need to have both Vector4 and SingleVector, and why SingleVector is a struct if it just serves as a way to implement the static abstract methods and delegates its X, Y, etc., to an external Vector4.

Sorry for being a pain 😁, but can you post a simple snippet of how you would read, for example, a color semantic of type UNormByte4.

I don't want to slow you with too many questions, as this seems to be a pretty interesting concept to me. What about doing some kind of prototype where we can discuss and iterate?

@Eideren
Copy link
Collaborator Author

Eideren commented Sep 22, 2025

Welp, looked into it more thoroughly and the jit still isn't quite there yet I'm afraid, conversion from large to smaller data types has too much added overhead compared to manual conversion

Here's were I'll leave things off for now https://github.com/Eideren/VectorConversion

But it is a 32-bit type with 4 bytes. It's just the way you can interpret those values as unsigned and normalized, not the internal representation.

That datatype was replaced with Color since that's what it was supposed to be in the first place, but the logic hasn't changed.
Is the issue with the fact that a datatype exist for this purpose, or that there is conversion between datatypes with normalization involved ?

@Ethereal77
Copy link
Contributor

Is the issue with the fact that a datatype exist for this purpose, or that there is conversion between datatypes with normalization involved?

I'm not really opposed to having data types that represent UNorm values. Just that, as it is a concept separated from the memory layout and more related to how you operate on the data, it can lead to an explosion of types.

But I really don't know what would be preferable. Having a few UNorm types for when data is always going to be UNorm does not seem so bad to me.

My initial fear was that having UNormByte4 would make people to ask why there is no other UNorm types, or why there are no SNorm types, specially if we wanted (not sure if it is even a good idea 😁) to introduce some types for pixel formats. If you look at the PixelFormat enum, datatypes can be typeless, but then for each one, it can have UInt, UNorm, SNorm, ..., variants.

@Eideren
Copy link
Collaborator Author

Eideren commented Sep 23, 2025

Makes sense, thankfully this became a non-issue once Color got involved, we can figure it out once there is a need to cover more datatypes.
I don't have anything else to add on my end, feel free to review again when you have time :)

@Eideren Eideren merged commit 7b07233 into stride3d:master Oct 1, 2025
7 checks passed
@Eideren Eideren deleted the meshbuffer_api branch October 1, 2025 11:31
@Eideren Eideren changed the title feat: Mesh vertex buffer reading API feat: Mesh API for index and vertex buffer reading Oct 1, 2025
Eideren added a commit that referenced this pull request Oct 26, 2025
xen2 pushed a commit that referenced this pull request Dec 2, 2025
azeno added a commit to vvvv/stride that referenced this pull request Dec 2, 2025
…d#2858

Reverts the method to what it was before stride3d#2858. This fix is not necessary on main branch where it was already applied by 8e791ad. However that fix is not applicable on 4.2 branch because it needs .net10.
xen2 pushed a commit that referenced this pull request Dec 2, 2025
Reverts the method to what it was before #2858. This fix is not necessary on main branch where it was already applied by 8e791ad. However that fix is not applicable on 4.2 branch because it needs .net10.
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.

2 participants