(ReadOnly)SpanView<T> and (ReadOnly)MemoryView<T>
Describe the problem this feature would solve
Currently you cannot conveniently manipulate strided memory via Memory<T> and Span<T>, as they have no support for stride. While it may soon be possible to manually create instances of (ReadOnly)RefEnumerable<T> (via https://github.com/windows-toolkit/WindowsCommunityToolkit/pull/3645/), they cannot always be used for this purpose as they operate in terms of T and not byte.
Describe the solution
These four new types (SpanView<T>, ReadOnlySpanView<T>, MemoryView<T> and ReadOnlyMemoryView<T>) provide a solution to this problem by providing a strided "view" over an existing (ReadOnly)Span<T> or (ReadOnly)Memory<T>'s data. This allows safe and convenient access to strided data, such as interleaved mesh data buffers:
// ReadOnly variants omitted for brevity.
namespace Microsoft.Toolkit.HighPerformance.Memory
{
public readonly ref struct SpanView<T>
where T : unmanaged
{
public int Length { get; }
public int Stride { get; }
public bool IsEmpty { get; }
public ref T this[int index] { 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 static implicit operator SpanView<T>(Span<T> span);
public static bool operator ==(SpanView<T> left, SpanView<T> right);
public static bool operator !=(SpanView<T> left, SpanView<T> right);
public unsafe SpanView(void* pointer, int stride, int length);
public SpanView(Span<byte> span, int offset, int stride);
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 CopyFrom(ReadOnlySpanView<T> source);
public bool TryCopyFrom(ReadOnlySpanView<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); // NotSupportedException
public override int GetHashCode(); // NotSupportedException
public override string ToString();
public T[] ToArray();
public ref struct Enumerator
{
public ref T Current { get; }
public bool MoveNext();
}
}
public readonly struct MemoryView<T> : IEquatable<MemoryView<T>>
where T : unmanaged
{
public int Length { get; }
public int Stride { get; }
public bool IsEmpty { get; }
public SpanView<T> SpanView { get; }
public static MemoryView<T> Empty { get; }
public static MemoryView<T> DangerousCreate<TBuffer>(Memory<TBuffer> buffer, ref T field) where TBuffer : unmanaged;
public static MemoryView<T> DangerousCreate<TBuffer>(Memory<TBuffer> buffer, int offset) where TBuffer : unmanaged;
public static implicit operator MemoryView<T>(Memory<T> span);
public static bool operator ==(MemoryView<T> left, MemoryView<T> right);
public static bool operator !=(MemoryView<T> left, MemoryView<T> right);
public MemoryView(Memory<byte> memory, int offset, int stride);
public MemoryView<T> Slice(int start);
public MemoryView<T> Slice(int start, int length);
public void CopyTo(MemoryView<T> destination);
public bool TryCopyTo(MemoryView<T> destination);
public void CopyTo(Memory<T> destination);
public bool TryCopyTo(Memory<T> destination);
public bool Equals(MemoryView<T> other);
public override bool Equals(object obj);
public override int GetHashCode();
public override string ToString();
public T[] ToArray();
public MemoryHandle Pin();
}
public static class MemoryViewMarshal
{
public static Memory<byte> GetMemory<T>(MemoryView<T> view) where T : unmanaged;
public static ReadOnlyMemory<byte> GetMemory<T>(ReadOnlyMemoryView<T> view) where T : unmanaged;
public static Span<byte> GetSpan<T>(SpanView<T> view) where T : unmanaged;
public static ReadOnlySpan<byte> GetSpan<T>(ReadOnlySpanView<T> view) where T : unmanaged;
public static MemoryView<TTo> Cast<TFrom, TTo>(MemoryView<TFrom> view)
where TFrom : unmanaged
where TTo : unmanaged;
public static ReadOnlyMemoryView<TTo> Cast<TFrom, TTo>(ReadOnlyMemoryView<TFrom> view)
where TFrom : unmanaged
where TTo : unmanaged;
public static SpanView<TTo> Cast<TFrom, TTo>(SpanView<TFrom> view)
where TFrom : unmanaged
where TTo : unmanaged;
public static ReadOnlySpanView<TTo> Cast<TFrom, TTo>(ReadOnlySpanView<TFrom> view)
where TFrom : unmanaged
where TTo : unmanaged;
}
}
Using the proposed API, the following code can be written:
// An interleaved vertex buffer for uploading mesh data to the GPU
struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
public Vector2 TexCoord;
}
// 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);
// The interleaved data can now be iterated over field-wise
foreach (ref Vector3 position in positions)
{
position += new Vector3(0, 1, 0);
}
A sample implementation of these types can be found here. Note that there are several optimizations and improvements that can be made (especially as this was written for .NET Standard 2.0). These are only intended to serve as a reference.
Unanswered questions
- Can/should the
unmanagedconstraint be removed from the API? Currently it is necessitated by the use of (ReadOnly)Span<byte>. - How beneficial is the existence of
MemoryViewMarshal?
Describe alternatives you've considered
Previously I proposed https://github.com/windows-toolkit/WindowsCommunityToolkit/issues/3641 to allow manual creation of (ReadOnly)RefEnumerable<T>, but it was found to be unsuitable for many of the scenarios these types are intended to solve.
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!
FYI @Sergio0694