Currently, there is no support for using a stride in a Span<T> (and I don't expect that to change for many reasons). This can complicate the process of iterating over certain pieces of data within a struct. For example, individual vertex attributes in a vertex buffer.
Given a Span<Vertex>, you may want to pass around a Span<Vector3> representing normals (or some other vertex attribute), allowing you to write one method for processing them for all possible vertex layouts. RefEnumerable<T> could solve this problem but currently cannot be created manually and thus cannot be used for this purpose.
This is a hard API to design correctly and safely. My immediate thought would be to make the RefEnumerable constructors public, but that may not be the best approach. In an ideal world, something akin to the following example would be possible:
interface IVertexLayout
{
// methods for querying the layout
}
struct Vertex : IVertexLayout
{
public Vector3 Position;
public Vector3 Normal;
public Vector2 TexCoord;
// implementation of IVertexLayout
}
RefEnumerable<Vector3> GetNormals<T>(Span<T> vertices)
where T : unmanaged, IVertexLayout
{
var offset = /* Retrieve the offset using IVertexLayout */;
var normals = new RefEnumerable<Vector3>(..., offset);
return normals;
}
Span2D<T> instances that can be used to create a RefEnumerable<T>. (By no means an ideal or good solution.)RefEnumerable<T> in my project to expose this functionality.Hello, 'DaZombieKiller! Thanks for submitting a new feature request. I've automatically added a vote 馃憤 reaction to help get things started. Other community members can vote to help us prioritize this feature in the future!
I realized that RefEnumerable<T> operates on T-based indices and not byte, so I've been experimenting with alternative no. 3 by creating a StrideSpan<T> type, with the following API surface:
public readonly ref struct StrideSpan<T>
where T : unmanaged
{
public int Length { get; }
public int Stride { get; }
public ref T this[int index] { get; }
public StrideSpan(Span<byte> span, int stride);
public Enumerator GetEnumerator();
public static implicit operator StrideSpan<T>(Span<T> span);
public ref struct Enumerator
{
public ref T Current { get; }
public bool MoveNext();
}
}
Which allows the GetNormals method in the example to be written as:
StrideSpan<Vector3> GetNormals<T>(Span<T> vertices)
where T : unmanaged, IVertexLayout
{
var offset = /* Retrieve the offset using IVertexLayout */;
var bytes = MemoryMarshal.AsBytes(vertices);
return new StrideSpan<Vector3>(bytes[offset..], sizeof(T));
}
Nice idea, I was thinking at something like this too recently, it reminds me of gltf buffer views!
I'd also pass the offset to the ctor instead of manually skipping it for the first element so whoever uses this hasn't to
it reminds me of gltf buffer views!
This has me thinking that maybe SpanView<T>, ViewSpan<T> or BufferView<T> would be a better name than StrideSpan<T>.
Thanks, @DaZombieKiller! Opening this up for discussion and see what the community has to say about this.
@Sergio0694 assume this is a HighPerformance package request? Thoughts?
This is indeed a HighPerformance package request, I probably should have clarified that somewhere in the OP.
Hey @DaZombieKiller - thank you for opening the issue, glad to see the HighPerformance types being used 馃槃
I think the best solution here is to expose a constructor for RefEnumerable<T>, as you mentioned. I don't want to just expose the internal constructor, since that's extremely unsafe and C# currently has no safeguards for that (same issue as the Ref<T> type). But, I think exposing a static constructor like Span2D<T>.DangerousCreate makes perfect sense - the intent is clear from the name, and it being a separate static method makes using it more deliberate on the part of consumers. Also that's pretty much an established pattern in the package now, since the Span<T> types have that as well.
I realized that RefEnumerable
operates on T-based indices and not byte
I mean, yes, but your T can also be a byte 馃榿
What I mean is, you could actually make it work by casting your Span<Vector4> to a Span<byte>, and then getting a reference and creating a 1-width Span2D<T> from it, and using the column enumerator. Of course, way less efficient, but still.
Will get to work on that new DangerousCreate method, should be pretty small so probably can slip it into 7.0 馃殌
I don't want to just expose the internal constructor, since that's extremely unsafe and C# currently has no safeguards for that (same issue as the
Ref<T>type).
That was the main concern that came to mind, yeah. A DangerousCreate method definitely sounds like a more reasonable approach.
@DaZombieKiller As a temporary workaround, wouldn't this be doable too? It should do exactly the same anyway:
Vector4[] vertices = new Vector4[4];
Span<float> values = vertices.AsSpan().Cast<Vector4, float>();
RefEnumerable<float> column = Span2D<float>.DangerousCreate(ref values[0], 4, 4, 4).GetColumn(0);
Or, something like that - point being you _can_ already get a custom RefEnumerable<T>, by going through Span2D<T> 馃檪
@DaZombieKiller As a temporary workaround, wouldn't this be doable too? It should do exactly the same anyway:
Vector4[] vertices = new Vector4[4]; Span<float> values = vertices.AsSpan().Cast<Vector4, float>(); RefEnumerable<float> column = Span2D<float>.DangerousCreate(ref values[0], 4, 4, 4).GetColumn(0);Or, something like that - point being you _can_ already get a custom
RefEnumerable<T>, by going throughSpan2D<T>馃檪
I did some more thinking and I'm not sure it would cover all cases. The primary issue is that RefEnumerable<T> operates in terms of T and not byte (i.e., it uses Unsafe.Add and not Unsafe.AddByteOffset). Consider the following struct:
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Vertex
{
public byte BlendIndex;
public byte BlendWeight;
public Vector3 Position;
public Vector3 Normal;
public Vector2 TexCoord;
}
Iterating over the values of Normal from a Span<Vertex> becomes a bit of a problem now. Changing RefEnumerable<T>/RefEnumerableHelper to use AddByteOffset is also likely a non-starter due to the behavioral changes that would introduce.
There is of course the option of using a RefEnumerable<byte>, but then it becomes necessary to use Unsafe.As<byte, T> all over the place.
Right, yeah that makes sense. Though to be clear, that applies to RefEnumerable<T>.DangerousCreate as well as you said. Thought it might still be a useful API to add anyway, also for parity with Span2D<T>.DangerousCreate). These would both work but like you mentioned you'd also need to manually do Unsafe.As for each item in the sequence. 馃
Alternatively, couldn't this problem be solved by just sticking to a constrained generic parameter with an interface?
As in, by literally just passing your Span<T> where T : unmanaged, IVertexLayout around, and then just getting your vertices for each entry direclty from there (assuming IVertexLayout exposes a Vector3 Vertices property, or something).
Alternatively, couldn't this problem be solved by just sticking to a constrained generic parameter with an interface?
As in, by literally just passing yourSpan<T> where T : unmanaged, IVertexLayoutaround, and then just getting your vertices for each entry direclty from there (assumingIVertexLayoutexposes aVector3 Verticesproperty, or something).
Unfortunately, the contents of the vertex layout as well as the types inside it are not always known statically, so the IVertexLayout interface can't make assumptions about it (e.g., it can't expose Vector3 Position since it might not be a Vector3, and it might not be present in that layout at all). Instead it exposes an API more like this:
struct VertexAttributeInfo
{
public VertexAttributeFormat Format;
public int Dimension;
public int FieldOffset;
}
interface IVertexLayout
{
bool HasAttribute(VertexAttribute attribute);
VertexAttributeInfo GetAttributeInfo(VertexAttribute attribute);
}
And then higher level APIs allow accessing the data over generic T (currently using a Span-like type similar to the StrideSpan mentioned above).
After further consideration I've come to the conclusion that a new type representing a view over a buffer is likely the best way forward. I've currently implemented the following APIs (in addition to ReadOnlySpanView<T>, MemoryView<T> and ReadOnlyMemoryView<T>):
public readonly ref struct SpanView<T>
where T : unmanaged
{
public int Length { get; }
public int Stride { get; }
public bool IsEmpty { get; }
public static SpanView<T> Empty { get; }
public static SpanView<T> DangerousCreate<TBuffer>(Span<TBuffer> buffer, ref T field) where TBuffer : unmanaged;
public static SpanView<T> DangerousCreate<TBuffer>(Span<TBuffer> buffer, int offset) where TBuffer : unmanaged;
public SpanView(void* pointer, int length, int stride);
public SpanView(Span<byte> span, int offset, int stride);
public ref T this[int index] { get; }
public Enumerator GetEnumerator();
public SpanView<T> Slice(int start);
public SpanView<T> Slice(int start, int length);
public void Clear();
public void Fill(T value);
public void CopyFrom(ReadOnlySpan<T> source);
public bool TryCopyFrom(ReadOnlySpan<T> source);
public void CopyTo(SpanView<T> destination);
public bool TryCopyTo(SpanView<T> destination);
public void CopyTo(Span<T> destination);
public bool TryCopyTo(Span<T> destination);
public ref T DangerousGetReference();
public ref T DangerousGetReferenceAt(int index);
public ref T GetPinnableReference();
public bool Equals(SpanView<T> other);
public override bool Equals(object obj);
public override int GetHashCode();
public override string ToString();
public T[] ToArray();
public static implicit operator ReadOnlySpanView<T>(SpanView<T> view);
public static bool operator ==(SpanView<T> left, SpanView<T> right);
public static bool operator !=(SpanView<T> left, SpanView<T> right);
public ref struct Enumerator
{
public ref T Current { get; }
public bool MoveNext();
}
}
Which allows me to write the following:
// Allocate the vertex buffer
var vertices = new Span<Vertex>(new Vertex[100]);
// Create views over the fields
var positions = SpanView<Vector3>.DangerousCreate(vertices, ref vertices[0].Position);
var normals = SpanView<Vector3>.DangerousCreate(vertices, ref vertices[0].Normal);
var coords = SpanView<Vector2>.DangerousCreate(vertices, ref vertices[0].TexCoord);
Does this seem like something that would have value in being added to the toolkit? I feel like it should be moved to a new issue if that's the case, since this one is currently tracking the DangerousCreate API for RefEnumerable.
Most helpful comment
That was the main concern that came to mind, yeah. A
DangerousCreatemethod definitely sounds like a more reasonable approach.