// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Buffers; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; #if NETSTANDARD2_1_OR_GREATER using CommunityToolkit.HighPerformance.Buffers.Internals; #endif using CommunityToolkit.HighPerformance.Helpers; using CommunityToolkit.HighPerformance.Memory.Internals; using CommunityToolkit.HighPerformance.Memory.Views; using static CommunityToolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; #pragma warning disable CA2231 namespace CommunityToolkit.HighPerformance; /// /// A readonly version of . /// /// The type of items in the current instance. [DebuggerTypeProxy(typeof(MemoryDebugView2D<>))] [DebuggerDisplay("{ToString(),raw}")] public readonly struct ReadOnlyMemory2D : IEquatable> { /// /// The target instance, if present. /// private readonly object? instance; /// /// The initial offset within . /// private readonly IntPtr offset; /// /// The height of the specified 2D region. /// private readonly int height; /// /// The width of the specified 2D region. /// private readonly int width; /// /// The pitch of the specified 2D region. /// private readonly int pitch; /// /// Initializes a new instance of the struct. /// /// The target to wrap. /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// /// Thrown when either or are invalid. /// /// The total area must match the length of . public ReadOnlyMemory2D(string text, int height, int width) : this(text, 0, height, width, 0) { } /// /// Initializes a new instance of the struct. /// /// The target to wrap. /// The initial offset within . /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// The pitch in the resulting 2D area. /// /// Thrown when one of the input parameters is out of range. /// /// /// Thrown when the requested area is outside of bounds for . /// public ReadOnlyMemory2D(string text, int offset, int height, int width, int pitch) { if ((uint)offset > (uint)text.Length) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); } if (height < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if (width < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } if (pitch < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); } int area = OverflowHelper.ComputeInt32Area(height, width, pitch); int remaining = text.Length - offset; if (area > remaining) { ThrowHelper.ThrowArgumentException(); } this.instance = text; this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(text, ref text.DangerousGetReferenceAt(offset)); this.height = height; this.width = width; this.pitch = pitch; } /// /// Initializes a new instance of the struct. /// /// The target array to wrap. /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// /// Thrown when either or are invalid. /// /// The total area must match the length of . public ReadOnlyMemory2D(T[] array, int height, int width) : this(array, 0, height, width, 0) { } /// /// Initializes a new instance of the struct. /// /// The target array to wrap. /// The initial offset within . /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// The pitch in the resulting 2D area. /// /// Thrown when one of the input parameters is out of range. /// /// /// Thrown when the requested area is outside of bounds for . /// public ReadOnlyMemory2D(T[] array, int offset, int height, int width, int pitch) { if ((uint)offset > (uint)array.Length) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); } if (height < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if (width < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } if (pitch < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); } int area = OverflowHelper.ComputeInt32Area(height, width, pitch); int remaining = array.Length - offset; if (area > remaining) { ThrowHelper.ThrowArgumentException(); } this.instance = array; this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(offset)); this.height = height; this.width = width; this.pitch = pitch; } /// /// Initializes a new instance of the struct wrapping a 2D array. /// /// The given 2D array to wrap. public ReadOnlyMemory2D(T[,]? array) { if (array is null) { this = default; return; } this.instance = array; this.offset = GetArray2DDataByteOffset(); this.height = array.GetLength(0); this.width = array.GetLength(1); this.pitch = 0; } /// /// Initializes a new instance of the struct wrapping a 2D array. /// /// The given 2D array to wrap. /// The target row to map within . /// The target column to map within . /// The height to map within . /// The width to map within . /// /// Thrown when either , or /// are negative or not within the bounds that are valid for . /// public ReadOnlyMemory2D(T[,]? array, int row, int column, int height, int width) { if (array is null) { if (row != 0 || column != 0 || height != 0 || width != 0) { ThrowHelper.ThrowArgumentException(); } this = default; return; } int rows = array.GetLength(0); int columns = array.GetLength(1); if ((uint)row >= (uint)rows) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); } if ((uint)column >= (uint)columns) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); } if ((uint)height > (uint)(rows - row)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if ((uint)width > (uint)(columns - column)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } this.instance = array; this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(row, column)); this.height = height; this.width = width; this.pitch = columns - width; } /// /// Initializes a new instance of the struct wrapping a layer in a 3D array. /// /// The given 3D array to wrap. /// The target layer to map within . /// Thrown when a parameter is invalid. public ReadOnlyMemory2D(T[,,] array, int depth) { if ((uint)depth >= (uint)array.GetLength(0)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); } this.instance = array; this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(depth, 0, 0)); this.height = array.GetLength(1); this.width = array.GetLength(2); this.pitch = 0; } /// /// Initializes a new instance of the struct wrapping a layer in a 3D array. /// /// The given 3D array to wrap. /// The target layer to map within . /// The target row to map within . /// The target column to map within . /// The height to map within . /// The width to map within . /// Thrown when a parameter is invalid. public ReadOnlyMemory2D(T[,,] array, int depth, int row, int column, int height, int width) { if ((uint)depth >= (uint)array.GetLength(0)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); } int rows = array.GetLength(1); int columns = array.GetLength(2); if ((uint)row >= (uint)rows) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); } if ((uint)column >= (uint)columns) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); } if ((uint)height > (uint)(rows - row)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if ((uint)width > (uint)(columns - column)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } this.instance = array; this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(depth, row, column)); this.height = height; this.width = width; this.pitch = columns - width; } #if NETSTANDARD2_1_OR_GREATER /// /// Initializes a new instance of the struct. /// /// The target to wrap. /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// /// Thrown when either or are invalid. /// /// The total area must match the length of . public ReadOnlyMemory2D(MemoryManager memoryManager, int height, int width) : this(memoryManager, 0, height, width, 0) { } /// /// Initializes a new instance of the struct. /// /// The target to wrap. /// The initial offset within . /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// The pitch in the resulting 2D area. /// /// Thrown when one of the input parameters is out of range. /// /// /// Thrown when the requested area is outside of bounds for . /// public ReadOnlyMemory2D(MemoryManager memoryManager, int offset, int height, int width, int pitch) { int length = memoryManager.GetSpan().Length; if ((uint)offset > (uint)length) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); } if (height < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if (width < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } if (pitch < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); } if (width == 0 || height == 0) { this = default; return; } int area = OverflowHelper.ComputeInt32Area(height, width, pitch); int remaining = length - offset; if (area > remaining) { ThrowHelper.ThrowArgumentException(); } this.instance = memoryManager; this.offset = (nint)(uint)offset; this.height = height; this.width = width; this.pitch = pitch; } /// /// Initializes a new instance of the struct. /// /// The target to wrap. /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// /// Thrown when either or are invalid. /// /// The total area must match the length of . internal ReadOnlyMemory2D(ReadOnlyMemory memory, int height, int width) : this(memory, 0, height, width, 0) { } /// /// Initializes a new instance of the struct. /// /// The target to wrap. /// The initial offset within . /// The height of the resulting 2D area. /// The width of each row in the resulting 2D area. /// The pitch in the resulting 2D area. /// /// Thrown when one of the input parameters is out of range. /// /// /// Thrown when the requested area is outside of bounds for . /// internal ReadOnlyMemory2D(ReadOnlyMemory memory, int offset, int height, int width, int pitch) { if ((uint)offset > (uint)memory.Length) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); } if (height < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if (width < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } if (pitch < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); } if (width == 0 || height == 0) { this = default; return; } int area = OverflowHelper.ComputeInt32Area(height, width, pitch); int remaining = memory.Length - offset; if (area > remaining) { ThrowHelper.ThrowArgumentException(); } // Check the supported underlying objects, like in Memory2D if (typeof(T) == typeof(char) && MemoryMarshal.TryGetString(Unsafe.As, ReadOnlyMemory>(ref memory), out string? text, out int textStart, out _)) { ref char r0 = ref text.DangerousGetReferenceAt(textStart + offset); this.instance = text; this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(text, ref r0); } else if (MemoryMarshal.TryGetArray(memory, out ArraySegment segment)) { T[] array = segment.Array!; this.instance = array; this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(segment.Offset + offset)); } else if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager? memoryManager, out int memoryManagerStart, out _)) { this.instance = memoryManager; this.offset = (nint)(uint)(memoryManagerStart + offset); } else { ThrowHelper.ThrowArgumentExceptionForUnsupportedType(); this.instance = null; this.offset = default; } this.height = height; this.width = width; this.pitch = pitch; } #endif /// /// Initializes a new instance of the struct with the specified parameters. /// /// The target instance. /// The initial offset within . /// The height of the 2D memory area to map. /// The width of the 2D memory area to map. /// The pitch of the 2D memory area to map. [MethodImpl(MethodImplOptions.AggressiveInlining)] private ReadOnlyMemory2D(object instance, IntPtr offset, int height, int width, int pitch) { this.instance = instance; this.offset = offset; this.height = height; this.width = width; this.pitch = pitch; } /// /// Creates a new instance from an arbitrary object. /// /// The instance holding the data to map. /// The target reference to point to (it must be within ). /// The height of the 2D memory area to map. /// The width of the 2D memory area to map. /// The pitch of the 2D memory area to map. /// A instance with the specified parameters. /// The parameter is not validated, and it's responsibility of the caller to ensure it's valid. /// /// Thrown when one of the input parameters is out of range. /// [Pure] public static ReadOnlyMemory2D DangerousCreate(object instance, ref T value, int height, int width, int pitch) { if (height < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if (width < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } if (pitch < 0) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForPitch(); } OverflowHelper.EnsureIsInNativeIntRange(height, width, pitch); IntPtr offset = ObjectMarshal.DangerousGetObjectDataByteOffset(instance, ref value); return new ReadOnlyMemory2D(instance, offset, height, width, pitch); } /// /// Gets an empty instance. /// public static ReadOnlyMemory2D Empty => default; /// /// Gets a value indicating whether the current instance is empty. /// public bool IsEmpty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.height == 0 || this.width == 0; } /// /// Gets the length of the current instance. /// public nint Length { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (nint)(uint)this.height * (nint)(uint)this.width; } /// /// Gets the height of the underlying 2D memory area. /// public int Height { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.height; } /// /// Gets the width of the underlying 2D memory area. /// public int Width { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => this.width; } /// /// Gets a instance from the current memory. /// public ReadOnlySpan2D Span { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (this.instance is not null) { #if NETSTANDARD2_1_OR_GREATER if (this.instance is MemoryManager memoryManager) { ref T r0 = ref memoryManager.GetSpan().DangerousGetReference(); ref T r1 = ref Unsafe.Add(ref r0, this.offset); return new ReadOnlySpan2D(in r1, this.height, this.width, this.pitch); } else { // This handles both arrays and strings ref T r0 = ref ObjectMarshal.DangerousGetObjectDataReferenceAt(this.instance, this.offset); return new ReadOnlySpan2D(in r0, this.height, this.width, this.pitch); } #else return new ReadOnlySpan2D(this.instance, this.offset, this.height, this.width, this.pitch); #endif } return default; } } #if NETSTANDARD2_1_OR_GREATER /// /// Slices the current instance with the specified parameters. /// /// The target range of rows to select. /// The target range of columns to select. /// /// Thrown when either or are invalid. /// /// A new instance representing a slice of the current one. public ReadOnlyMemory2D this[Range rows, Range columns] { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { (int row, int height) = rows.GetOffsetAndLength(this.height); (int column, int width) = columns.GetOffsetAndLength(this.width); return Slice(row, column, height, width); } } #endif /// /// Slices the current instance with the specified parameters. /// /// The target row to map within the current instance. /// The target column to map within the current instance. /// The height to map within the current instance. /// The width to map within the current instance. /// /// Thrown when either , or /// are negative or not within the bounds that are valid for the current instance. /// /// A new instance representing a slice of the current one. [Pure] public ReadOnlyMemory2D Slice(int row, int column, int height, int width) { if ((uint)row >= Height) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); } if ((uint)column >= this.width) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); } if ((uint)height > (Height - row)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); } if ((uint)width > (this.width - column)) { ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); } int shift = ((this.width + this.pitch) * row) + column; int pitch = this.pitch + (this.width - width); IntPtr offset = this.offset + (shift * Unsafe.SizeOf()); return new ReadOnlyMemory2D(this.instance!, offset, height, width, pitch); } /// /// Copies the contents of this into a destination instance. /// /// The destination instance. /// /// Thrown when is shorter than the source instance. /// public void CopyTo(Memory destination) => Span.CopyTo(destination.Span); /// /// Attempts to copy the current instance to a destination . /// /// The target of the copy operation. /// Whether or not the operation was successful. public bool TryCopyTo(Memory destination) => Span.TryCopyTo(destination.Span); /// /// Copies the contents of this into a destination instance. /// For this API to succeed, the target has to have the same shape as the current one. /// /// The destination instance. /// /// Thrown when is shorter than the source instance. /// public void CopyTo(Memory2D destination) => Span.CopyTo(destination.Span); /// /// Attempts to copy the current instance to a destination . /// For this API to succeed, the target has to have the same shape as the current one. /// /// The target of the copy operation. /// Whether or not the operation was successful. public bool TryCopyTo(Memory2D destination) => Span.TryCopyTo(destination.Span); /// /// Creates a handle for the memory. /// The GC will not move the memory until the returned /// is disposed, enabling taking and using the memory's address. /// /// /// An instance with nonprimitive (non-blittable) members cannot be pinned. /// /// A instance wrapping the pinned handle. public unsafe MemoryHandle Pin() { if (this.instance is not null) { if (this.instance is MemoryManager memoryManager) { return memoryManager.Pin(); } GCHandle handle = GCHandle.Alloc(this.instance, GCHandleType.Pinned); void* pointer = Unsafe.AsPointer(ref ObjectMarshal.DangerousGetObjectDataReferenceAt(this.instance, this.offset)); return new MemoryHandle(pointer, handle); } return default; } /// /// Tries to get a instance, if the underlying buffer is contiguous and small enough. /// /// The resulting , in case of success. /// Whether or not was correctly assigned. public bool TryGetMemory(out ReadOnlyMemory memory) { if (this.pitch == 0 && Length <= int.MaxValue) { // Empty Memory2D instance if (this.instance is null) { memory = default; } else if (typeof(T) == typeof(char) && this.instance.GetType() == typeof(string)) { // Here we need to create a Memory from the wrapped string, and to do so we need to do an inverse // lookup to find the initial index of the string with respect to the byte offset we're currently using, // which refers to the raw string object data. This can include variable padding or other additional // fields on different runtimes. The lookup operation is still O(1) and just computes the byte offset // difference between the start of the Span (which directly wraps just the actual character data // within the string), and the input reference, which we can get from the byte offset in use. The result // is the character index which we can use to create the final Memory instance. string text = Unsafe.As(this.instance)!; int index = text.AsSpan().IndexOf(in ObjectMarshal.DangerousGetObjectDataReferenceAt(text, this.offset)); ReadOnlyMemory temp = text.AsMemory(index, (int)Length); memory = Unsafe.As, ReadOnlyMemory>(ref temp); } else if (this.instance is MemoryManager memoryManager) { // If the object is a MemoryManager, just slice it as needed memory = memoryManager.Memory.Slice((int)(nint)this.offset, this.height * this.width); } else if (this.instance.GetType() == typeof(T[])) { // If it's a T[] array, also handle the initial offset T[] array = Unsafe.As(this.instance)!; int index = array.AsSpan().IndexOf(ref ObjectMarshal.DangerousGetObjectDataReferenceAt(array, this.offset)); memory = array.AsMemory(index, this.height * this.width); } #if NETSTANDARD2_1_OR_GREATER else if (this.instance.GetType() == typeof(T[,]) || this.instance.GetType() == typeof(T[,,])) { memory = new RawObjectMemoryManager(this.instance, this.offset, this.height * this.width).Memory; } #endif else { // Reuse a single failure path to reduce // the number of returns in the method goto Failure; } return true; } Failure: memory = default; return false; } /// /// Copies the contents of the current instance into a new 2D array. /// /// A 2D array containing the data in the current instance. [Pure] public T[,] ToArray() => Span.ToArray(); /// [EditorBrowsable(EditorBrowsableState.Never)] public override bool Equals(object? obj) { if (obj is ReadOnlyMemory2D readOnlyMemory) { return Equals(readOnlyMemory); } if (obj is Memory2D memory) { return Equals(memory); } return false; } /// public bool Equals(ReadOnlyMemory2D other) { return this.instance == other.instance && this.offset == other.offset && this.height == other.height && this.width == other.width && this.pitch == other.pitch; } /// [EditorBrowsable(EditorBrowsableState.Never)] public override int GetHashCode() { if (this.instance is not null) { return HashCode.Combine( RuntimeHelpers.GetHashCode(this.instance), this.offset, this.height, this.width, this.pitch); } return 0; } /// public override string ToString() { return $"CommunityToolkit.HighPerformance.ReadOnlyMemory2D<{typeof(T)}>[{this.height}, {this.width}]"; } /// /// Defines an implicit conversion of an array to a /// public static implicit operator ReadOnlyMemory2D(T[,]? array) => new(array); /// /// Defines an implicit conversion of a to a /// public static implicit operator ReadOnlyMemory2D(Memory2D memory) => Unsafe.As, ReadOnlyMemory2D>(ref memory); }