CommunityToolkit / CommunityToolkit/dotnet
(ReadOnly)SpanView<T> and (ReadOnly)MemoryView<T>
- Dominant language
- C#
- Stars
- 3.8k
- Forks
- 400
- PR merge metrics
- No merged PRs in 30d
Description
## Describe the problem this feature would solve
Currently you cannot conveniently manipulate strided memory via `Memory` and `Span`, as they have no support for stride. While it may soon be possible to manually create instances of (`ReadOnly`)`RefEnumerable` (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`, `ReadOnlySpanView`, `MemoryView` and `ReadOnlyMemoryView`) provide a solution to this problem by providing a strided "view" over an existing (`ReadOnly`)`Span` or (`ReadOnly`)`Memory`'s data. This allows safe and convenient access to strided data, such as interleaved mesh data buffers:
```CSharp
// ReadOnly variants omitted for brevity.
namespace Microsoft.Toolkit.HighPerformance.Memory
{
public readonly ref struct SpanView
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 Empty { get; }
public static SpanView DangerousCreate(Span buffer, ref T field) where TBuffer : unmanaged;
public static SpanView DangerousCreate(Span buffer, int offset) where TBuffer : unmanaged;
public static implicit operator SpanView(Span span);
public static bool operator ==(SpanView left, SpanView right);
public static bool operator !=(SpanView left, SpanView right);
public unsafe SpanView(void* pointer, int stride, int length);
public SpanView(Span span, int offset, int stride);
public Enumerator GetEnumerator();
public SpanView Slice(int start);
public SpanView Slice(int start, int length);
public void Clear();
public void Fill(T value);
public void CopyFrom(ReadOnlySpan source);
public bool TryCopyFrom(ReadOnlySpan source);
public void CopyFrom(ReadOnlySpanView source);
public bool TryCopyFrom(ReadOnlySpanView source);
public void CopyTo(SpanView destination);
public bool TryCopyTo(SpanView destination);
public void CopyTo(Span destination);
public bool TryCopyTo(Span destination);
public ref T DangerousGetReference();
public ref T DangerousGetReferenceAt(int index);
public ref T GetPinnableReference();
public bool Equals(SpanView 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 : IEquatable>
where T : unmanaged
{
public int Length { get; }
public int Stride { get; }
public bool IsEmpty { get; }
public SpanView SpanView { get; }
public static MemoryView Empty { get; }
public static MemoryView DangerousCreate(Memory buffer, ref T field) where TBuffer : unmanaged;
public static MemoryView DangerousCreate(Memory buffer, int offset) where TBuffer : unmanaged;
public static implicit operator MemoryView(Memory span);
public static bool operator ==(MemoryView left, MemoryView right);
public static bool operator !=(MemoryView left, MemoryView right);
public MemoryView(Memory memory, int offset, int stride);
public MemoryView Slice(int start);
public MemoryView Slice(int start, int length);
public void CopyTo(MemoryView destination);
public bool TryCopyTo(MemoryView destination);
public void CopyTo(Memory destination);
public bool TryCopyTo(Memory destination);
public bool Equals(MemoryView 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 GetMemory(MemoryView view) where T : unmanaged;
public static ReadOnlyMemory GetMemory(ReadOnlyMemoryView view) where T : unmanaged;
public static Span GetSpan(SpanView view) where T : unmanaged;
public static ReadOnlySpan GetSpan(ReadOnlySpanView view) where T : unmanaged;
public static MemoryView Cast(MemoryView view)
where TFrom : unmanaged
where TTo : unmanaged;
public static ReadOnlyMemoryView Cast(ReadOnlyMemoryView view)
where TFrom : unmanaged
where TTo : unmanaged;
public static SpanView Cast(SpanView view)
where TFrom : unmanaged
where TTo : unmanaged;
public static ReadOnlySpanView Cast(ReadOnlySpanView view)
where TFrom : unmanaged
where TTo : unmanaged;
}
}
```
Using the proposed API, the following code can be written:
```CSharp
// 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(new Vertex[100]);
// Create views over the fields
var positions = SpanView.DangerousCreate(vertices, ref vertices[0].Position);
var normals = SpanView.DangerousCreate(vertices, ref vertices[0].Normal);
var coords = SpanView.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](https://gist.github.com/DaZombieKiller/585163a01e78f03b31baa304a2aa2e28). 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 `unmanaged` constraint be removed from the API? Currently it is necessitated by the use of (`ReadOnly`)`Span`.
* 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`, but it was found to be unsuitable for many of the scenarios these types are intended to solve.
Contributor guide
Assessment
This issue has not been assessed yet.