CommunityToolkit / CommunityToolkit/dotnet

(ReadOnly)SpanView<T> and (ReadOnly)MemoryView<T>

Open
#24 2 comments 2 reactions 0 assignees View on GitHub
feature request :mailbox_with_mail: high-performance 🚂
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.