From 65c89e3f3e906687ec7be9b6bd6df9408143d81e Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Thu, 12 Feb 2026 01:52:03 +0100 Subject: [PATCH 1/9] Add logic to check for overflows in 3D parameters --- .../Memory/Internals/OverflowHelper.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs b/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs index 41db2563..e7651a34 100644 --- a/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs +++ b/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs @@ -66,4 +66,47 @@ public static int ComputeInt32Area(int height, int width, int pitch) { return Max(checked(((width + pitch) * (height - 1)) + width), 0); } + + /// + /// Ensures that the input parameters will not exceed the maximum native int value when indexing. + /// + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The depth of the 3D memory area to map. + /// The row pitch of the 3D memory area (the distance between each row). + /// The slice pitch of the 3D memory area (the distance between each 2D slice). + /// Throw when the inputs don't fit in the expected range. + /// The input parameters are assumed to always be positive. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void EnsureIsInNativeIntRange(int depth, int height, int width, int slicePitch, int rowPitch) + { + // Refer to the explanation above for the Memory2D and Span2D types. + // For the Memory3D and Span3D types it is similar, except we now have a "volume" + // consisting of one or more 2D slices. For these 2D slices, rowPitch is the distance + // between consecutive rows. slicePitch is the distance between consecutive slices. + // Note that we're also subtracting 1 to the depth as we don't want to include the trailing pitch + // for the 3D memory area. + _ = checked(((nint)slicePitch * Max(unchecked(depth - 1), 0)) + + ((nint)(width + rowPitch) * Max(unchecked(height - 1), 0)) + + Max(unchecked(width - 1), 0)); + } + + /// + /// Ensures that the input parameters will not exceed when indexing. + /// + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The depth of the 3D memory area to map. + /// The row pitch of the 3D memory area (the distance between each row). + /// The slice pitch of the 3D memory area (the distance between each 2D slice). + /// The volume resulting from the given parameters. + /// Throw when the inputs don't fit in the expected range. + /// The input parameters are assumed to always be positive. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ComputeInt32Volume(int depth, int height, int width, int slicePitch, int rowPitch) + { + return Max(checked((slicePitch * (depth - 1)) + + ((width + rowPitch) * (height - 1)) + + width), 0); + } } From f54976d0c1cb976d928a10b627e4167155b515b6 Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Thu, 12 Feb 2026 01:55:39 +0100 Subject: [PATCH 2/9] Add memory primitives for 3D * Span3D and ReadOnlySpan3D * Ref-Enumerators for both * Memory3D and ReadOnlyMemory3D --- .../Memory/Internals/OverflowHelper.cs | 3 +- .../Memory/Internals/ThrowHelper.cs | 24 + .../Memory/Memory3D{T}.cs | 914 +++++++++++ .../Memory/ReadOnlyMemory3D{T}.cs | 970 +++++++++++ .../Memory/ReadOnlySpan3D{T}.Enumerator.cs | 298 ++++ .../Memory/ReadOnlySpan3D{T}.cs | 1263 +++++++++++++++ .../Memory/Span3D{T}.Enumerator.cs | 298 ++++ .../Memory/Span3D{T}.cs | 1430 +++++++++++++++++ .../Memory/Views/MemoryDebugView3D{T}.cs | 56 + 9 files changed, 5255 insertions(+), 1 deletion(-) create mode 100644 src/CommunityToolkit.HighPerformance/Memory/Memory3D{T}.cs create mode 100644 src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory3D{T}.cs create mode 100644 src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.Enumerator.cs create mode 100644 src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.cs create mode 100644 src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.Enumerator.cs create mode 100644 src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.cs create mode 100644 src/CommunityToolkit.HighPerformance/Memory/Views/MemoryDebugView3D{T}.cs diff --git a/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs b/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs index e7651a34..e8d10d84 100644 --- a/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs +++ b/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs @@ -9,7 +9,8 @@ namespace CommunityToolkit.HighPerformance.Memory.Internals; /// -/// A helper to validate arithmetic operations for and . +/// A helper to validate arithmetic operations for , , +/// , and . /// internal static class OverflowHelper { diff --git a/src/CommunityToolkit.HighPerformance/Memory/Internals/ThrowHelper.cs b/src/CommunityToolkit.HighPerformance/Memory/Internals/ThrowHelper.cs index c31d2108..9fee68e0 100644 --- a/src/CommunityToolkit.HighPerformance/Memory/Internals/ThrowHelper.cs +++ b/src/CommunityToolkit.HighPerformance/Memory/Internals/ThrowHelper.cs @@ -78,6 +78,14 @@ public static void ThrowArgumentOutOfRangeExceptionForDepth() { throw new ArgumentOutOfRangeException("depth"); } + + /// + /// Throws an when the "slice" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForSlice() + { + throw new ArgumentOutOfRangeException("slice"); + } /// /// Throws an when the "row" parameter is invalid. @@ -126,4 +134,20 @@ public static void ThrowArgumentOutOfRangeExceptionForPitch() { throw new ArgumentOutOfRangeException("pitch"); } + + /// + /// Throws an when the "rowPitch" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForRowPitch() + { + throw new ArgumentOutOfRangeException("rowPitch"); + } + + /// + /// Throws an when the "slicePitch" parameter is invalid. + /// + public static void ThrowArgumentOutOfRangeExceptionForSlicePitch() + { + throw new ArgumentOutOfRangeException("slicePitch"); + } } diff --git a/src/CommunityToolkit.HighPerformance/Memory/Memory3D{T}.cs b/src/CommunityToolkit.HighPerformance/Memory/Memory3D{T}.cs new file mode 100644 index 00000000..de5368d2 --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Memory/Memory3D{T}.cs @@ -0,0 +1,914 @@ +// 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.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; + +/// +/// represents a 3D region of arbitrary memory. It is to +/// what is to . For further details on how the internal layout +/// is structured, see the docs for . The type can wrap arrays +/// of any rank, provided that a valid series of parameters for the target memory area(s) are specified. +/// +/// The type of items in the current instance. +[DebuggerTypeProxy(typeof(MemoryDebugView3D<>))] +[DebuggerDisplay("{ToString(),raw}")] +public readonly struct Memory3D : IEquatable> +{ + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial byte offset within . + /// + private readonly nint offset; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; + + /// + /// The height of the specified 3D region. + /// + private readonly int height; + + /// + /// The width of the specified 3D region. + /// + private readonly int width; + + /// + /// The pitch of each row in the specified 3D region. + /// + private readonly int rowPitch; + + /// + /// The pitch of each slice in the specified 3D region. + /// + private readonly int slicePitch; + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , or are invalid. + /// + /// The total volume must match the length of . + public Memory3D(T[] array, int depth, int height, int width) + : this(array, 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The pitch of each row in the resulting 3D area. + /// The pitch of each slice in the resulting 3D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + public Memory3D(T[] array, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = array.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = array; + this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(offset)); + this.depth = depth; + this.height = height; + this.width = width; + this.rowPitch = rowPitch; + this.slicePitch = slicePitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + /// + /// Thrown when doesn't match . + /// + public Memory3D(T[,,]? array) + { + if (array is null) + { + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + this.instance = array; + this.offset = GetArray3DDataByteOffset(); + this.depth = array.GetLength(0); + this.height = array.GetLength(1); + this.width = array.GetLength(2); + this.rowPitch = 0; + this.slicePitch = 0; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + /// The target slice to map within . + /// The target row to map within . + /// The target column to map within . + /// The depth to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , , or , + /// , , or + /// are negative or not within the bounds that are valid for . + /// + public Memory3D(T[,,]? array, int slice, int row, int column, int depth, int height, int width) + { + if (array is null) + { + if (slice != 0 || row != 0 || column != 0 || depth != 0 || height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + int slices = array.GetLength(0); + int rows = array.GetLength(1); + int columns = array.GetLength(2); + + if ((uint)slice >= (uint)slices) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (uint)(slices - slice)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + 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(slice, row, column)); + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slices - depth; + this.rowPitch = columns - width; + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , , or are invalid. + /// + /// The total volume must match the length of . + public Memory3D(MemoryManager memoryManager, int depth, int height, int width) + : this(memoryManager, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The pitch of each row in the resulting 3D area. + /// The pitch of each slice in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + public unsafe Memory3D(MemoryManager memoryManager, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + int length = memoryManager.GetSpan().Length; + + if ((uint)offset > (uint)length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = memoryManager; + this.offset = (nint)(uint)offset * (nint)(uint)sizeof(T); + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slicePitch; + this.rowPitch = rowPitch; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , , or are invalid. + /// + /// The total volume must match the length of . + internal Memory3D(Memory memory, int depth, int height, int width) + : this(memory, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The pitch of each slice in the resulting 3D area. + /// The pitch of each row in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + internal unsafe Memory3D(Memory memory, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if ((uint)offset > (uint)memory.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = memory.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + // Check if the Memory instance wraps a string. This is possible in case + // consumers do an unsafe cast for the entire Memory object, and while not + // really safe it is still supported in CoreCLR too, so we're following suit here. + if (typeof(T) == typeof(char) && + MemoryMarshal.TryGetString(Unsafe.As, Memory>(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)) + { + // Check if the input Memory instance wraps an array we can access. + // This is fine, since Memory on its own doesn't control the lifetime + // of the underlying array anyway, and this Memory3D type would do the same. + // Using the array directly makes retrieving a Span3D faster down the line, + // as we no longer have to jump through the boxed Memory first anymore. + 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) * (nint)(uint)sizeof(T); + } + else + { + ThrowHelper.ThrowArgumentExceptionForUnsupportedType(); + + this.instance = null; + this.offset = default; + } + + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slicePitch; + this.rowPitch = rowPitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within . + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The pitch of each slice in the 3D memory area to map. + /// The pitch of each row in the 3D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Memory3D(object instance, IntPtr offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + this.instance = instance; + this.offset = offset; + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slicePitch; + this.rowPitch = rowPitch; + } + + /// + /// 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 depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The pitch of each slice in the 3D memory area to map. + /// The pitch of each row in the 3D 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. + /// + public static Memory3D DangerousCreate(object instance, ref T value, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + + IntPtr offset = ObjectMarshal.DangerousGetObjectDataByteOffset(instance, ref value); + + return new(instance, offset, depth, height, width, slicePitch, rowPitch); + } + + /// + /// Gets an empty instance. + /// + public static Memory3D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.depth == 0 || this.height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)this.depth * (nint)(uint)this.height * (nint)(uint)this.width; + } + + /// + /// Gets the depth of the underlying 3D memory area. + /// + public int Depth + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.depth; + } + + /// + /// Gets the height of the underlying 3D memory area. + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.height; + } + + /// + /// Gets the width of the underlying 3D memory area. + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.width; + } + + /// + /// Gets a instance from the current memory. + /// + public Span3D 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.AddByteOffset(ref r0, this.offset); + + return new(ref r1, this.depth, this.height, this.width, this.slicePitch, this.rowPitch); + } + else + { + ref T r0 = ref ObjectMarshal.DangerousGetObjectDataReferenceAt(this.instance, this.offset); + + return new(ref r0, this.depth, this.height, this.width, this.slicePitch, this.rowPitch); + } +#else + return new(this.instance, this.offset, this.depth, this.height, this.width, this.slicePitch, this.rowPitch); +#endif + } + + return default; + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of slices to select. + /// 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 Memory3D this[Range slices, Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + (int slice, int depth) = slices.GetOffsetAndLength(this.depth); + (int row, int height) = rows.GetOffsetAndLength(this.height); + (int column, int width) = columns.GetOffsetAndLength(this.width); + + return Slice(slice, row, column, depth, height, width); + } + } +#endif + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target slice to map within the current instance. + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The depth 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. + /// See additional remarks in the docs. + public unsafe Memory3D Slice(int slice, int row, int column, int depth, int height, int width) + { + if ((uint)slice >= this.depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (this.depth - slice)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if ((uint)height > (this.height - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (this.width - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + int shift = (slice * ((this.width + this.rowPitch) * this.height + this.slicePitch)) + + (row * (this.width + this.rowPitch)) + + column; + + int rowPitch = this.rowPitch + (this.width - width); + int slicePitch = this.slicePitch + ((this.width + this.rowPitch) * (this.height - height)); + + IntPtr offset = this.offset + (shift * sizeof(T)); + + return new(this.instance!, offset, depth, height, width, slicePitch, rowPitch); + } + + /// + /// 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(Memory3D 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(Memory3D 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 non-primitive (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(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 Memory memory) + { + if (this.slicePitch == 0 && + this.rowPitch == 0 && + Length <= int.MaxValue) + { + // Empty Memory3D 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); + + // The string type could still be present if a user ends up creating a + // Memory3D instance from a string using DangerousCreate. Similarly to + // how CoreCLR handles the equivalent case in Memory, here we just do + // the necessary steps to still retrieve a Memory instance correctly + // wrapping the target string. In this case, it is up to the caller + // to make sure not to ever actually write to the resulting Memory. + memory = MemoryMarshal.AsMemory(Unsafe.As, Memory>(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.depth * 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.depth * this.height * this.width); + } +#if NETSTANDARD2_1_OR_GREATER + else if (this.instance.GetType() == typeof(T[,]) || + this.instance.GetType() == typeof(T[,,])) + { + // If the object is a 2D or 3D array, we can create a Memory from the RawObjectMemoryManager type. + // We just need to use the precomputed offset pointing to the first item in the current instance, + // and the current usable length. We don't need to retrieve the current index, as the manager just offsets. + memory = new RawObjectMemoryManager(this.instance, this.offset, this.depth * 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 3D array. + /// + /// A 3D array containing the data in the current instance. + public T[,,] ToArray() => Span.ToArray(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) + { + if (obj is Memory3D memory) + { + return Equals(memory); + } + + if (obj is ReadOnlyMemory3D readOnlyMemory) + { + return readOnlyMemory.Equals(this); + } + + return false; + } + + /// + public bool Equals(Memory3D other) + { + return + this.instance == other.instance && + this.offset == other.offset && + this.depth == other.depth && + this.height == other.height && + this.width == other.width && + this.slicePitch == other.slicePitch && + this.rowPitch == other.rowPitch; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() + { + if (this.instance is not null) + { + return HashCode.Combine( + RuntimeHelpers.GetHashCode(this.instance), + this.offset, + this.depth, + this.height, + this.width, + this.slicePitch, + this.rowPitch); + } + + return 0; + } + + /// + public override string ToString() + { + return $"CommunityToolkit.HighPerformance.Memory3D<{typeof(T)}>[{this.depth}, {this.height}, {this.width}]"; + } + + /// + /// Defines an implicit conversion of an array to a + /// + public static implicit operator Memory3D(T[,,]? array) => new(array); +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory3D{T}.cs b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory3D{T}.cs new file mode 100644 index 00000000..c87f5caa --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlyMemory3D{T}.cs @@ -0,0 +1,970 @@ +// 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.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(MemoryDebugView3D<>))] +[DebuggerDisplay("{ToString(),raw}")] +public readonly struct ReadOnlyMemory3D : IEquatable> +{ + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial byte offset within . + /// + private readonly nint offset; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; + + /// + /// The height of the specified 3D region. + /// + private readonly int height; + + /// + /// The width of the specified 2D region. + /// + private readonly int width; + + /// + /// The pitch of each row in the specified 3D region. + /// + private readonly int rowPitch; + + /// + /// The pitch of each slice in the specified 3D region. + /// + private readonly int slicePitch; + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , or are invalid. + /// + /// The total volume must match the length of . + public ReadOnlyMemory3D(string text, int depth, int height, int width) + : this(text, 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The pitch of each row in the resulting 3D area. + /// The pitch of each slice in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + public ReadOnlyMemory3D(string text, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if ((uint)offset > (uint)text.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = text.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = text; + this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(text, ref text.DangerousGetReferenceAt(offset)); + this.depth = depth; + this.height = height; + this.width = width; + this.rowPitch = rowPitch; + this.slicePitch = slicePitch; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , or are invalid. + /// + /// The total volume must match the length of . + public ReadOnlyMemory3D(T[] array, int depth, int height, int width) + : this(array, 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The pitch of each row in the resulting 3D area. + /// The pitch of each slice in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + public ReadOnlyMemory3D(T[] array, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = array.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = array; + this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(offset)); + this.depth = depth; + this.height = height; + this.width = width; + this.rowPitch = rowPitch; + this.slicePitch = slicePitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + public ReadOnlyMemory3D(T[,,]? array) + { + if (array is null) + { + this = default; + + return; + } + + this.instance = array; + this.offset = GetArray3DDataByteOffset(); + this.depth = array.GetLength(0); + this.height = array.GetLength(1); + this.width = array.GetLength(2); + this.rowPitch = 0; + this.slicePitch = 0; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + /// The target slice to map within . + /// The target row to map within . + /// The target column to map within . + /// The depth to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when either , , or , + /// , , or + /// are negative or not within the bounds that are valid for . + /// + public ReadOnlyMemory3D(T[,,]? array, int slice, int row, int column, int depth, int height, int width) + { + if (array is null) + { + if (slice != 0 || row != 0 || column != 0 || depth != 0 || height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + int slices = array.GetLength(0); + int rows = array.GetLength(1); + int columns = array.GetLength(2); + + if ((uint)slice >= (uint)slices) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (uint)(slices - slice)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + 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(slice, row, column)); + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slices - depth; + this.rowPitch = columns - width; + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , , or are invalid. + /// + /// The total volume must match the length of . + public ReadOnlyMemory3D(MemoryManager memoryManager, int depth, int height, int width) + : this(memoryManager, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The pitch of each row in the resulting 3D area. + /// The pitch of each slice in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + public unsafe ReadOnlyMemory3D(MemoryManager memoryManager, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + int length = memoryManager.GetSpan().Length; + + if ((uint)offset > (uint)length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + this.instance = memoryManager; + this.offset = (nint)(uint)offset * (nint)(uint)sizeof(T); + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slicePitch; + this.rowPitch = rowPitch; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , , or are invalid. + /// + /// The total volume must match the length of . + internal ReadOnlyMemory3D(ReadOnlyMemory memory, int depth, int height, int width) + : this(memory, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The pitch of each slice in the resulting 3D area. + /// The pitch of each row in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + internal unsafe ReadOnlyMemory3D(ReadOnlyMemory memory, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if ((uint)offset > (uint)memory.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = memory.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + + // Check if the ReadOnlyMemory instance wraps a string. This is possible in case + // consumers do an unsafe cast for the entire ReadOnlyMemory object, and while not + // really safe it is still supported in CoreCLR too, so we're following suit here. + 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)) + { + // Check if the input ReadOnlyMemory instance wraps an array we can access. + // This is fine, since ReadOnlyMemory on its own doesn't control the lifetime + // of the underlying array anyway, and this ReadOnlyMemory3D type would do the same. + // Using the array directly makes retrieving a ReadOnlySpan3D faster down the line, + // as we no longer have to jump through the boxed ReadOnlyMemory first anymore. + 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) * (nint)(uint)sizeof(T); + } + else + { + ThrowHelper.ThrowArgumentExceptionForUnsupportedType(); + + this.instance = null; + this.offset = default; + } + + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slicePitch; + this.rowPitch = rowPitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within . + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The pitch of each slice in the 3D memory area to map. + /// The pitch of each row in the 3D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ReadOnlyMemory3D(object instance, IntPtr offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + this.instance = instance; + this.offset = offset; + this.depth = depth; + this.height = height; + this.width = width; + this.slicePitch = slicePitch; + this.rowPitch = rowPitch; + } + + /// + /// 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 depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The pitch of each slice in the 3D memory area to map. + /// The pitch of each row in the 3D 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. + /// + public static ReadOnlyMemory3D DangerousCreate(object instance, ref T value, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + + IntPtr offset = ObjectMarshal.DangerousGetObjectDataByteOffset(instance, ref value); + + return new(instance, offset, depth, height, width, slicePitch, rowPitch); + } + + /// + /// Gets an empty instance. + /// + public static ReadOnlyMemory3D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.depth == 0 || this.height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)this.depth * (nint)(uint)this.height * (nint)(uint)this.width; + } + + /// + /// Gets the depth of the underlying 3D memory area. + /// + public int Depth + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.depth; + } + + /// + /// 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 ReadOnlySpan3D 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.AddByteOffset(ref r0, this.offset); + + return new(in r1, this.depth, this.height, this.width, this.slicePitch, this.rowPitch); + } + else + { + // This handles both arrays and strings + ref T r0 = ref ObjectMarshal.DangerousGetObjectDataReferenceAt(this.instance, this.offset); + + return new(in r0, this.depth, this.height, this.width, this.slicePitch, this.rowPitch); + } +#else + return new(this.instance, this.offset, this.depth, this.height, this.width, this.slicePitch, this.rowPitch); +#endif + } + + return default; + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of slices to select. + /// 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 ReadOnlyMemory3D this[Range slices, Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + (int slice, int depth) = slices.GetOffsetAndLength(this.depth); + (int row, int height) = rows.GetOffsetAndLength(this.height); + (int column, int width) = columns.GetOffsetAndLength(this.width); + + return Slice(slice, row, column, depth, height, width); + } + } +#endif + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target slice to map within the current instance. + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The depth 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. + /// See additional remarks in the docs. + public unsafe ReadOnlyMemory3D Slice(int slice, int row, int column, int depth, int height, int width) + { + if ((uint)slice >= this.depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (this.depth - slice)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if ((uint)height > (this.height - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (this.width - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + int shift = (slice * ((this.width + this.rowPitch) * this.height + this.slicePitch)) + + (row * (this.width + this.rowPitch)) + + column; + + int rowPitch = this.rowPitch + (this.width - width); + int slicePitch = this.slicePitch + ((this.width + this.rowPitch) * (this.height - height)); + + IntPtr offset = this.offset + (shift * sizeof(T)); + + return new(this.instance!, offset, depth, height, width, slicePitch, rowPitch); + } + + /// + /// 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(Memory3D 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(Memory3D 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 non-primitive (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(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.slicePitch == 0 && + this.rowPitch == 0 && + Length <= int.MaxValue) + { + // Empty ReadOnlyMemory3D 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); + + // The string type could still be present if a user ends up creating a + // Memory3D instance from a string using DangerousCreate. Similarly to + // how CoreCLR handles the equivalent case in Memory, here we just do + // the necessary steps to still retrieve a Memory instance correctly + // wrapping the target string. + 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.depth * 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.depth * this.height * this.width); + } +#if NETSTANDARD2_1_OR_GREATER + else if (this.instance.GetType() == typeof(T[,]) || + this.instance.GetType() == typeof(T[,,])) + { + // If the object is a 2D or 3D array, we can create a Memory from the RawObjectMemoryManager type. + // We just need to use the precomputed offset pointing to the first item in the current instance, + // and the current usable length. We don't need to retrieve the current index, as the manager just offsets. + memory = new RawObjectMemoryManager(this.instance, this.offset, this.depth * 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 3D array. + /// + /// A 3D array containing the data in the current instance. + public T[,,] ToArray() => Span.ToArray(); + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) + { + if (obj is ReadOnlyMemory3D readOnlyMemory) + { + return Equals(readOnlyMemory); + } + + if (obj is Memory3D memory) + { + return Equals(memory); + } + + return false; + } + + /// + public bool Equals(ReadOnlyMemory3D other) + { + return + this.instance == other.instance && + this.offset == other.offset && + this.depth == other.depth && + this.height == other.height && + this.width == other.width && + this.slicePitch == other.slicePitch && + this.rowPitch == other.rowPitch; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() + { + if (this.instance is not null) + { + return HashCode.Combine( + RuntimeHelpers.GetHashCode(this.instance), + this.offset, + this.depth, + this.height, + this.width, + this.slicePitch, + this.rowPitch); + } + + return 0; + } + + /// + public override string ToString() + { + return $"CommunityToolkit.HighPerformance.ReadOnlyMemory3D<{typeof(T)}>[{this.depth}, {this.height}, {this.width}]"; + } + + /// + /// Defines an implicit conversion of an array to a + /// + public static implicit operator ReadOnlyMemory3D(T[,,]? array) => new(array); + + /// + /// Defines an implicit conversion of a to a + /// + public static implicit operator ReadOnlyMemory3D(Memory3D memory) => Unsafe.As, ReadOnlyMemory3D>(ref memory); +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.Enumerator.cs b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.Enumerator.cs new file mode 100644 index 00000000..ba77db1f --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.Enumerator.cs @@ -0,0 +1,298 @@ +// 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. + +#if !NET8_0_OR_GREATER +using System; +#endif +using System.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.Enumerables; +using CommunityToolkit.HighPerformance.Memory.Internals; +#if NETSTANDARD2_1_OR_GREATER && !NET8_0_OR_GREATER +using System.Runtime.InteropServices; +#elif NETSTANDARD2_0 +using RuntimeHelpers = CommunityToolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace CommunityToolkit.HighPerformance; + +/// +partial struct ReadOnlySpan3D +{ + /// + /// Gets an enumerable that traverses items in a specified row of a specific slice. + /// + /// The target slice to enumerate within the current instance. + /// The target row to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyRefEnumerable GetRow(int slice, int row) + { + if ((uint)slice >= Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + nint startIndex = (nint)(uint)this.SliceStride * (nint)(uint)slice + + (nint)(uint)this.RowStride * (nint)(uint)row; + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if NETSTANDARD2_1_OR_GREATER + return new(in r1, length: this.width, step: 1); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.instance, ref r1); + + return new(this.instance!, offset, length: this.width, step: 1); +#endif + } + + /// + /// Gets an enumerable that traverses items in a specified column of a specific slice. + /// + /// The target slice to enumerate within the current instance. + /// The target column to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyRefEnumerable GetColumn(int slice,int column) + { + if ((uint)slice >= Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + nint startIndex = (nint)(uint)this.SliceStride * (nint)(uint)slice + column; + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if NETSTANDARD2_1_OR_GREATER + return new(in r1, length: this.height, step: this.RowStride); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.instance, ref r1); + + return new(this.instance!, offset, length: this.height, step: this.RowStride); +#endif + } + + /// + /// Gets an enumerable that traverses items in a specified row along slices. + /// + /// The target row to enumerate within the current instance. + /// The target column to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ReadOnlyRefEnumerable GetDepthColumn(int row, int column) + { + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + nint startIndex = (nint)(uint)this.RowStride * (nint)(uint)row + column; + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if NETSTANDARD2_1_OR_GREATER + return new(in r1, length: Depth, step: this.SliceStride); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.instance, ref r1); + + return new(this.instance, offset, length: Depth, step: this.SliceStride); +#endif + } + + /// + /// Returns an enumerator for the current instance. + /// + /// + /// An enumerator that can be used to traverse the items in the current instance + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new(this); + + /// + /// Provides an enumerator for the elements of a instance. + /// + public ref struct Enumerator + { +#if NET8_0_OR_GREATER + /// + /// The reference for the instance. + /// + private readonly ref readonly T reference; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#elif NETSTANDARD2_1_OR_GREATER + /// + /// The instance pointing to the first item in the target memory area. + /// + /// Just like in , the length is the depth of the 3D region. + private readonly ReadOnlySpan span; +#else + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial byte offset within . + /// + private readonly nint offset; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#endif + + /// + /// The height of the specified 3D region. + /// + private readonly int height; + + /// + /// The width of the specified 3D region. + /// + private readonly int width; + + /// + /// The row stride of the specified 3D region. + /// + private readonly int rowStride; + + /// + /// The slice stride of the specified 3D region. + /// + private readonly int sliceStride; + + /// + /// The current horizontal offset. + /// + private int x; + + /// + /// The current vertical offset. + /// + private int y; + + /// + /// The current slice offset. + /// + private int z; + + /// + /// Initializes a new instance of the struct. + /// + /// The target instance to enumerate. + internal Enumerator(ReadOnlySpan3D span) + { +#if NET8_0_OR_GREATER + this.reference = ref span.reference; + this.depth = span.depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = span.span; +#else + this.instance = span.instance; + this.offset = span.offset; + this.depth = span.depth; +#endif + this.height = span.height; + this.width = span.width; + this.rowStride = span.RowStride; + this.sliceStride = span.SliceStride; + this.x = -1; + this.y = 0; + this.z = 0; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int x = this.x + 1; + + // Horizontal move, within range + if (x < this.width) + { + this.x = x; + + return true; + } + + // We reached the end of a row and there is at least + // another row available: wrap to a new line and continue. + this.x = 0; + int y = this.y + 1; + + if (y < this.height) + { + this.y = y; + + return true; + } + + // We reached the end of a slice and there is at least + // another slice available: wrap to the start of the next one and continue. + this.y = 0; + this.z++; + +#if NET8_0_OR_GREATER + return this.z < this.depth; +#elif NETSTANDARD2_1_OR_GREATER + return this.z < this.span.Length; +#else + return this.z < this.depth; +#endif + } + + /// + /// Gets the duck-typed property. + /// + public readonly ref readonly T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NET8_0_OR_GREATER + ref T r0 = ref Unsafe.AsRef(in this.reference); +#elif NETSTANDARD2_1_OR_GREATER + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + nint index = ((nint)(uint)this.z * (nint)(uint)this.sliceStride) + + ((nint)(uint)this.y * (nint)(uint)this.rowStride) + + (nint)(uint)this.x; + + return ref Unsafe.Add(ref r0, index); + } + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.cs b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.cs new file mode 100644 index 00000000..1f107270 --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Memory/ReadOnlySpan3D{T}.cs @@ -0,0 +1,1263 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if !NETSTANDARD2_1_OR_GREATER +using CommunityToolkit.HighPerformance.Helpers; +#endif +using CommunityToolkit.HighPerformance.Memory.Internals; +using CommunityToolkit.HighPerformance.Memory.Views; +#if !NETSTANDARD2_1_OR_GREATER +using RuntimeHelpers = CommunityToolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +#pragma warning disable CS0809, CA1065 + +namespace CommunityToolkit.HighPerformance; + +/// +/// A readonly version of . +/// +/// The type of items in the current instance. +[DebuggerTypeProxy(typeof(MemoryDebugView3D<>))] +[DebuggerDisplay("{ToString(),raw}")] +public readonly ref partial struct ReadOnlySpan3D +{ +#if NET8_0_OR_GREATER + /// + /// The reference for the instance. + /// + private readonly ref readonly T reference; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#elif NETSTANDARD2_1_OR_GREATER + /// + /// The instance pointing to the first item in the target memory area. + /// + /// + /// The field maps to the depth of the 3D region. + /// This is done to save 4 bytes in the layout of the type. + /// + private readonly ReadOnlySpan span; +#else + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial byte offset within . + /// + private readonly nint offset; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#endif + + /// + /// The height of the specified 3D region. + /// + private readonly int height; + + /// + /// The width of the specified 3D region. + /// + private readonly int width; + + /// + /// The slice stride of the specified 3D region. + /// + /// + /// This combines both the slice size (RowStride and height) and the slice pitch + /// in a single value so that the indexing logic can be simplified + /// (no need to recompute the sum every time) and be faster. + /// + internal readonly int SliceStride; + + /// + /// The row stride of the specified 3D region. + /// + /// + /// This combines both the width and row pitch in a single value so that the indexing + /// logic can be simplified (no need to recompute the sum every time) and be faster. + /// + internal readonly int RowStride; + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map (the distance between each slice). + /// The row pitch of the 3D memory area to map (the distance between each row). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadOnlySpan3D(in T value, int depth, int height, int width, int slicePitch, int rowPitch) + { +#if NET8_0_OR_GREATER + this.reference = ref value; + this.depth = depth; +#else + this.span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(value), depth); +#endif + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The pointer to the first item to map. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map (the distance between each slice). + /// The pitch of the 3D memory area to map (the distance between each row). + /// Thrown when one of the parameters are negative. + public unsafe ReadOnlySpan3D(void* pointer, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentExceptionForManagedType(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + +#if NET8_0_OR_GREATER + this.reference = ref Unsafe.AsRef(pointer); + this.depth = depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = new ReadOnlySpan(pointer, depth); +#else + this.instance = null; + this.offset = (IntPtr)pointer; + this.depth = depth; +#endif + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } + +#if !NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within the target instance. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map. + /// The row pitch of the 3D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal ReadOnlySpan3D(object? instance, IntPtr offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + this.instance = instance; + this.offset = offset; + this.depth = depth; + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } +#endif + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , , or + /// are invalid. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + /// The total volume must match the length of . + public ReadOnlySpan3D(T[] array, int depth, int height, int width) + : this(array, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The slice pitch in the resulting 3D area. + /// The row pitch in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + public ReadOnlySpan3D(T[] array, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = array.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + +#if NET8_0_OR_GREATER + this.reference = ref array.DangerousGetReferenceAt(offset); + this.depth = depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = MemoryMarshal.CreateReadOnlySpan(ref array.DangerousGetReferenceAt(offset), depth); +#else + this.instance = array; + this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(offset)); + this.depth = depth; +#endif + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + public ReadOnlySpan3D(T[,,]? array) + { + if (array is null) + { + this = default; + + return; + } + +#if NET8_0_OR_GREATER + this.reference = ref array.DangerousGetReference(); + this.depth = array.GetLength(0); +#elif NETSTANDARD2_1_OR_GREATER + this.span = MemoryMarshal.CreateReadOnlySpan(ref array.DangerousGetReference(), array.GetLength(0)); +#else + this.instance = array; + this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(0, 0, 0)); + this.depth = array.GetLength(0); +#endif + this.height = array.GetLength(1); + this.width = this.RowStride = array.GetLength(2); + this.SliceStride = this.RowStride * this.height; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + /// The target slice to map within . + /// The target row to map within . + /// The target column to map within . + /// The depth 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 ReadOnlySpan3D(T[,,]? array, int slice, int row, int column, int depth, int height, int width) + { + if (array is null) + { + if (slice != 0 || row != 0 || column != 0 || depth != 0|| height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + int slices = array.GetLength(0); + int rows = array.GetLength(1); + int columns = array.GetLength(2); + + if ((uint)slice >= (uint)slices) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (uint)(slices - slice)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + +#if NET8_0_OR_GREATER + this.reference = ref array.DangerousGetReferenceAt(slice, row, column); + this.depth = depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReferenceAt(slice, row, column), depth); +#else + this.instance = array; + this.offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(slice, row, column)); + this.depth = depth; +#endif + this.height = height; + this.width = width; + this.RowStride = columns; + this.SliceStride = this.RowStride * rows; + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , , or are invalid. + /// + /// The total volume must match the length of . + internal ReadOnlySpan3D(ReadOnlySpan span, int depth, int height, int width) + : this(span, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The slice pitch in the resulting 3D area. + /// The row pitch in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + internal ReadOnlySpan3D(ReadOnlySpan span, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if ((uint)offset > (uint)span.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = span.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + +#if NET8_0_OR_GREATER + this.reference = ref span.DangerousGetReferenceAt(offset); + this.depth = depth; +#else + this.span = MemoryMarshal.CreateSpan(ref span.DangerousGetReferenceAt(offset), depth); +#endif + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } + + /// + /// Creates a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map (the distance between each slice). + /// The row pitch of the 3D memory area to map (the distance between each row). + /// A instance with the specified parameters. + /// Thrown when one of the parameters are negative. + public static ReadOnlySpan3D DangerousCreate(in T value, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + + return new(in value, depth, height, width, slicePitch, rowPitch); + } +#endif + + /// + /// Gets an empty instance. + /// + public static ReadOnlySpan3D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Depth == 0 || this.height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)Depth * (nint)(uint)this.height * (nint)(uint)this.width; + } + + /// + /// Gets the depth of the underlying 3D memory area. + /// + public int Depth + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NET8_0_OR_GREATER + return this.depth; +#elif NETSTANDARD2_1_OR_GREATER + return this.span.Length; +#else + return this.depth; +#endif + } + } + + /// + /// Gets the height of the underlying 3D memory area. + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.height; + } + + /// + /// Gets the width of the underlying 3D memory area. + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.width; + } + + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target slice to get the element from. + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either , , or are invalid. + /// + public ref readonly T this[int slice, int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if ((uint)slice >= (uint)Depth || + (uint)row >= (uint)this.height || + (uint)column >= (uint)this.width) + { + ThrowHelper.ThrowIndexOutOfRangeException(); + } + + return ref DangerousGetReferenceAt(slice, row, column); + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target slice to get the element from. + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either , , or are invalid. + /// + public ref readonly T this[Index slice, Index row, Index column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this[slice.GetOffset(Depth), row.GetOffset(this.height), column.GetOffset(this.width)]; + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of slices to select. + /// 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 ReadOnlySpan3D this[Range slices, Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + (int slice, int depth) = slices.GetOffsetAndLength(Depth); + (int row, int height) = rows.GetOffsetAndLength(this.height); + (int column, int width) = columns.GetOffsetAndLength(this.width); + + return Slice(slice, row, column, depth, height, width); + } + } +#endif + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span destination) + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out ReadOnlySpan span)) + { + span.CopyTo(destination); + } + else + { + if (Length > destination.Length) + { + ThrowHelper.ThrowArgumentExceptionForDestinationTooShort(); + } + + int depth = Depth; + int offset = 0; + + if (this.SliceStride == this.height * this.RowStride) + { + // Copy one slice at a time + for (int z = 0; z < depth; z++) + { + GetSliceSpan(z).CopyTo(destination.Slice(offset)); + offset += this.height * this.width; + } + } + else + { + // Copy each row individually +#if NETSTANDARD2_1_OR_GREATER + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < this.height; y++) + { + GetRowSpan(z, y).CopyTo(destination.Slice(offset)); + offset += this.width; + } + } +#else + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T sourceStart = ref DangerousGetReferenceAt(z, y, 0); + ref T sourceEnd = ref Unsafe.Add(ref sourceStart, width); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationRef = sourceStart; + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationRef = ref Unsafe.Add(ref destinationRef, 1); + } + } + } +#endif + } + } + } + + /// + /// 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 does not have the same shape as the source instance. + /// + public void CopyTo(Span3D destination) + { + if (destination.Depth != Depth || + destination.Height != this.height || + destination.Width != this.width) + { + ThrowHelper.ThrowArgumentExceptionForDestinationWithNotSameShape(); + } + + if (IsEmpty) + { + return; + } + + if (destination.TryGetSpan(out Span span)) + { + CopyTo(span); + } + else + { + int depth = Depth; + + if (this.SliceStride == this.height * this.RowStride && + destination.SliceStride == destination.Height * destination.RowStride) + { + for (int z = 0; z < depth; z++) + { + GetSliceSpan(z).CopyTo(destination.GetSliceSpan(z)); + } + } + else + { + // Copy each row individually +#if NETSTANDARD2_1_OR_GREATER + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < this.height; y++) + { + GetRowSpan(z, y).CopyTo(destination.GetRowSpan(z, y)); + } + } +#else + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T sourceStart = ref DangerousGetReferenceAt(z, y, 0); + ref T sourceEnd = ref Unsafe.Add(ref sourceStart, width); + ref T destinationRef = ref destination.DangerousGetReferenceAt(z, y, 0); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationRef = sourceStart; + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationRef = ref Unsafe.Add(ref destinationRef, 1); + } + } + } +#endif + } + } + } + + /// + /// 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(Span destination) + { + if (destination.Length >= Length) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// 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(Span3D destination) + { + if (destination.Depth == Depth && + destination.Height == this.height && + destination.Width == this.width) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Returns a reference to the 0th element of the instance. If the current + /// instance is empty, returns a reference. It can be used for pinning + /// and is required to support the use of span within a fixed statement. + /// + /// A reference to the 0th element, or a reference. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe ref readonly T GetPinnableReference() + { + ref readonly T r0 = ref Unsafe.AsRef(null); + + if (Length != 0) + { +#if NET8_0_OR_GREATER + r0 = ref this.reference; +#elif NETSTANDARD2_1_OR_GREATER + r0 = ref MemoryMarshal.GetReference(this.span); +#else + r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + } + + return ref r0; + } + + /// + /// Returns a reference to the first element within the current instance, with no bounds check. + /// + /// A reference to the first element within the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReference() + { +#if NET8_0_OR_GREATER + return ref Unsafe.AsRef(in this.reference); +#elif NETSTANDARD2_1_OR_GREATER + return ref MemoryMarshal.GetReference(this.span); +#else + return ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + } + + /// + /// Returns a reference to a specified element within the current instance, with no bounds check. + /// + /// The target slice to get the element from. + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReferenceAt(int i, int j, int k) + { +#if NET8_0_OR_GREATER + ref T r0 = ref Unsafe.AsRef(in this.reference); +#elif NETSTANDARD2_1_OR_GREATER + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + nint index = ((nint)(uint)i * (nint)(uint)this.SliceStride) + + ((nint)(uint)j * (nint)(uint)this.RowStride) + + (nint)(uint)k; + + return ref Unsafe.Add(ref r0, index); + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target slice to map within the current instance. + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The depth 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 , + /// , , 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. + /// + /// + /// Contrary to , this method will throw an + /// if attempting to perform a slice operation that would result in either axes being 0. That is, trying to call + /// as e.g. Slice(slice: 2, row: 1, column: 0, depth: 1, height: 0, width: 2) + /// on an instance that has 3 slices, 2 rows, and 1 column will throw, rather than returning a new + /// instance with 1 slice, 0 rows and 2 columns. For contrast, trying to e.g. call Slice(start: 1, length: 0) + /// on a instance of length 1 would return a span of length 0, with the internal reference being + /// set to right past the end of the memory. + /// + /// + /// This is by design, and it is due to the internal memory layout that has. That is, in the case + /// of , the only edge case scenario would be to obtain a new span of size 0, referencing the very end + /// of the backing object (e.g. an array or a instance). In that case, the GC can correctly track things. + /// With , on the other hand, it would be possible to slice an instance with a size of 0 in either axis, + /// but with the computed starting reference pointing well past the end of the internal memory area. Such a behavior would not + /// be valid if the reference was pointing to a managed object, and it would cause memory corruptions (i.e. "GC holes"). + /// + /// + /// If you specifically need to be able to obtain empty values from slicing past the valid range, consider performing the range + /// validation yourself (i.e. through some helper method), and then only invoking + /// once the parameters are in the accepted range. Otherwise, consider returning another return explicitly, such as + /// . + /// + /// + public unsafe ReadOnlySpan3D Slice(int slice, int row, int column, int depth, int height, int width) + { + if ((uint)slice >= Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (Depth - slice) || depth == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if ((uint)height > (this.height - row) || height == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (this.width - column) || width == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + nint shift = ((nint)(uint)slice * (nint)(uint)this.SliceStride) + + ((nint)(uint)row * (nint)(uint)this.RowStride) + + (nint)(uint)column; + + int rowPitch = this.RowStride - width; + int slicePitch = this.SliceStride - (height * this.RowStride); + +#if NET8_0_OR_GREATER + ref T r0 = ref Unsafe.Add(ref Unsafe.AsRef(in this.reference), shift); + + return new(in r0, depth, height, width, slicePitch, rowPitch); +#elif NETSTANDARD2_1_OR_GREATER + ref T r0 = ref this.span.DangerousGetReferenceAt(shift); + + return new(in r0, depth, height, width, slicePitch, rowPitch); +#else + IntPtr offset = this.offset + (shift * (nint)(uint)sizeof(T)); + + return new(this.instance, offset, depth, height, width, slicePitch, rowPitch); +#endif + } + + /// + /// Gets a for a specified slice. + /// + /// The index of the target slice to retrieve. + /// Throw when is out of range. + /// The resulting slice . + public unsafe ReadOnlySpan2D GetSliceSpan(int slice) + { + if ((uint)slice >= (uint)Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + int rowPitch = this.RowStride - this.width; + +#if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref DangerousGetReferenceAt(slice, 0, 0); + + return new(in r0, this.height, this.width, rowPitch); +#else + nint shift = (nint)(uint)slice * (nint)(uint)this.SliceStride; + IntPtr offset = this.offset + (shift * (nint)(uint)sizeof(T)); + + return new(this.instance, offset, this.height, this.width, rowPitch); +#endif + } + + /// + /// Gets a for a specified row in a specific slice. + /// + /// The index of the target slice to retrieve. + /// The index of the target row in to retrieve. + /// + /// Throw when either or are out of range. + /// + /// The resulting row . + public unsafe ReadOnlySpan GetRowSpan(int slice, int row) + { + if ((uint)slice >= (uint)Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + +#if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref DangerousGetReferenceAt(slice, row, 0); + + return MemoryMarshal.CreateReadOnlySpan(ref r0, this.width); +#else + nint shift = ((nint)(uint)slice * (nint)(uint)this.SliceStride) + + ((nint)(uint)row * (nint)(uint)this.RowStride); + + if (this.instance is null) + { + return new ReadOnlySpan((void*)(this.offset + (shift * (nint)(uint)sizeof(T))), this.width); + } + + if (this.instance.GetType() == typeof(T[])) + { + T[] array = Unsafe.As(this.instance)!; + ref T r0 = ref ObjectMarshal.DangerousGetObjectDataReferenceAt(array, this.offset + (shift * (nint)(uint)sizeof(T))); + int index = array.AsSpan().IndexOf(ref r0); + + return array.AsSpan(index, this.width); + } + + throw new NotSupportedException("GetRowSpan is not supported for this backing store on NETSTANDARD2.0."); +#endif + } + + /// + /// 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 TryGetSpan(out ReadOnlySpan span) + { + // We can only create a ReadOnlySpan if the buffer is contiguous + if (this.RowStride == this.width && + this.SliceStride == (this.width * this.height) && + Length <= int.MaxValue) + { +#if NET8_0_OR_GREATER + span = MemoryMarshal.CreateReadOnlySpan(in this.reference, (int)Length); + + return true; +#elif NETSTANDARD2_1_OR_GREATER + span = MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(this.span), (int)Length); + + return true; +#else + // An empty ReadOnlySpan3D is still valid + if (IsEmpty) + { + span = default; + + return true; + } + + // Pinned ReadOnlySpan3D + if (this.instance is null) + { + unsafe + { + span = new ReadOnlySpan((void*)this.offset, (int)Length); + } + + return true; + } + + // Without ReadOnlySpan runtime support, we can only get a ReadOnlySpan from a T[] instance + if (this.instance.GetType() == typeof(T[])) + { + T[] array = Unsafe.As(this.instance)!; + int index = array.AsSpan().IndexOf(ref ObjectMarshal.DangerousGetObjectDataReferenceAt(array, this.offset)); + + span = array.AsSpan(index, (int)Length); + + return true; + } +#endif + } + + span = default; + + return false; + } + + /// + /// Copies the contents of the current instance into a new 3D array. + /// + /// A 3D array containing the data in the current instance. + public T[,,] ToArray() + { + T[,,] array = new T[Depth, this.height, this.width]; + +#if NETSTANDARD2_1_OR_GREATER + CopyTo(array.AsSpan()); +#else + // Skip the initialization if the array is empty + if (Length > 0) + { + int depth = Depth; + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref array.DangerousGetReference(); + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T sourceStart = ref DangerousGetReferenceAt(z, y, 0); + ref T sourceEnd = ref Unsafe.Add(ref sourceStart, width); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationRef = sourceStart; + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationRef = ref Unsafe.Add(ref destinationRef, 1); + } + } + } + } +#endif + + return array; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Equals() on Span will always throw an exception. Use == instead.")] + public override bool Equals(object? obj) + { + throw new NotSupportedException("CommunityToolkit.HighPerformance.ReadOnlySpan3D.Equals(object) is not supported."); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("GetHashCode() on Span will always throw an exception.")] + public override int GetHashCode() + { + throw new NotSupportedException("CommunityToolkit.HighPerformance.ReadOnlySpan3D.GetHashCode() is not supported."); + } + + /// + public override string ToString() + { + return $"CommunityToolkit.HighPerformance.ReadOnlySpan3D<{typeof(T)}>[{Depth}, {this.height}, {this.width}]"; + } + + /// + /// Checks whether two instances are equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are equal. + public static bool operator ==(ReadOnlySpan3D left, ReadOnlySpan3D right) + { + return +#if NET8_0_OR_GREATER + Unsafe.AreSame(ref Unsafe.AsRef(in left.reference), ref Unsafe.AsRef(in right.reference)) && + left.depth == right.depth && +#elif NETSTANDARD2_1_OR_GREATER + left.span == right.span && +#else + ReferenceEquals( + left.instance, right.instance) && + left.offset == right.offset && + left.depth == right.depth && +#endif + left.height == right.height && + left.width == right.width && + left.RowStride == right.RowStride && + left.SliceStride == right.SliceStride; + } + + /// + /// Checks whether two instances are not equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are not equal. + public static bool operator !=(ReadOnlySpan3D left, ReadOnlySpan3D right) + { + return !(left == right); + } + + /// + /// Implicitly converts a given 3D array into a instance. + /// + /// The input 3D array to convert. + public static implicit operator ReadOnlySpan3D(T[,,]? array) => new(array); + + /// + /// Implicitly converts a given into a instance. + /// + /// The input to convert. + public static implicit operator ReadOnlySpan3D(Span3D span) + { + int rowPitch = span.RowStride - span.Width; + int slicePitch = span.SliceStride - (rowPitch * span.Height); + +#if NETSTANDARD2_1_OR_GREATER + return new(in span.DangerousGetReference(), span.Depth, span.Height, span.Width, slicePitch, rowPitch); +#else + return new(span.Instance!, span.Offset, span.Depth, span.Height, span.Width, slicePitch, rowPitch); +#endif + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.Enumerator.cs b/src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.Enumerator.cs new file mode 100644 index 00000000..4fda2253 --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.Enumerator.cs @@ -0,0 +1,298 @@ +// 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. + +#if !NET8_0_OR_GREATER +using System; +#endif +using System.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.Enumerables; +using CommunityToolkit.HighPerformance.Memory.Internals; +#if NETSTANDARD2_1_OR_GREATER && !NET8_0_OR_GREATER +using System.Runtime.InteropServices; +#elif NETSTANDARD2_0 +using RuntimeHelpers = CommunityToolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +namespace CommunityToolkit.HighPerformance; + +/// +partial struct Span3D +{ + /// + /// Gets an enumerable that traverses items in a specified row of a specific slice. + /// + /// The target slice to enumerate within the current instance. + /// The target row to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RefEnumerable GetRow(int slice, int row) + { + if ((uint)slice >= Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + nint startIndex = (nint)(uint)this.SliceStride * (nint)(uint)slice + + (nint)(uint)this.RowStride * (nint)(uint)row; + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if NETSTANDARD2_1_OR_GREATER + return new(ref r1, length: this.width, step: 1); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.Instance, ref r1); + + return new(this.Instance, offset, length: this.width, step: 1); +#endif + } + + /// + /// Gets an enumerable that traverses items in a specified column of a specific slice. + /// + /// The target slice to enumerate within the current instance. + /// The target column to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RefEnumerable GetColumn(int slice,int column) + { + if ((uint)slice >= Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + nint startIndex = (nint)(uint)this.SliceStride * (nint)(uint)slice + column; + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if NETSTANDARD2_1_OR_GREATER + return new(ref r1, length: this.height, step: this.RowStride); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.Instance, ref r1); + + return new(this.Instance, offset, length: this.height, step: this.RowStride); +#endif + } + + /// + /// Gets an enumerable that traverses items in a specified row along slices. + /// + /// The target row to enumerate within the current instance. + /// The target column to enumerate within the current instance. + /// A with target items to enumerate. + /// The returned value shouldn't be used directly: use this extension in a loop. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public RefEnumerable GetDepthColumn(int row, int column) + { + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + nint startIndex = (nint)(uint)this.RowStride * (nint)(uint)row + column; + + ref T r0 = ref DangerousGetReference(); + ref T r1 = ref Unsafe.Add(ref r0, startIndex); + +#if NETSTANDARD2_1_OR_GREATER + return new(ref r1, length: Depth, step: this.SliceStride); +#else + IntPtr offset = RuntimeHelpers.GetObjectDataOrReferenceByteOffset(this.Instance, ref r1); + + return new(this.Instance, offset, length: Depth, step: this.SliceStride); +#endif + } + + /// + /// Returns an enumerator for the current instance. + /// + /// + /// An enumerator that can be used to traverse the items in the current instance + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator GetEnumerator() => new(this); + + /// + /// Provides an enumerator for the elements of a instance. + /// + public ref struct Enumerator + { +#if NET8_0_OR_GREATER + /// + /// The reference for the instance. + /// + private readonly ref T reference; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#elif NETSTANDARD2_1_OR_GREATER + /// + /// The instance pointing to the first item in the target memory area. + /// + /// Just like in , the length is the depth of the 3D region. + private readonly Span span; +#else + /// + /// The target instance, if present. + /// + private readonly object? instance; + + /// + /// The initial byte offset within . + /// + private readonly nint offset; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#endif + + /// + /// The height of the specified 3D region. + /// + private readonly int height; + + /// + /// The width of the specified 3D region. + /// + private readonly int width; + + /// + /// The row stride of the specified 3D region. + /// + private readonly int rowStride; + + /// + /// The slice stride of the specified 3D region. + /// + private readonly int sliceStride; + + /// + /// The current horizontal offset. + /// + private int x; + + /// + /// The current vertical offset. + /// + private int y; + + /// + /// The current slice offset. + /// + private int z; + + /// + /// Initializes a new instance of the struct. + /// + /// The target instance to enumerate. + internal Enumerator(Span3D span) + { +#if NET8_0_OR_GREATER + this.reference = ref span.reference; + this.depth = span.depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = span.span; +#else + this.instance = span.Instance; + this.offset = span.Offset; + this.depth = span.depth; +#endif + this.height = span.height; + this.width = span.width; + this.rowStride = span.RowStride; + this.sliceStride = span.SliceStride; + this.x = -1; + this.y = 0; + this.z = 0; + } + + /// + /// Implements the duck-typed method. + /// + /// whether a new element is available, otherwise + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() + { + int x = this.x + 1; + + // Horizontal move, within range + if (x < this.width) + { + this.x = x; + + return true; + } + + // We reached the end of a row and there is at least + // another row available: wrap to a new line and continue. + this.x = 0; + int y = this.y + 1; + + if (y < this.height) + { + this.y = y; + + return true; + } + + // We reached the end of a slice and there is at least + // another slice available: wrap to the start of the next one and continue. + this.y = 0; + this.z++; + +#if NET8_0_OR_GREATER + return this.z < this.depth; +#elif NETSTANDARD2_1_OR_GREATER + return this.z < this.span.Length; +#else + return this.z < this.depth; +#endif + } + + /// + /// Gets the duck-typed property. + /// + public readonly ref T Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NET8_0_OR_GREATER + ref T r0 = ref this.reference; +#elif NETSTANDARD2_1_OR_GREATER + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.instance, this.offset); +#endif + nint index = ((nint)(uint)this.z * (nint)(uint)this.sliceStride) + + ((nint)(uint)this.y * (nint)(uint)this.rowStride) + + (nint)(uint)this.x; + + return ref Unsafe.Add(ref r0, index); + } + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.cs b/src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.cs new file mode 100644 index 00000000..1d13becd --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Memory/Span3D{T}.cs @@ -0,0 +1,1430 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +#if !NETSTANDARD2_1_OR_GREATER +using CommunityToolkit.HighPerformance.Helpers; +#endif +using CommunityToolkit.HighPerformance.Memory.Internals; +using CommunityToolkit.HighPerformance.Memory.Views; +#if !NETSTANDARD2_1_OR_GREATER +using RuntimeHelpers = CommunityToolkit.HighPerformance.Helpers.Internals.RuntimeHelpers; +#endif + +#pragma warning disable CS0809, CA1065 + +namespace CommunityToolkit.HighPerformance; + +/// +/// represents a 3D region of arbitrary memory. Like the type, +/// it can point to either managed or native memory, or to memory allocated on the stack. It is type- and memory-safe. +/// One key difference with and arrays is that the underlying buffer for a +/// instance might not be contiguous in memory: this is supported to enable mapping arbitrary 3D regions even if they +/// require padding between boundaries of sequential rows. All this logic is handled internally by the +/// type and it is transparent to the user, but note that working over discontiguous buffers has a performance impact. +/// +/// The type of items in the current instance. +[DebuggerTypeProxy(typeof(MemoryDebugView3D<>))] +[DebuggerDisplay("{ToString(),raw}")] +public readonly ref partial struct Span3D +{ + // Let's consider a representation of a discontiguous 3D memory + // region within an existing array. The data is represented as + // an array of 2D slices, each in row-major order as usual, and + // the 'XX' grid cells represent locations that are mapped by a + // given Span3D instance for one of those slices: + // + // _____________________stride_____... + // reference__ /________width_________ ________... + // \/ \/ + // | -- | -- | |- | -- | -- | -- | -- | -- | -- | -- |_ + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | | + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | | + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- | |_height + // | -- | -- | XX | XX | XX | XX | XX | XX | -- | -- |_| + // | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | + // | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | + // ...__pitch__/ + // ...________/ + // + // The pitch is used to calculate the offset between each + // discontiguous row in a slice, so that any arbitrary memory + // locations can be used to internally represent a 3D span. + // This gives users much more flexibility when creating spans + // from data. + // + // This is also extended to 3D, where each slice has a slice pitch + // that is used to calculate the offset between each discontinuous + // slice in the 3D memory region. +#if NET8_0_OR_GREATER + /// + /// The reference for the instance. + /// + private readonly ref T reference; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#elif NETSTANDARD2_1_OR_GREATER + /// + /// The instance pointing to the first item in the target memory area. + /// + /// + /// The field maps to the depth of the 3D region. + /// This is done to save 4 bytes in the layout of the type. + /// + private readonly Span span; +#else + /// + /// The target instance, if present. + /// + internal readonly object? Instance; + + /// + /// The initial byte offset within . + /// + internal readonly nint Offset; + + /// + /// The depth of the specified 3D region. + /// + private readonly int depth; +#endif + + /// + /// The height of the specified 3D region. + /// + private readonly int height; + + /// + /// The width of the specified 3D region. + /// + private readonly int width; + + /// + /// The slice stride of the specified 3D region. + /// + /// + /// This combines both the slice size (RowStride and height) and the slice pitch + /// in a single value so that the indexing logic can be simplified + /// (no need to recompute the sum every time) and be faster. + /// + internal readonly int SliceStride; + + /// + /// The row stride of the specified 3D region. + /// + /// + /// This combines both the width and row pitch in a single value so that the indexing + /// logic can be simplified (no need to recompute the sum every time) and be faster. + /// + internal readonly int RowStride; + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map (the distance between each slice). + /// The row pitch of the 3D memory area to map (the distance between each row). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Span3D(ref T value, int depth, int height, int width, int slicePitch, int rowPitch) + { +#if NET8_0_OR_GREATER + this.reference = ref value; + this.depth = depth; +#else + this.span = MemoryMarshal.CreateSpan(ref value, depth); +#endif + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } +#endif + + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The pointer to the first item to map. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map (the distance between each slice). + /// The pitch of the 3D memory area to map (the distance between each row). + /// Thrown when one of the parameters are negative. + public unsafe Span3D(void* pointer, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ThrowHelper.ThrowArgumentExceptionForManagedType(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + +#if NET8_0_OR_GREATER + this.reference = ref Unsafe.AsRef(pointer); + this.depth = depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = new Span(pointer, depth); +#else + this.Instance = null; + this.Offset = (IntPtr)pointer; + this.depth = depth; +#endif + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } + +#if !NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct with the specified parameters. + /// + /// The target instance. + /// The initial offset within the target instance. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map. + /// The row pitch of the 3D memory area to map. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal Span3D(object? instance, IntPtr offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + this.Instance = instance; + this.Offset = offset; + this.depth = depth; + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } +#endif + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , , or + /// are invalid. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + /// The total volume must match the length of . + public Span3D(T[] array, int depth, int height, int width) + : this(array, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The slice pitch in the resulting 3D area. + /// The row pitch in the resulting 3D area. + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + public Span3D(T[] array, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + if ((uint)offset > (uint)array.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = array.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + +#if NET8_0_OR_GREATER + this.reference = ref array.DangerousGetReferenceAt(offset); + this.depth = depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReferenceAt(offset), depth); +#else + this.Instance = array; + this.Offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(offset)); + this.depth = depth; +#endif + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + /// + /// Thrown when doesn't match . + /// + public Span3D(T[,,]? array) + { + if (array is null) + { + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + +#if NET8_0_OR_GREATER + this.reference = ref array.DangerousGetReference(); + this.depth = array.GetLength(0); +#elif NETSTANDARD2_1_OR_GREATER + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReference(), array.GetLength(0)); +#else + this.Instance = array; + this.Offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(0, 0, 0)); + this.depth = array.GetLength(0); +#endif + this.height = array.GetLength(1); + this.width = this.RowStride = array.GetLength(2); + this.SliceStride = this.RowStride * this.height; + } + + /// + /// Initializes a new instance of the struct wrapping a 3D array. + /// + /// The given 3D array to wrap. + /// The target slice to map within . + /// The target row to map within . + /// The target column to map within . + /// The depth to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , , , + /// , , or + /// are negative or not within the bounds that are valid for . + /// + public Span3D(T[,,]? array, int slice, int row, int column, int depth, int height, int width) + { + if (array is null) + { + if (slice != 0 || row != 0 || column != 0 || depth != 0|| height != 0 || width != 0) + { + ThrowHelper.ThrowArgumentException(); + } + + this = default; + + return; + } + + if (array.IsCovariant()) + { + ThrowHelper.ThrowArrayTypeMismatchException(); + } + + int slices = array.GetLength(0); + int rows = array.GetLength(1); + int columns = array.GetLength(2); + + if ((uint)slice >= (uint)slices) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)rows) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)columns) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (uint)(slices - slice)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if ((uint)height > (uint)(rows - row)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (uint)(columns - column)) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + +#if NET8_0_OR_GREATER + this.reference = ref array.DangerousGetReferenceAt(slice, row, column); + this.depth = depth; +#elif NETSTANDARD2_1_OR_GREATER + this.span = MemoryMarshal.CreateSpan(ref array.DangerousGetReferenceAt(slice, row, column), depth); +#else + this.Instance = array; + this.Offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref array.DangerousGetReferenceAt(slice, row, column)); + this.depth = depth; +#endif + this.height = height; + this.width = width; + this.RowStride = columns; + this.SliceStride = this.RowStride * rows; + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// + /// Thrown when either , , or are invalid. + /// + /// The total volume must match the length of . + internal Span3D(Span span, int depth, int height, int width) + : this(span, offset: 0, depth, height, width, slicePitch: 0, rowPitch: 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target to wrap. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The slice pitch in the resulting 3D area. + /// The row pitch in the resulting 3D area. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + internal Span3D(Span span, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + if ((uint)offset > (uint)span.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForOffset(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + int volume = OverflowHelper.ComputeInt32Volume(depth, height, width, slicePitch, rowPitch); + int remaining = span.Length - offset; + + if (volume > remaining) + { + ThrowHelper.ThrowArgumentException(); + } + +#if NET8_0_OR_GREATER + this.reference = ref span.DangerousGetReferenceAt(offset); + this.depth = depth; +#else + this.span = MemoryMarshal.CreateSpan(ref span.DangerousGetReferenceAt(offset), depth); +#endif + this.height = height; + this.width = width; + this.RowStride = width + rowPitch; + this.SliceStride = (this.RowStride * height) + slicePitch; + } + + /// + /// Creates a new instance of the struct with the specified parameters. + /// + /// The reference to the first item to map. + /// The depth of the 3D memory area to map. + /// The height of the 3D memory area to map. + /// The width of the 3D memory area to map. + /// The slice pitch of the 3D memory area to map (the distance between each slice). + /// The row pitch of the 3D memory area to map (the distance between each row). + /// A instance with the specified parameters. + /// Thrown when one of the parameters are negative. + public static Span3D DangerousCreate(ref T value, int depth, int height, int width, int slicePitch, int rowPitch) + { + if (width < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + if (height < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if (depth < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if (slicePitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlicePitch(); + } + + if (rowPitch < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRowPitch(); + } + + OverflowHelper.EnsureIsInNativeIntRange(depth, height, width, slicePitch, rowPitch); + + return new(ref value, depth, height, width, slicePitch, rowPitch); + } +#endif + + /// + /// Gets an empty instance. + /// + public static Span3D Empty => default; + + /// + /// Gets a value indicating whether the current instance is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Depth == 0 || this.height == 0 || this.width == 0; + } + + /// + /// Gets the length of the current instance. + /// + public nint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)(uint)Depth * (nint)(uint)this.height * (nint)(uint)this.width; + } + + /// + /// Gets the depth of the underlying 3D memory area. + /// + public int Depth + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { +#if NET8_0_OR_GREATER + return this.depth; +#elif NETSTANDARD2_1_OR_GREATER + return this.span.Length; +#else + return this.depth; +#endif + } + } + + /// + /// Gets the height of the underlying 3D memory area. + /// + public int Height + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.height; + } + + /// + /// Gets the width of the underlying 3D memory area. + /// + public int Width + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.width; + } + + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target slice to get the element from. + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either , , or are invalid. + /// + public ref T this[int slice, int row, int column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + if ((uint)slice >= (uint)Depth || + (uint)row >= (uint)this.height || + (uint)column >= (uint)this.width) + { + ThrowHelper.ThrowIndexOutOfRangeException(); + } + + return ref DangerousGetReferenceAt(slice, row, column); + } + } + +#if NETSTANDARD2_1_OR_GREATER + /// + /// Gets the element at the specified zero-based indices. + /// + /// The target slice to get the element from. + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + /// + /// Thrown when either , , or are invalid. + /// + public ref T this[Index slice, Index row, Index column] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref this[slice.GetOffset(Depth), row.GetOffset(this.height), column.GetOffset(this.width)]; + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target range of slices to select. + /// 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 Span3D this[Range slices, Range rows, Range columns] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + (int slice, int depth) = slices.GetOffsetAndLength(Depth); + (int row, int height) = rows.GetOffsetAndLength(this.height); + (int column, int width) = columns.GetOffsetAndLength(this.width); + + return Slice(slice, row, column, depth, height, width); + } + } +#endif + + /// + /// Clears the contents of the current instance. + /// + public void Clear() + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out Span span)) + { + span.Clear(); + } + else + { + int depth = Depth; + + if (this.SliceStride == this.height * this.RowStride) + { + // Clear one slice at a time + for (int z = 0; z < depth; z++) + { + GetSliceSpan(z).Clear(); + } + } + else + { + // Clear row by row +#if NETSTANDARD2_1_OR_GREATER + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < this.height; y++) + { + GetRowSpan(z, y).Clear(); + } + } +#else + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T rStart = ref DangerousGetReferenceAt(z, y, 0); + ref T rEnd = ref Unsafe.Add(ref rStart, width); + + while (Unsafe.IsAddressLessThan(ref rStart, ref rEnd)) + { + rStart = default!; + + rStart = ref Unsafe.Add(ref rStart, 1); + } + } + } +#endif + } + } + } + + /// + /// Copies the contents of this into a destination instance. + /// + /// The destination instance. + /// + /// Thrown when is shorter than the source instance. + /// + public void CopyTo(Span destination) + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out Span span)) + { + span.CopyTo(destination); + } + else + { + if (Length > destination.Length) + { + ThrowHelper.ThrowArgumentExceptionForDestinationTooShort(); + } + + int depth = Depth; + int offset = 0; + + if (this.SliceStride == this.height * this.RowStride) + { + // Copy one slice at a time + for (int z = 0; z < depth; z++) + { + GetSliceSpan(z).CopyTo(destination.Slice(offset)); + offset += this.height * this.width; + } + } + else + { + // Copy each row individually +#if NETSTANDARD2_1_OR_GREATER + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < this.height; y++) + { + GetRowSpan(z, y).CopyTo(destination.Slice(offset)); + offset += this.width; + } + } +#else + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref MemoryMarshal.GetReference(destination); + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T sourceStart = ref DangerousGetReferenceAt(z, y, 0); + ref T sourceEnd = ref Unsafe.Add(ref sourceStart, width); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationRef = sourceStart; + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationRef = ref Unsafe.Add(ref destinationRef, 1); + } + } + } +#endif + } + } + } + + /// + /// 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 does not have the same shape as the source instance. + /// + public void CopyTo(Span3D destination) + { + if (destination.Depth != Depth || + destination.height != this.height || + destination.width != this.width) + { + ThrowHelper.ThrowArgumentExceptionForDestinationWithNotSameShape(); + } + + if (IsEmpty) + { + return; + } + + if (destination.TryGetSpan(out Span span)) + { + CopyTo(span); + } + else + { + int depth = Depth; + + if (this.SliceStride == this.height * this.RowStride && + destination.SliceStride == destination.Height * destination.RowStride) + { + for (int z = 0; z < depth; z++) + { + GetSliceSpan(z).CopyTo(destination.GetSliceSpan(z)); + } + } + else + { + // Copy each row individually +#if NETSTANDARD2_1_OR_GREATER + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < this.height; y++) + { + GetRowSpan(z, y).CopyTo(destination.GetRowSpan(z, y)); + } + } +#else + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T sourceStart = ref DangerousGetReferenceAt(z, y, 0); + ref T sourceEnd = ref Unsafe.Add(ref sourceStart, width); + ref T destinationRef = ref destination.DangerousGetReferenceAt(z, y, 0); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationRef = sourceStart; + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationRef = ref Unsafe.Add(ref destinationRef, 1); + } + } + } +#endif + } + } + } + + /// + /// 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(Span destination) + { + if (destination.Length >= Length) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// 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(Span3D destination) + { + if (destination.Depth == Depth && + destination.height == this.height && + destination.width == this.width) + { + CopyTo(destination); + + return true; + } + + return false; + } + + /// + /// Fills the elements of this span with a specified value. + /// + /// The value to assign to each element of the instance. + public void Fill(T value) + { + if (IsEmpty) + { + return; + } + + if (TryGetSpan(out Span span)) + { + span.Fill(value); + } + else + { + int depth = Depth; + + if (this.SliceStride == this.height * this.RowStride) + { + // Fill one slice at a time + for (int z = 0; z < depth; z++) + { + GetSliceSpan(z).Fill(value); + } + } + else + { + // Fill row by row +#if NETSTANDARD2_1_OR_GREATER + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < this.height; y++) + { + GetRowSpan(z, y).Fill(value); + } + } +#else + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T rStart = ref DangerousGetReferenceAt(z, y, 0); + ref T rEnd = ref Unsafe.Add(ref rStart, width); + + while (Unsafe.IsAddressLessThan(ref rStart, ref rEnd)) + { + rStart = value; + + rStart = ref Unsafe.Add(ref rStart, 1); + } + } + } +#endif + } + } + } + + /// + /// Returns a reference to the 0th element of the instance. If the current + /// instance is empty, returns a reference. It can be used for pinning + /// and is required to support the use of span within a fixed statement. + /// + /// A reference to the 0th element, or a reference. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [EditorBrowsable(EditorBrowsableState.Never)] + public unsafe ref T GetPinnableReference() + { + ref T r0 = ref Unsafe.AsRef(null); + + if (Length != 0) + { +#if NET8_0_OR_GREATER + r0 = ref this.reference; +#elif NETSTANDARD2_1_OR_GREATER + r0 = ref MemoryMarshal.GetReference(this.span); +#else + r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); +#endif + } + + return ref r0; + } + + /// + /// Returns a reference to the first element within the current instance, with no bounds check. + /// + /// A reference to the first element within the current instance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReference() + { +#if NET8_0_OR_GREATER + return ref this.reference; +#elif NETSTANDARD2_1_OR_GREATER + return ref MemoryMarshal.GetReference(this.span); +#else + return ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); +#endif + } + + /// + /// Returns a reference to a specified element within the current instance, with no bounds check. + /// + /// The target slice to get the element from. + /// The target row to get the element from. + /// The target column to get the element from. + /// A reference to the element at the specified indices. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ref T DangerousGetReferenceAt(int i, int j, int k) + { +#if NET8_0_OR_GREATER + ref T r0 = ref this.reference; +#elif NETSTANDARD2_1_OR_GREATER + ref T r0 = ref MemoryMarshal.GetReference(this.span); +#else + ref T r0 = ref RuntimeHelpers.GetObjectDataAtOffsetOrPointerReference(this.Instance, this.Offset); +#endif + nint index = ((nint)(uint)i * (nint)(uint)this.SliceStride) + + ((nint)(uint)j * (nint)(uint)this.RowStride) + + (nint)(uint)k; + + return ref Unsafe.Add(ref r0, index); + } + + /// + /// Slices the current instance with the specified parameters. + /// + /// The target slice to map within the current instance. + /// The target row to map within the current instance. + /// The target column to map within the current instance. + /// The depth 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 , + /// , , 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. + /// + /// + /// Contrary to , this method will throw an + /// if attempting to perform a slice operation that would result in either axes being 0. That is, trying to call + /// as e.g. Slice(slice: 2, row: 1, column: 0, depth: 1, height: 0, width: 2) + /// on an instance that has 3 slices, 2 rows, and 1 column will throw, rather than returning a new + /// instance with 1 slice, 0 rows and 2 columns. For contrast, trying to e.g. call Slice(start: 1, length: 0) + /// on a instance of length 1 would return a span of length 0, with the internal reference being + /// set to right past the end of the memory. + /// + /// + /// This is by design, and it is due to the internal memory layout that has. That is, in the case + /// of , the only edge case scenario would be to obtain a new span of size 0, referencing the very end + /// of the backing object (e.g. an array or a instance). In that case, the GC can correctly track things. + /// With , on the other hand, it would be possible to slice an instance with a size of 0 in either axis, + /// but with the computed starting reference pointing well past the end of the internal memory area. Such a behavior would not + /// be valid if the reference was pointing to a managed object, and it would cause memory corruptions (i.e. "GC holes"). + /// + /// + /// If you specifically need to be able to obtain empty values from slicing past the valid range, consider performing the range + /// validation yourself (i.e. through some helper method), and then only invoking + /// once the parameters are in the accepted range. Otherwise, consider returning another return explicitly, such as + /// . + /// + /// + public unsafe Span3D Slice(int slice, int row, int column, int depth, int height, int width) + { + if ((uint)slice >= Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= this.width) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForColumn(); + } + + if ((uint)depth > (Depth - slice) || depth == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForDepth(); + } + + if ((uint)height > (this.height - row) || height == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForHeight(); + } + + if ((uint)width > (this.width - column) || width == 0) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForWidth(); + } + + nint shift = ((nint)(uint)slice * (nint)(uint)this.SliceStride) + + ((nint)(uint)row * (nint)(uint)this.RowStride) + + (nint)(uint)column; + + int rowPitch = this.RowStride - width; + int slicePitch = this.SliceStride - (height * this.RowStride); + +#if NET8_0_OR_GREATER + ref T r0 = ref Unsafe.Add(ref this.reference, shift); + + return new(ref r0, depth, height, width, slicePitch, rowPitch); +#elif NETSTANDARD2_1_OR_GREATER + ref T r0 = ref this.span.DangerousGetReferenceAt(shift); + + return new(ref r0, depth, height, width, slicePitch, rowPitch); +#else + IntPtr offset = this.Offset + (shift * (nint)(uint)sizeof(T)); + + return new(this.Instance, offset, depth, height, width, slicePitch, rowPitch); +#endif + } + + /// + /// Gets a for a specified slice. + /// + /// The index of the target slice to retrieve. + /// Throw when is out of range. + /// The resulting slice . + public unsafe Span2D GetSliceSpan(int slice) + { + if ((uint)slice >= (uint)Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + int rowPitch = this.RowStride - this.width; + +#if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref DangerousGetReferenceAt(slice, 0, 0); + + return new(ref r0, this.height, this.width, rowPitch); +#else + nint shift = (nint)(uint)slice * (nint)(uint)this.SliceStride; + IntPtr offset = this.Offset + (shift * (nint)(uint)sizeof(T)); + + return new(this.Instance, offset, this.height, this.width, rowPitch); +#endif + } + + /// + /// Gets a for a specified row in a specific slice. + /// + /// The index of the target slice to retrieve. + /// The index of the target row in to retrieve. + /// + /// Throw when either or are out of range. + /// + /// The resulting row . + public unsafe Span GetRowSpan(int slice, int row) + { + if ((uint)slice >= (uint)Depth) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)this.height) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionForRow(); + } + +#if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref DangerousGetReferenceAt(slice, row, 0); + + return MemoryMarshal.CreateSpan(ref r0, this.width); +#else + nint shift = ((nint)(uint)slice * (nint)(uint)this.SliceStride) + + ((nint)(uint)row * (nint)(uint)this.RowStride); + + if (this.Instance is null) + { + return new Span((void*)(this.Offset + (shift * (nint)(uint)sizeof(T))), this.width); + } + + if (this.Instance.GetType() == typeof(T[])) + { + T[] array = Unsafe.As(this.Instance)!; + ref T r0 = ref ObjectMarshal.DangerousGetObjectDataReferenceAt(array, this.Offset + (shift * (nint)(uint)sizeof(T))); + int index = array.AsSpan().IndexOf(ref r0); + + return array.AsSpan(index, this.width); + } + + throw new NotSupportedException("GetRowSpan is not supported for this backing store on NETSTANDARD2.0."); +#endif + } + + /// + /// 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 TryGetSpan(out Span span) + { + // We can only create a Span if the buffer is contiguous + if (this.RowStride == this.width && + this.SliceStride == (this.width * this.height) && + Length <= int.MaxValue) + { +#if NET8_0_OR_GREATER + span = MemoryMarshal.CreateSpan(ref this.reference, (int)Length); + + return true; +#elif NETSTANDARD2_1_OR_GREATER + span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(this.span), (int)Length); + + return true; +#else + // An empty Span3D is still valid + if (IsEmpty) + { + span = default; + + return true; + } + + // Pinned Span3D + if (this.Instance is null) + { + unsafe + { + span = new Span((void*)this.Offset, (int)Length); + } + + return true; + } + + // Without Span runtime support, we can only get a Span from a T[] instance + if (this.Instance.GetType() == typeof(T[])) + { + T[] array = Unsafe.As(this.Instance)!; + int index = array.AsSpan().IndexOf(ref ObjectMarshal.DangerousGetObjectDataReferenceAt(array, this.Offset)); + + span = array.AsSpan(index, (int)Length); + + return true; + } +#endif + } + + span = default; + + return false; + } + + /// + /// Copies the contents of the current instance into a new 3D array. + /// + /// A 3D array containing the data in the current instance. + public T[,,] ToArray() + { + T[,,] array = new T[Depth, this.height, this.width]; + +#if NETSTANDARD2_1_OR_GREATER + CopyTo(array.AsSpan()); +#else + // Skip the initialization if the array is empty + if (Length > 0) + { + int depth = Depth; + nint height = (nint)(uint)this.height; + nint width = (nint)(uint)this.width; + + ref T destinationRef = ref array.DangerousGetReference(); + + for (int z = 0; z < depth; z++) + { + for (int y = 0; y < height; y++) + { + ref T sourceStart = ref DangerousGetReferenceAt(z, y, 0); + ref T sourceEnd = ref Unsafe.Add(ref sourceStart, width); + + while (Unsafe.IsAddressLessThan(ref sourceStart, ref sourceEnd)) + { + destinationRef = sourceStart; + + sourceStart = ref Unsafe.Add(ref sourceStart, 1); + destinationRef = ref Unsafe.Add(ref destinationRef, 1); + } + } + } + } +#endif + + return array; + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Equals() on Span will always throw an exception. Use == instead.")] + public override bool Equals(object? obj) + { + throw new NotSupportedException("CommunityToolkit.HighPerformance.Span3D.Equals(object) is not supported."); + } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("GetHashCode() on Span will always throw an exception.")] + public override int GetHashCode() + { + throw new NotSupportedException("CommunityToolkit.HighPerformance.Span3D.GetHashCode() is not supported."); + } + + /// + public override string ToString() + { + return $"CommunityToolkit.HighPerformance.Span3D<{typeof(T)}>[{Depth}, {this.height}, {this.width}]"; + } + + /// + /// Checks whether two instances are equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are equal. + public static bool operator ==(Span3D left, Span3D right) + { + return +#if NET8_0_OR_GREATER + Unsafe.AreSame(ref left.reference, ref right.reference) && + left.depth == right.depth && +#elif NETSTANDARD2_1_OR_GREATER + left.span == right.span && +#else + ReferenceEquals( + left.Instance, right.Instance) && + left.Offset == right.Offset && + left.depth == right.depth && +#endif + left.height == right.height && + left.width == right.width && + left.RowStride == right.RowStride && + left.SliceStride == right.SliceStride; + } + + /// + /// Checks whether two instances are not equal. + /// + /// The first instance to compare. + /// The second instance to compare. + /// Whether or not and are not equal. + public static bool operator !=(Span3D left, Span3D right) + { + return !(left == right); + } + + /// + /// Implicitly converts a given 3D array into a instance. + /// + /// The input 3D array to convert. + public static implicit operator Span3D(T[,,]? array) => new(array); +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Memory/Views/MemoryDebugView3D{T}.cs b/src/CommunityToolkit.HighPerformance/Memory/Views/MemoryDebugView3D{T}.cs new file mode 100644 index 00000000..e9185126 --- /dev/null +++ b/src/CommunityToolkit.HighPerformance/Memory/Views/MemoryDebugView3D{T}.cs @@ -0,0 +1,56 @@ +// 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.Diagnostics; + +namespace CommunityToolkit.HighPerformance.Memory.Views; + +/// +/// A debug proxy used to display items in a 3D layout. +/// +/// The type of items to display. +internal sealed class MemoryDebugView3D +{ + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView3D(Memory3D memory) + { + Items = memory.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView3D(ReadOnlyMemory3D memory) + { + Items = memory.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView3D(Span3D span) + { + Items = span.ToArray(); + } + + /// + /// Initializes a new instance of the class with the specified parameters. + /// + /// The input instance with the items to display. + public MemoryDebugView3D(ReadOnlySpan3D span) + { + Items = span.ToArray(); + } + + /// + /// Gets the items to display for the current instance + /// + [DebuggerBrowsable(DebuggerBrowsableState.Collapsed)] + public T[,,]? Items { get; } +} From 9c5d9bf68303315bd73c7d88e4fb50c88709cd46 Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Thu, 12 Feb 2026 18:42:24 +0100 Subject: [PATCH 3/9] Correct the way 3D indices are calculated --- .../Memory/Internals/OverflowHelper.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs b/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs index e8d10d84..1cc49faa 100644 --- a/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs +++ b/src/CommunityToolkit.HighPerformance/Memory/Internals/OverflowHelper.cs @@ -84,10 +84,11 @@ public static void EnsureIsInNativeIntRange(int depth, int height, int width, in // Refer to the explanation above for the Memory2D and Span2D types. // For the Memory3D and Span3D types it is similar, except we now have a "volume" // consisting of one or more 2D slices. For these 2D slices, rowPitch is the distance - // between consecutive rows. slicePitch is the distance between consecutive slices. + // between the end of a row and the start of the next row. Similarly, slicePitch is + // the distance between the end of a slice and the start of the next slice. // Note that we're also subtracting 1 to the depth as we don't want to include the trailing pitch // for the 3D memory area. - _ = checked(((nint)slicePitch * Max(unchecked(depth - 1), 0)) + + _ = checked(((nint)(((width + rowPitch) * height) + slicePitch) * Max(unchecked(depth - 1), 0)) + ((nint)(width + rowPitch) * Max(unchecked(height - 1), 0)) + Max(unchecked(width - 1), 0)); } @@ -106,8 +107,8 @@ public static void EnsureIsInNativeIntRange(int depth, int height, int width, in [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ComputeInt32Volume(int depth, int height, int width, int slicePitch, int rowPitch) { - return Max(checked((slicePitch * (depth - 1)) + - ((width + rowPitch) * (height - 1)) + - width), 0); + return Max(checked(((width + rowPitch) * height + slicePitch) * (depth - 1) + + (width + rowPitch) * (height - 1) + + width), 0); } -} +} \ No newline at end of file From 16f671d0d61d78fbd239f0b1d168eac85e6d2abc Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Sat, 14 Feb 2026 00:14:27 +0100 Subject: [PATCH 4/9] Add extensions for Memory3D and Span3D (and RO too) --- .../Extensions/MemoryExtensions.cs | 47 ++++++++++++++++++- .../Extensions/ReadOnlyMemoryExtensions.cs | 47 ++++++++++++++++++- .../Extensions/ReadOnlySpanExtensions.cs | 47 ++++++++++++++++++- .../Extensions/SpanExtensions.cs | 47 ++++++++++++++++++- 4 files changed, 184 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.HighPerformance/Extensions/MemoryExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/MemoryExtensions.cs index 26eff532..3cab4677 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/MemoryExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/MemoryExtensions.cs @@ -57,6 +57,51 @@ public static Memory2D AsMemory2D(this Memory memory, int offset, int h { return new(memory, offset, height, width, pitch); } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The depth of the resulting 3D volume. + /// The height of each slice in the resulting 3D volume. + /// The width of each row in the resulting 3D volume. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory3D AsMemory3D(this Memory memory, int depth, int height, int width) + { + return new(memory, depth, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The depth of the resulting 3D volume. + /// The height of each slice in the resulting 3D volume. + /// The width of each row in the resulting 3D volume. + /// The pitch of each slice in the resulting 3D volume (distance between slices). + /// The pitch of each row in the resulting 3D volume (distance between rows). + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory3D AsMemory3D(this Memory memory, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + return new(memory, offset, depth, height, width, slicePitch, rowPitch); + } #endif /// @@ -109,4 +154,4 @@ public static Stream AsStream(this Memory memory) { return MemoryStream.Create(memory, false); } -} +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs index 93df6f24..cba16aa1 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlyMemoryExtensions.cs @@ -60,6 +60,51 @@ public static ReadOnlyMemory2D AsMemory2D(this ReadOnlyMemory memory, i { return new(memory, offset, height, width, pitch); } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory3D AsMemory3D(this ReadOnlyMemory memory, int depth, int height, int width) + { + return new(memory, depth, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The depth of the resulting 3D area. + /// The height of each slice in the resulting 3D area. + /// The width of each row in the resulting 3D area. + /// The slice pitch in the resulting 3D area (distance between slices). + /// The row pitch in the resulting 3D area (distance between rows). + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlyMemory3D AsMemory3D(this ReadOnlyMemory memory, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + return new(memory, offset, depth, height, width, slicePitch, rowPitch); + } #endif /// @@ -148,4 +193,4 @@ public static Stream AsStream(this ReadOnlyMemory memory) { return MemoryStream.Create(memory, true); } -} +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs index e7e64c78..761e19fa 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/ReadOnlySpanExtensions.cs @@ -200,6 +200,51 @@ public static ReadOnlySpan2D AsSpan2D(this ReadOnlySpan span, int offse { return new(span, offset, height, width, pitch); } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The depth of the resulting 3D volume. + /// The height of each slice in the resulting 3D volume. + /// The width of each row in the resulting 3D volume. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan3D AsSpan3D(this ReadOnlySpan span, int depth, int height, int width) + { + return new(span, depth, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The depth of the resulting 3D volume. + /// The height of each slice in the resulting 3D volume. + /// The width of each row in the resulting 3D volume. + /// The slice pitch in the resulting 3D volume (distance between slices). + /// The row pitch in the resulting 3D volume (distance between rows). + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ReadOnlySpan3D AsSpan3D(this ReadOnlySpan span, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + return new(span, offset, depth, height, width, slicePitch, rowPitch); + } #endif /// @@ -383,4 +428,4 @@ public static bool TryCopyTo(this ReadOnlySpan span, RefEnumerable dest { return destination.TryCopyFrom(span); } -} +} \ No newline at end of file diff --git a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs index 21f0725e..51fddd36 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/SpanExtensions.cs @@ -104,6 +104,51 @@ public static Span2D AsSpan2D(this Span span, int offset, int height, i { return new(span, offset, height, width, pitch); } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The depth of the resulting 3D volume. + /// The height of each slice in the resulting 3D volume. + /// The width of each row in the resulting 3D volume. + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span3D AsSpan3D(this Span span, int depth, int height, int width) + { + return new(span, depth, height, width); + } + + /// + /// Returns a instance wrapping the underlying data for the given instance. + /// + /// The type of items in the input instance. + /// The input instance. + /// The initial offset within . + /// The depth of the resulting 3D volume. + /// The height of each slice in the resulting 3D volume. + /// The width of each row in the resulting 3D volume. + /// The slice pitch in the resulting 3D volume (distance between slices). + /// The row pitch in the resulting 3D volume (distance between rows). + /// The resulting instance. + /// + /// Thrown when one of the input parameters is out of range. + /// + /// + /// Thrown when the requested volume is outside of bounds for . + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span3D AsSpan3D(this Span span, int offset, int depth, int height, int width, int slicePitch, int rowPitch) + { + return new(span, offset, depth, height, width, slicePitch, rowPitch); + } #endif /// @@ -280,4 +325,4 @@ public static bool TryCopyTo(this Span span, RefEnumerable destination) { return destination.TryCopyFrom(span); } -} +} \ No newline at end of file From d9f7b42507d88f8c498a02009f05a814b531537a Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Sat, 14 Feb 2026 00:20:11 +0100 Subject: [PATCH 5/9] Add ArrayExtensions for the new 3D memory types --- .../Extensions/ArrayExtensions.3D.cs | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs b/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs index 2c02a323..16808755 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs @@ -187,6 +187,82 @@ public static Memory AsMemory(this T[,,] array, int depth) } #endif + /// + /// Creates a new over an input 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The input 3D array instance. + /// A instance with the values of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span3D AsSpan3D(this T[,,]? array) + { + return new(array); + } + + /// + /// Creates a new over an input 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The input 3D array instance. + /// The target slice to map within . + /// The target row to map within . + /// The target column to map within . + /// The depth to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , , , + /// , , or + /// are negative or not within the bounds that are valid for . + /// + /// A instance with the values of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span3D AsSpan3D(this T[,,]? array, int slice, int row, int column, int depth, int height, int width) + { + return new(array, slice, row, column, depth, height, width); + } + + /// + /// Creates a new over an input 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The input 3D array instance. + /// A instance with the values of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory3D AsMemory3D(this T[,,]? array) + { + return new(array); + } + + /// + /// Creates a new over an input 3D array. + /// + /// The type of elements in the input 3D array instance. + /// The input 3D array instance. + /// The target slice to map within . + /// The target row to map within . + /// The target column to map within . + /// The depth to map within . + /// The height to map within . + /// The width to map within . + /// + /// Thrown when doesn't match . + /// + /// + /// Thrown when either , , , + /// , , or + /// are negative or not within the bounds that are valid for . + /// + /// A instance with the values of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory3D AsMemory3D(this T[,,]? array, int slice, int row, int column, int depth, int height, int width) + { + return new(array, slice, row, column, depth, height, width); + } + /// /// Creates a new instance of the struct wrapping a layer in a 3D array. /// @@ -281,4 +357,12 @@ private static void ThrowArgumentOutOfRangeExceptionForDepth() { throw new ArgumentOutOfRangeException("depth"); } -} + + /// + /// Throws an when the "slice" parameter is invalid. + /// + private static void ThrowArgumentOutOfRangeExceptionForSlice() + { + throw new ArgumentOutOfRangeException("slice"); + } +} \ No newline at end of file From efde3e3623a20d3a2687a62ccf0fe922720fe5bd Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Sat, 14 Feb 2026 00:22:15 +0100 Subject: [PATCH 6/9] Add some 3D extensions for Memory and Span (and RO too) --- .../Extensions/Test_MemoryExtensions.cs | 37 ++++++++++++++++++- .../Test_ReadOnlyMemoryExtensions.cs | 37 ++++++++++++++++++- .../Extensions/Test_ReadOnlySpanExtensions.cs | 37 ++++++++++++++++++- .../Extensions/Test_SpanExtensions.cs | 37 ++++++++++++++++++- 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_MemoryExtensions.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_MemoryExtensions.cs index e9fcdcfb..e00e2ae9 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_MemoryExtensions.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_MemoryExtensions.cs @@ -591,6 +591,41 @@ public void Test_MemoryExtensions_AsMemory2D_Empty() Assert.AreEqual(7, empty3.Width); Assert.AreEqual(0, empty3.Height); } + + [TestMethod] + public void Test_MemoryExtensions_AsMemory3D_Empty() + { + Memory3D empty1 = Array.Empty().AsMemory().AsMemory3D(0, 0, 0); + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Width); + Assert.AreEqual(0, empty1.Height); + + Memory3D empty2 = Array.Empty().AsMemory().AsMemory3D(4, 0, 0); + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(4, empty2.Width); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Depth); + + Memory3D empty3 = Array.Empty().AsMemory().AsMemory3D(0, 7, 0); + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Width); + Assert.AreEqual(7, empty3.Height); + Assert.AreEqual(0, empty2.Depth); + + Memory3D empty4 = Array.Empty().AsMemory().AsMemory3D(0, 0, 3); + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(3, empty4.Width); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(0, empty2.Depth); + } #endif private sealed class ArrayMemoryManager : MemoryManager @@ -630,4 +665,4 @@ public static implicit operator Memory(ArrayMemoryManager memoryManager) return memoryManager.Memory; } } -} +} \ No newline at end of file diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlyMemoryExtensions.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlyMemoryExtensions.cs index 80f1fc99..0abdee7b 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlyMemoryExtensions.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlyMemoryExtensions.cs @@ -61,5 +61,40 @@ public void Test_ReadOnlyMemoryExtensions_AsMemory2D_Empty() Assert.AreEqual(7, empty3.Width); Assert.AreEqual(0, empty3.Height); } + + [TestMethod] + public void Test_ReadOnlyMemoryExtensions_AsMemory3D_Empty() + { + ReadOnlyMemory3D empty1 = ((ReadOnlyMemory)Array.Empty().AsMemory()).AsMemory3D(0, 0, 0); + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Width); + Assert.AreEqual(0, empty1.Height); + + ReadOnlyMemory3D empty2 = ((ReadOnlyMemory)Array.Empty().AsMemory()).AsMemory3D(4, 0, 0); + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(4, empty2.Width); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Depth); + + ReadOnlyMemory3D empty3 = ((ReadOnlyMemory)Array.Empty().AsMemory()).AsMemory3D(0, 7, 0); + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Width); + Assert.AreEqual(7, empty3.Height); + Assert.AreEqual(0, empty2.Depth); + + ReadOnlyMemory3D empty4 = ((ReadOnlyMemory)Array.Empty().AsMemory()).AsMemory3D(0, 0, 3); + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(3, empty4.Width); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(0, empty2.Depth); + } #endif -} +} \ No newline at end of file diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlySpanExtensions.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlySpanExtensions.cs index 374b5032..bb946ad1 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlySpanExtensions.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ReadOnlySpanExtensions.cs @@ -320,5 +320,40 @@ public void Test_ReadOnlySpanExtensions_AsSpan2D_Empty() Assert.AreEqual(7, empty3.Width); Assert.AreEqual(0, empty3.Height); } + + [TestMethod] + public void Test_ReadOnlySpanExtensions_AsSpan3D_Empty() + { + ReadOnlySpan3D empty1 = ReadOnlySpan.Empty.AsSpan3D(0, 0, 0); + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Width); + Assert.AreEqual(0, empty1.Height); + + ReadOnlySpan3D empty2 = ReadOnlySpan.Empty.AsSpan3D(4, 0, 0); + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(4, empty2.Width); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Depth); + + ReadOnlySpan3D empty3 = ReadOnlySpan.Empty.AsSpan3D(0, 7, 0); + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Width); + Assert.AreEqual(7, empty3.Height); + Assert.AreEqual(0, empty2.Depth); + + ReadOnlySpan3D empty4 = ReadOnlySpan.Empty.AsSpan3D(0, 0, 3); + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(3, empty4.Width); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(0, empty2.Depth); + } #endif -} +} \ No newline at end of file diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs index 05d2a2e6..51f5311a 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_SpanExtensions.cs @@ -225,5 +225,40 @@ public void Test_SpanExtensions_AsSpan2D_Empty() Assert.AreEqual(7, empty3.Width); Assert.AreEqual(0, empty3.Height); } + + [TestMethod] + public void Test_SpanExtensions_AsSpan3D_Empty() + { + Span3D empty1 = Span.Empty.AsSpan3D(0, 0, 0); + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Width); + Assert.AreEqual(0, empty1.Height); + + Span3D empty2 = Span.Empty.AsSpan3D(4, 0, 0); + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(4, empty2.Width); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Depth); + + Span3D empty3 = Span.Empty.AsSpan3D(0, 7, 0); + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Width); + Assert.AreEqual(7, empty3.Height); + Assert.AreEqual(0, empty2.Depth); + + Span3D empty4 = Span.Empty.AsSpan3D(0, 0, 3); + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(3, empty4.Width); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(0, empty2.Depth); + } #endif -} +} \ No newline at end of file From e44c916bbce2b037d9bcb542ad1dc9a59046cee7 Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Sat, 14 Feb 2026 00:24:30 +0100 Subject: [PATCH 7/9] Add tests for Memory3D and Span3D (and RO too) * They mimic the tests that already exist for Memory2D and Span2D --- .../Memory/Test_Memory3D{T}.cs | 642 ++++++++ .../Memory/Test_ReadOnlyMemory3D{T}.cs | 610 ++++++++ .../Memory/Test_ReadOnlySpan3D{T}.cs | 1318 ++++++++++++++++ .../Memory/Test_Span3D{T}.cs | 1324 +++++++++++++++++ 4 files changed, 3894 insertions(+) create mode 100644 tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Memory3D{T}.cs create mode 100644 tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlyMemory3D{T}.cs create mode 100644 tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlySpan3D{T}.cs create mode 100644 tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Span3D{T}.cs diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Memory3D{T}.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Memory3D{T}.cs new file mode 100644 index 00000000..b3759c71 --- /dev/null +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Memory3D{T}.cs @@ -0,0 +1,642 @@ +// 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.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.UnitTests.Buffers.Internals; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.HighPerformance.UnitTests; + +[TestClass] +public class Test_Memory3DT +{ + [TestMethod] + public void Test_Memory3DT_Empty() + { + // Create a few empty Memory3D instances in different ways and + // check to ensure the right parameters were used to initialize them. + Memory3D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Depth); + Assert.AreEqual(0, empty1.Height); + Assert.AreEqual(0, empty1.Width); + + Memory3D empty2 = Memory3D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(0, empty2.Depth); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Width); + + Memory3D empty3 = new int[0, 2, 3]; + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Depth); + Assert.AreEqual(2, empty3.Height); + Assert.AreEqual(3, empty3.Width); + + Memory3D empty4 = new int[2, 0, 3]; + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(2, empty4.Depth); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(3, empty4.Width); + + Memory3D empty5 = new int[2, 3, 0]; + + Assert.IsTrue(empty5.IsEmpty); + Assert.AreEqual(0, empty5.Length); + Assert.AreEqual(2, empty5.Depth); + Assert.AreEqual(3, empty5.Height); + Assert.AreEqual(0, empty5.Width); + +#if NET6_0_OR_GREATER + MemoryManager memoryManager = new UnmanagedSpanOwner(1); + Memory3D empty6 = new(memoryManager, 0, 0, 0); + + Assert.IsTrue(empty6.IsEmpty); + Assert.AreEqual(0, empty6.Length); + Assert.AreEqual(0, empty6.Depth); + Assert.AreEqual(0, empty6.Height); + Assert.AreEqual(0, empty6.Width); + + Memory3D empty7 = new(memoryManager, 2, 0, 0); + + Assert.IsTrue(empty7.IsEmpty); + Assert.AreEqual(0, empty7.Length); + Assert.AreEqual(2, empty7.Depth); + Assert.AreEqual(0, empty7.Height); + Assert.AreEqual(0, empty7.Width); + + Memory3D empty8 = new(memoryManager, 0, 2, 0); + + Assert.IsTrue(empty8.IsEmpty); + Assert.AreEqual(0, empty8.Length); + Assert.AreEqual(0, empty8.Depth); + Assert.AreEqual(2, empty8.Height); + Assert.AreEqual(0, empty8.Width); + + Memory3D empty9 = new(memoryManager, 0, 0, 3); + + Assert.IsTrue(empty9.IsEmpty); + Assert.AreEqual(0, empty9.Length); + Assert.AreEqual(0, empty9.Depth); + Assert.AreEqual(0, empty9.Height); + Assert.AreEqual(3, empty9.Width); +#endif + } + + [TestMethod] + public void Test_Memory3DT_Array1DConstructor() + { + int[] array = + { + 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20 + }; + + // Create a memory over a 1D array with 23 data in row-major order. This tests + // the T[] array constructor for Memory3D with custom sizes and pitches. + Memory3D memory3d = new(array, 1, 2, 2, 2, 2, 1); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Width); + Assert.AreEqual(2, memory3d.Span[0, 0, 0]); + Assert.AreEqual(14, memory3d.Span[1, 1, 1]); + + // Also ensure the right exceptions are thrown with invalid parameters, such as + // negative indices, indices out of range, values that are too big, etc. + _ = Assert.ThrowsExactly(() => new Memory3D(new string[1], 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, -1, 1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, -1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 1, -1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 1, 1, -1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 1, 1, 1, -1, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 1, 1, 1, 0, -1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 2, 2, 2, 30, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 2, 2, 2, 0, 30)); + } + + [TestMethod] + public void Test_Memory3DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Test the constructor taking a T[,,] array that is mapped directly (no slicing) + Memory3D memory3d = new(array); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(12, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(3, memory3d.Width); + Assert.AreEqual(2, memory3d.Span[0, 0, 1]); + Assert.AreEqual(60, memory3d.Span[1, 1, 2]); + + // Here we test the check for covariance: we can't create a Memory3D from a U[,,] array + // where U is assignable to T (as in, U : T). This would cause a type safety violation on write. + _ = Assert.ThrowsExactly(() => new Memory3D(new string[1, 1, 1])); + } + + [TestMethod] + public void Test_Memory3DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, but this time we also slice the memory to test the other constructor + Memory3D memory3d = new(array, 0, 0, 1, 2, 2, 2); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Width); + Assert.AreEqual(2, memory3d.Span[0, 0, 0]); + Assert.AreEqual(60, memory3d.Span[1, 1, 1]); + + _ = Assert.ThrowsExactly(() => new Memory3D(new string[1, 1, 1], 0, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, -1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 0, 0, 3, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 0, 0, 1, 3, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array, 0, 0, 0, 1, 1, 5)); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_Memory3DT_MemoryConstructor() + { + Memory memory = new[] + { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 + }; + + // We also test the constructor that takes an input Memory instance. + // This is only available on runtimes with fast Span support, as otherwise + // the implementation would be too complex and slow to work in this case. + // Conceptually, this works the same as when wrapping a 1D array with row-major items. + Memory3D memory3d = memory.AsMemory3D(1, 2, 2, 2, 1, 2); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Width); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Depth); + + Assert.AreEqual(2, memory3d.Span[0, 0, 0]); + Assert.AreEqual(3, memory3d.Span[0, 0, 1]); + Assert.AreEqual(6, memory3d.Span[0, 1, 0]); + Assert.AreEqual(7, memory3d.Span[0, 1, 1]); + Assert.AreEqual(11, memory3d.Span[1, 0, 0]); + Assert.AreEqual(16, memory3d.Span[1, 1, 1]); + + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(-99, 1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, -10, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, -10, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, 1, -100, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, 1, 1, -1, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, 1, 1, 0, -1)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 2, 4, 4, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 3, 3, 3, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(1, 2, 3, 3, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 2, 2, 2, 11, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 2, 2, 2, 0, 20)); + } + + [TestMethod] + public void Test_Memory3DT_MemoryManagerConstructor() + { + MemoryManager memoryManager = new UnmanagedSpanOwner(8); + + Memory3D memory3d = new(memoryManager, 2, 2, 2); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Width); + + _ = Assert.ThrowsExactly(() => new Memory3D(memoryManager, -1, 2, 2, 2, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(memoryManager, 0, -2, 2, 2, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(memoryManager, 0, 2, -2, 2, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(memoryManager, 0, 2, 2, -2, 0, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(memoryManager, 0, 2, 2, 2, -1, 0)); + _ = Assert.ThrowsExactly(() => new Memory3D(memoryManager, 0, 2, 2, 2, 0, -1)); + _ = Assert.ThrowsExactly(() => new Memory3D(memoryManager, 0, 2, 2, 2, 30, 0)); + } +#endif + + [TestMethod] + public void Test_Memory3DT_Slice_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Memory3D memory3d = new(array); + + // Test a slice from a Memory3D with valid parameters + Memory3D slice1 = memory3d.Slice(1, 0, 1, 1, 2, 2); + + Assert.AreEqual(4, slice1.Length); + Assert.AreEqual(1, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(8, slice1.Span[0, 0, 0]); + Assert.AreEqual(12, slice1.Span[0, 1, 1]); + + // Same above, but we test slicing a pre-sliced instance as well. This + // is done to verify that the internal offsets are properly tracked + // across multiple slicing operations, instead of just in the first. + Memory3D slice2 = memory3d.Slice(0, 1, 0, 2, 1, 3); + + Assert.AreEqual(6, slice2.Length); + Assert.AreEqual(2, slice2.Depth); + Assert.AreEqual(1, slice2.Height); + Assert.AreEqual(3, slice2.Width); + Assert.AreEqual(4, slice2.Span[0, 0, 0]); + Assert.AreEqual(10, slice2.Span[1, 0, 0]); + Assert.AreEqual(12, slice2.Span[1, 0, 2]); + + // A few invalid slicing operations, with out of range parameters + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(-1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(10, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(0, 10, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(0, 0, 10, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(0, 0, 0, 3, 1, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(0, 0, 0, 1, 3, 1)); + _ = Assert.ThrowsExactly(() => new Memory3D(array).Slice(0, 0, 0, 1, 1, 5)); + } + + [TestMethod] + public void Test_Memory3DT_Slice_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Memory3D memory3d = new(array); + + // Mostly the same test as above, just with different parameters + Memory3D slice1 = memory3d.Slice(0, 0, 0, 2, 2, 2); + + Assert.AreEqual(8, slice1.Length); + Assert.AreEqual(2, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(1, slice1.Span[0, 0, 0]); + Assert.AreEqual(11, slice1.Span[1, 1, 1]); + + Memory3D slice2 = slice1.Slice(1, 0, 1, 1, 2, 1); + + Assert.AreEqual(2, slice2.Length); + Assert.AreEqual(1, slice2.Depth); + Assert.AreEqual(2, slice2.Height); + Assert.AreEqual(1, slice2.Width); + Assert.AreEqual(8, slice2.Span[0, 0, 0]); + Assert.AreEqual(11, slice2.Span[0, 1, 0]); + } + + [TestMethod] + public void Test_Memory3DT_TryGetMemory_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Memory3D memory3d = new(array); + + // Here we test that we can get a Memory from a 3D one when the underlying + // data is contiguous. Note that in this case this can only work on runtimes + // with fast Span support, because otherwise it's not possible to get a + // Memory (or a Span too, for that matter) from a 3D array. + bool success = memory3d.TryGetMemory(out Memory memory); + +#if NETFRAMEWORK + Assert.IsFalse(success); + Assert.IsTrue(memory.IsEmpty); +#else + Assert.IsTrue(success); + Assert.HasCount(memory.Length, array); + Assert.IsTrue(Unsafe.AreSame(ref array[0, 0, 0], ref memory.Span[0])); +#endif + } + + [TestMethod] + public void Test_Memory3DT_TryGetMemory_2() + { + int[] array = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + Memory3D memory3d = new(array, 2, 2, 2); + + // Same test as above, but this will always succeed on all runtimes, + // as creating a Memory from a 1D array is always supported. + bool success = memory3d.TryGetMemory(out Memory memory); + + Assert.IsTrue(success); + Assert.HasCount(memory.Length, array); + Assert.AreEqual(8, memory.Span[7]); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_Memory3DT_TryGetMemory_3() + { + Memory data = new[] { 1, 2, 3, 4 }; + + Memory3D memory3d = data.AsMemory3D(1, 2, 2); + + // Same as above, just with the extra Memory indirection. Same as above, + // this test is only supported on runtimes with fast Span support. + // On others, we just don't expose the Memory.AsMemory3D extension. + bool success = memory3d.TryGetMemory(out Memory memory); + + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, data.Length); + Assert.AreEqual(3, memory.Span[2]); + } +#endif + + [TestMethod] + public unsafe void Test_Memory3DT_Pin_1() + { + int[] array = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + // We create a Memory3D from an array and verify that pinning this + // instance correctly returns a pointer to the right array element. + Memory3D memory3d = new(array, 2, 2, 2); + + using MemoryHandle pin = memory3d.Pin(); + + Assert.AreEqual(1, ((int*)pin.Pointer)[0]); + Assert.AreEqual(8, ((int*)pin.Pointer)[7]); + } + + [TestMethod] + public unsafe void Test_Memory3DT_Pin_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but we test with a sliced Memory3D instance + Memory3D memory3d = new(array, 1, 0, 0, 1, 2, 2); + + using MemoryHandle pin = memory3d.Pin(); + + Assert.AreEqual(7, ((int*)pin.Pointer)[0]); + Assert.AreEqual(10, ((int*)pin.Pointer)[3]); + } + + [TestMethod] + public void Test_Memory3DT_ToArray_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Here we create a Memory3D instance from a 3D array and then verify that + // calling ToArray() creates an array that matches the contents of the first. + Memory3D memory3d = new(array); + + int[,,] copy = memory3d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + Assert.AreEqual(copy.GetLength(2), array.GetLength(2)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestMethod] + public void Test_Memory3DT_ToArray_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but with a sliced Memory3D instance + Memory3D memory3d = new(array, 0, 0, 0, 1, 2, 2); + + int[,,] copy = memory3d.ToArray(); + + Assert.AreEqual(1, copy.GetLength(0)); + Assert.AreEqual(2, copy.GetLength(1)); + Assert.AreEqual(2, copy.GetLength(2)); + + int[,,] expected = + { + { + { 1, 2 }, + { 4, 5 } + } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestMethod] + public void Test_Memory3DT_Equals() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Here we want to verify that the Memory3D.Equals method works correctly. This is true + // when the wrapped instance is the same, and the various internal offsets and sizes match. + Memory3D memory3d = new(array); + + Assert.IsFalse(memory3d.Equals(null)); + Assert.IsFalse(memory3d.Equals(new Memory3D(array, 0, 0, 1, 2, 2, 2))); + Assert.IsTrue(memory3d.Equals(new Memory3D(array))); + Assert.IsTrue(memory3d.Equals(memory3d)); + + // This should work also when casting to a ReadOnlyMemory3D instance + ReadOnlyMemory3D readOnlyMemory3d = memory3d; + + Assert.IsTrue(memory3d.Equals(readOnlyMemory3d)); + Assert.IsFalse(memory3d.Equals(readOnlyMemory3d.Slice(0, 0, 1, 2, 2, 2))); + } + + [TestMethod] + public void Test_Memory3DT_GetHashCode() + { + // An empty Memory3D has just 0 as the hashcode + Assert.AreEqual(0, Memory3D.Empty.GetHashCode()); + + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Memory3D memory3d = new(array); + + // Ensure that the GetHashCode method is repeatable + int a = memory3d.GetHashCode(); + int b = memory3d.GetHashCode(); + + Assert.AreEqual(a, b); + + // The hashcode shouldn't match when the size is different + int c = new Memory3D(array, 0, 0, 0, 1, 2, 2).GetHashCode(); + + Assert.AreNotEqual(a, c); + } + + [TestMethod] + public void Test_Memory3DT_ToString() + { + int[,,] array = new int[2, 2, 3]; + + Memory3D memory3d = new(array); + + // Here we just want to verify that the type is nicely printed as expected, along with the size + string text = memory3d.ToString(); + + const string expected = "CommunityToolkit.HighPerformance.Memory3D[2, 2, 3]"; + + Assert.AreEqual(expected, text); + } + + [TestMethod] + public void Test_Memory3DT_ImplicitCast() + { + int[,,] array = new int[2, 2, 3]; + + Memory3D memory3d_1 = array; + Memory3D memory3d_2 = new(array); + + Assert.IsTrue(memory3d_1.Equals(memory3d_2)); + } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3536 + [TestMethod] + [DataRow(64, 180, 320)] // depth, height, width + public void Test_Memory3DT_CastAndSlice_WorksCorrectly(int depth, int height, int width) + { + Memory3D data = + new byte[depth * height * width * sizeof(int)] + .AsMemory() + .Cast() + .AsMemory3D(depth: depth, height: height, width: width); + + Memory3D slice = data.Slice( + slice: depth / 2, + row: height / 2, + column: 0, + depth: depth / 2, + height: height / 2, + width: width); + + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth / 2, height / 2, 0], ref slice.Span[0, 0, 0])); + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth / 2, height / 2, width - 1], ref slice.Span[0, 0, width - 1])); + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth / 2, height - 1, 0], ref slice.Span[0, (height / 2) - 1, 0])); + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth / 2, height - 1, width - 1], ref slice.Span[0, (height / 2) - 1, width - 1])); + + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth - 1, height / 2, 0], ref slice.Span[(depth / 2) - 1, 0, 0])); + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth - 1, height / 2, width - 1], ref slice.Span[(depth / 2) - 1, 0, width - 1])); + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth - 1, height - 1, 0], ref slice.Span[(depth / 2) - 1, (height / 2) - 1, 0])); + Assert.IsTrue(Unsafe.AreSame(ref data.Span[depth - 1, height - 1, width - 1], ref slice.Span[(depth / 2) - 1, (height / 2) - 1, width - 1])); + } +#endif +} \ No newline at end of file diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlyMemory3D{T}.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlyMemory3D{T}.cs new file mode 100644 index 00000000..2cbddfca --- /dev/null +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlyMemory3D{T}.cs @@ -0,0 +1,610 @@ +// 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.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.UnitTests.Buffers.Internals; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.HighPerformance.UnitTests; + +/* ==================================================================== + * NOTE + * ==================================================================== + * All the tests here mirror the ones for ReadOnlyMemory3D, as the two types + * are basically the same except for some small differences in return types + * or some checks being done upon construction. See comments in the test + * file for ReadOnlyMemory3D for more info on these tests. */ +[TestClass] +public class Test_ReadOnlyMemory3DT +{ + [TestMethod] + public void Test_ReadOnlyMemory3DT_Empty() + { + ReadOnlyMemory3D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Depth); + Assert.AreEqual(0, empty1.Height); + Assert.AreEqual(0, empty1.Width); + + ReadOnlyMemory3D empty2 = ReadOnlyMemory3D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(0, empty2.Depth); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Width); + + ReadOnlyMemory3D empty3 = new int[0, 2, 3]; + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Depth); + Assert.AreEqual(2, empty3.Height); + Assert.AreEqual(3, empty3.Width); + + ReadOnlyMemory3D empty4 = new int[2, 0, 3]; + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(2, empty4.Depth); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(3, empty4.Width); + + ReadOnlyMemory3D empty5 = new int[2, 3, 0]; + + Assert.IsTrue(empty5.IsEmpty); + Assert.AreEqual(0, empty5.Length); + Assert.AreEqual(2, empty5.Depth); + Assert.AreEqual(3, empty5.Height); + Assert.AreEqual(0, empty5.Width); + +#if NET6_0_OR_GREATER + MemoryManager memoryManager = new UnmanagedSpanOwner(1); + ReadOnlyMemory3D empty6 = new(memoryManager, 0, 0, 0); + + Assert.IsTrue(empty6.IsEmpty); + Assert.AreEqual(0, empty6.Length); + Assert.AreEqual(0, empty6.Depth); + Assert.AreEqual(0, empty6.Height); + Assert.AreEqual(0, empty6.Width); + + ReadOnlyMemory3D empty7 = new(memoryManager, 2, 0, 0); + + Assert.IsTrue(empty7.IsEmpty); + Assert.AreEqual(0, empty7.Length); + Assert.AreEqual(2, empty7.Depth); + Assert.AreEqual(0, empty7.Height); + Assert.AreEqual(0, empty7.Width); + + ReadOnlyMemory3D empty8 = new(memoryManager, 0, 2, 0); + + Assert.IsTrue(empty8.IsEmpty); + Assert.AreEqual(0, empty8.Length); + Assert.AreEqual(0, empty8.Depth); + Assert.AreEqual(2, empty8.Height); + Assert.AreEqual(0, empty8.Width); + + ReadOnlyMemory3D empty9 = new(memoryManager, 0, 0, 3); + + Assert.IsTrue(empty9.IsEmpty); + Assert.AreEqual(0, empty9.Length); + Assert.AreEqual(0, empty9.Depth); + Assert.AreEqual(0, empty9.Height); + Assert.AreEqual(3, empty9.Width); +#endif + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_Array1DConstructor() + { + int[] array = + { + 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20 + }; + + ReadOnlyMemory3D memory3d = new(array, 1, 2, 2, 2, 2, 1); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Width); + Assert.AreEqual(2, memory3d.Span[0, 0, 0]); + Assert.AreEqual(14, memory3d.Span[1, 1, 1]); + + // Here we check to ensure a covariant array conversion is allowed for ReadOnlyMemory3D + _ = new ReadOnlyMemory3D(new string[1], 1, 1, 1); + + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, -1, 1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, -1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 1, -1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 1, 1, -1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 1, 1, 1, -1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 1, 1, 1, 0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 2, 2, 2, 30, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 2, 2, 2, 0, 30)); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlyMemory3D memory3d = new(array); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(12, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(3, memory3d.Width); + Assert.AreEqual(2, memory3d.Span[0, 0, 1]); + Assert.AreEqual(60, memory3d.Span[1, 1, 2]); + + _ = new ReadOnlyMemory3D(new string[1, 1, 2]); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlyMemory3D memory3d = new(array, 0, 0, 1, 2, 2, 2); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Width); + Assert.AreEqual(2, memory3d.Span[0, 0, 0]); + Assert.AreEqual(60, memory3d.Span[1, 1, 1]); + + _ = new ReadOnlyMemory3D(new string[1, 1, 2]); + + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, -1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 0, 0, 3, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 0, 0, 1, 3, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array, 0, 0, 0, 1, 1, 5)); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_ReadOnlyMemory3DT_ReadOnlyMemoryConstructor() + { + ReadOnlyMemory memory = new[] + { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 + }; + + ReadOnlyMemory3D memory3d = memory.AsMemory3D(1, 2, 2, 2, 1, 2); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Width); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Depth); + + Assert.AreEqual(2, memory3d.Span[0, 0, 0]); + Assert.AreEqual(3, memory3d.Span[0, 0, 1]); + Assert.AreEqual(6, memory3d.Span[0, 1, 0]); + Assert.AreEqual(7, memory3d.Span[0, 1, 1]); + Assert.AreEqual(11, memory3d.Span[1, 0, 0]); + Assert.AreEqual(16, memory3d.Span[1, 1, 1]); + + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(-99, 1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, -10, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, -10, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, 1, -100, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, 1, 1, -1, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 1, 1, 1, 0, -1)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 2, 4, 4, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 3, 3, 3, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(1, 2, 3, 3, 0, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 2, 2, 2, 11, 0)); + _ = Assert.ThrowsExactly(() => memory.AsMemory3D(0, 2, 2, 2, 0, 20)); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_MemoryManagerConstructor() + { + MemoryManager memoryManager = new UnmanagedSpanOwner(8); + + ReadOnlyMemory3D memory3d = new(memoryManager, 2, 2, 2); + + Assert.IsFalse(memory3d.IsEmpty); + Assert.AreEqual(8, memory3d.Length); + Assert.AreEqual(2, memory3d.Depth); + Assert.AreEqual(2, memory3d.Height); + Assert.AreEqual(2, memory3d.Width); + + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(memoryManager, -1, 2, 2, 2, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(memoryManager, 0, -2, 2, 2, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(memoryManager, 0, 2, -2, 2, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(memoryManager, 0, 2, 2, -2, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(memoryManager, 0, 2, 2, 2, -1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(memoryManager, 0, 2, 2, 2, 0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(memoryManager, 0, 2, 2, 2, 30, 0)); + } +#endif + + [TestMethod] + public void Test_ReadOnlyMemory3DT_Slice_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D memory3d = new(array); + + ReadOnlyMemory3D slice1 = memory3d.Slice(1, 0, 1, 1, 2, 2); + + Assert.AreEqual(4, slice1.Length); + Assert.AreEqual(1, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(8, slice1.Span[0, 0, 0]); + Assert.AreEqual(12, slice1.Span[0, 1, 1]); + + ReadOnlyMemory3D slice2 = memory3d.Slice(0, 1, 0, 2, 1, 3); + + Assert.AreEqual(6, slice2.Length); + Assert.AreEqual(2, slice2.Depth); + Assert.AreEqual(1, slice2.Height); + Assert.AreEqual(3, slice2.Width); + Assert.AreEqual(4, slice2.Span[0, 0, 0]); + Assert.AreEqual(10, slice2.Span[1, 0, 0]); + Assert.AreEqual(12, slice2.Span[1, 0, 2]); + + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(-1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(10, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(0, 10, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(0, 0, 10, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(0, 0, 0, 3, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(0, 0, 0, 1, 3, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlyMemory3D(array).Slice(0, 0, 0, 1, 1, 5)); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_Slice_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D memory3d = new(array); + + ReadOnlyMemory3D slice1 = memory3d.Slice(0, 0, 0, 2, 2, 2); + + Assert.AreEqual(8, slice1.Length); + Assert.AreEqual(2, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(1, slice1.Span[0, 0, 0]); + Assert.AreEqual(11, slice1.Span[1, 1, 1]); + + ReadOnlyMemory3D slice2 = slice1.Slice(1, 0, 1, 1, 2, 1); + + Assert.AreEqual(2, slice2.Length); + Assert.AreEqual(1, slice2.Depth); + Assert.AreEqual(2, slice2.Height); + Assert.AreEqual(1, slice2.Width); + Assert.AreEqual(8, slice2.Span[0, 0, 0]); + Assert.AreEqual(11, slice2.Span[0, 1, 0]); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_TryGetReadOnlyMemory_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D memory3d = new(array); + + bool success = memory3d.TryGetMemory(out ReadOnlyMemory memory); + +#if NETFRAMEWORK + Assert.IsFalse(success); + Assert.IsTrue(memory.IsEmpty); +#else + Assert.IsTrue(success); + Assert.HasCount(memory.Length, array); + Assert.IsTrue(Unsafe.AreSame(ref array[0, 0, 0], ref Unsafe.AsRef(in memory.Span[0]))); +#endif + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_TryGetReadOnlyMemory_2() + { + int[] array = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + ReadOnlyMemory3D memory3d = new(array, 2, 2, 2); + + bool success = memory3d.TryGetMemory(out ReadOnlyMemory memory); + + Assert.IsTrue(success); + Assert.HasCount(memory.Length, array); + Assert.AreEqual(8, memory.Span[7]); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_ReadOnlyMemory3DT_TryGetReadOnlyMemory_3() + { + Memory data = new[] { 1, 2, 3, 4 }; + + ReadOnlyMemory3D memory3d = data.AsMemory3D(1, 2, 2); + + bool success = memory3d.TryGetMemory(out ReadOnlyMemory memory); + + Assert.IsTrue(success); + Assert.AreEqual(memory.Length, data.Length); + Assert.AreEqual(3, memory.Span[2]); + } +#endif + + [TestMethod] + public unsafe void Test_ReadOnlyMemory3DT_Pin_1() + { + int[] array = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + ReadOnlyMemory3D memory3d = new(array, 2, 2, 2); + + using MemoryHandle pin = memory3d.Pin(); + + Assert.AreEqual(1, ((int*)pin.Pointer)[0]); + Assert.AreEqual(8, ((int*)pin.Pointer)[7]); + } + + [TestMethod] + public unsafe void Test_ReadOnlyMemory3DT_Pin_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D memory3d = new(array, 1, 0, 0, 1, 2, 2); + + using MemoryHandle pin = memory3d.Pin(); + + Assert.AreEqual(7, ((int*)pin.Pointer)[0]); + Assert.AreEqual(10, ((int*)pin.Pointer)[3]); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_ToArray_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D memory3d = new(array); + + int[,,] copy = memory3d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + Assert.AreEqual(copy.GetLength(2), array.GetLength(2)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_ToArray_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D memory3d = new(array, 0, 0, 0, 1, 2, 2); + + int[,,] copy = memory3d.ToArray(); + + Assert.AreEqual(1, copy.GetLength(0)); + Assert.AreEqual(2, copy.GetLength(1)); + Assert.AreEqual(2, copy.GetLength(2)); + + int[,,] expected = + { + { + { 1, 2 }, + { 4, 5 } + } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_Equals() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D readOnlyMemory3d = new(array); + + Assert.IsFalse(readOnlyMemory3d.Equals(null)); + Assert.IsFalse(readOnlyMemory3d.Equals(new ReadOnlyMemory3D(array, 0, 0, 1, 2, 2, 2))); + Assert.IsTrue(readOnlyMemory3d.Equals(new ReadOnlyMemory3D(array))); + Assert.IsTrue(readOnlyMemory3d.Equals(readOnlyMemory3d)); + + Memory3D memory3d = array; + + Assert.IsTrue(readOnlyMemory3d.Equals((object)memory3d)); + Assert.IsFalse(readOnlyMemory3d.Equals((object)memory3d.Slice(0, 0, 1, 2, 2, 2))); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_GetHashCode() + { + Assert.AreEqual(0, ReadOnlyMemory3D.Empty.GetHashCode()); + + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyMemory3D memory3d = new(array); + + int a = memory3d.GetHashCode(); + int b = memory3d.GetHashCode(); + + Assert.AreEqual(a, b); + + int c = new ReadOnlyMemory3D(array, 0, 0, 0, 1, 2, 2).GetHashCode(); + + Assert.AreNotEqual(a, c); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_ToString() + { + int[,,] array = new int[2, 2, 3]; + + ReadOnlyMemory3D memory3d = new(array); + + string text = memory3d.ToString(); + + const string expected = "CommunityToolkit.HighPerformance.ReadOnlyMemory3D[2, 2, 3]"; + + Assert.AreEqual(expected, text); + } + + [TestMethod] + public void Test_ReadOnlyMemory3DT_ImplicitCast() + { + int[,,] array = new int[2, 2, 3]; + + ReadOnlyMemory3D memory3d_1 = array; + ReadOnlyMemory3D memory3d_2 = new(array); + + Assert.IsTrue(memory3d_1.Equals(memory3d_2)); + } + +#if NET6_0_OR_GREATER + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3536 + [TestMethod] + [DataRow(64, 180, 320)] + public void Test_ReadOnlyMemory3DT_CastAndSlice_WorksCorrectly(int depth, int height, int width) + { + ReadOnlyMemory3D data = + new byte[depth * height * width * sizeof(int)] + .AsMemory() + .Cast() + .AsMemory3D(depth: depth, height: height, width: width); + + ReadOnlyMemory3D slice = data.Slice( + slice: depth / 2, + row: height / 2, + column: 0, + depth: depth / 2, + height: height / 2, + width: width); + + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth / 2, height / 2, 0]), ref Unsafe.AsRef(in slice.Span[0, 0, 0]))); + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth / 2, height / 2, width - 1]), ref Unsafe.AsRef(in slice.Span[0, 0, width - 1]))); + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth / 2, height - 1, 0]), ref Unsafe.AsRef(in slice.Span[0, (height / 2) - 1, 0]))); + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth / 2, height - 1, width - 1]), ref Unsafe.AsRef(in slice.Span[0, (height / 2) - 1, width - 1]))); + + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth - 1, height / 2, 0]), ref Unsafe.AsRef(in slice.Span[(depth / 2) - 1, 0, 0]))); + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth - 1, height / 2, width - 1]), ref Unsafe.AsRef(in slice.Span[(depth / 2) - 1, 0, width - 1]))); + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth - 1, height - 1, 0]), ref Unsafe.AsRef(in slice.Span[(depth / 2) - 1, (height / 2) - 1, 0]))); + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in data.Span[depth - 1, height - 1, width - 1]), ref Unsafe.AsRef(in slice.Span[(depth / 2) - 1, (height / 2) - 1, width - 1]))); + } +#endif +} \ No newline at end of file diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlySpan3D{T}.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlySpan3D{T}.cs new file mode 100644 index 00000000..720571f8 --- /dev/null +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_ReadOnlySpan3D{T}.cs @@ -0,0 +1,1318 @@ +// 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.Linq; +using System.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.Enumerables; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.HighPerformance.UnitTests; + +/* ==================================================================== + * NOTE + * ==================================================================== + * All the tests here mirror the ones for Span3D. See comments + * in the test file for Span3D for more info on these tests. */ +[TestClass] +public class Test_ReadOnlySpan3DT +{ + [TestMethod] + public void Test_ReadOnlySpan3DT_Empty() + { + ReadOnlySpan3D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Depth); + Assert.AreEqual(0, empty1.Height); + Assert.AreEqual(0, empty1.Width); + + ReadOnlySpan3D empty2 = ReadOnlySpan3D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(0, empty2.Depth); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Width); + + ReadOnlySpan3D empty3 = new int[0, 2, 3]; + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Depth); + Assert.AreEqual(2, empty3.Height); + Assert.AreEqual(3, empty3.Width); + + ReadOnlySpan3D empty4 = new int[2, 0, 3]; + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(2, empty4.Depth); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(3, empty4.Width); + + ReadOnlySpan3D empty5 = new int[2, 3, 0]; + + Assert.IsTrue(empty5.IsEmpty); + Assert.AreEqual(0, empty5.Length); + Assert.AreEqual(2, empty5.Depth); + Assert.AreEqual(3, empty5.Height); + Assert.AreEqual(0, empty5.Width); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_RefConstructor() + { + ReadOnlySpan span = stackalloc[] + { + 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12 + }; + + ReadOnlySpan3D span3d = ReadOnlySpan3D.DangerousCreate(span[0], 2, 2, 3, 0, 0); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(12, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(3, span3d.Width); + + _ = Assert.ThrowsExactly(() => ReadOnlySpan3D.DangerousCreate(Unsafe.AsRef(null), -1, 0, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => ReadOnlySpan3D.DangerousCreate(Unsafe.AsRef(null), 1, -2, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => ReadOnlySpan3D.DangerousCreate(Unsafe.AsRef(null), 1, 0, -5, 0, 0)); + _ = Assert.ThrowsExactly(() => ReadOnlySpan3D.DangerousCreate(Unsafe.AsRef(null), 1, 0, 0, -1, 0)); + _ = Assert.ThrowsExactly(() => ReadOnlySpan3D.DangerousCreate(Unsafe.AsRef(null), 1, 0, 0, 0, -1)); + } +#endif + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_PtrConstructor() + { + int* ptr = stackalloc[] + { + 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12 + }; + + ReadOnlySpan3D span3d = new(ptr, 2, 2, 3, 0, 0); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(12, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(3, span3d.Width); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D((void*)0, -1, 0, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D((void*)0, 1, -2, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D((void*)0, 1, 0, -5, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D((void*)0, 1, 0, 0, -1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D((void*)0, 1, 0, 0, 0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D((void*)0, 1, 1, 1, 0, 0)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_Array1DConstructor() + { + int[] array = Enumerable.Range(1, 20).ToArray(); + + ReadOnlySpan3D span3d = new(array, 1, 2, 2, 2, 2, 1); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(8, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(2, span3d.Width); + + // Same for ReadOnlyMemory3D, we need to check that covariant array conversions are allowed + _ = new ReadOnlySpan3D(new string[1], 1, 1, 1); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, -1, 1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, -1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 1, -1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 1, 1, -1, 0, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 1, 1, 1, -1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 1, 1, 1, 0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 4, 4, 4, 0, 0)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(12, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(3, span3d.Width); + + _ = new ReadOnlySpan2D(new string[1, 2]); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + ReadOnlySpan3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(8, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(2, span3d.Width); + + _ = new ReadOnlySpan3D(new string[1, 2, 1]); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, -1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 0, 0, 3, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 0, 0, 1, 3, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 0, 0, 1, 1, 5)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_CopyTo_Empty() + { + ReadOnlySpan3D span3d = ReadOnlySpan3D.Empty; + + int[] target = new int[0]; + + // Copying an empty ReadOnlySpan3D to an empty array is just a no-op + span3d.CopyTo(target); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_CopyTo_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + int[] target = new int[array.Length]; + + span3d.CopyTo(target); + + int[] expected = Enumerable.Range(1, 12).ToArray(); + + CollectionAssert.AreEqual(expected, target); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).CopyTo(Span.Empty)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_CopyTo_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + int[] target = new int[8]; + + span3d.CopyTo(target); + + int[] expected = { 2, 3, 5, 6, 8, 9, 11, 12 }; + + CollectionAssert.AreEqual(target, expected); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).CopyTo(Span.Empty)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 0, 0, 1, 2, 2, 2).CopyTo(Span.Empty)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_CopyTo3D_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + int[,,] target = new int[2, 2, 3]; + + span3d.CopyTo(target); + + CollectionAssert.AreEqual(array, target); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).CopyTo(Span3D.Empty)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_CopyTo3D_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + int[,,] target = new int[2, 2, 2]; + + span3d.CopyTo(target); + + int[,,] expected = + { + { + { 2, 3 }, + { 5, 6 } + }, + { + { 8, 9 }, + { 11, 12 } + } + }; + + CollectionAssert.AreEqual(target, expected); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).CopyTo(new Span3D(target))); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_TryCopyTo() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + int[] target = new int[array.Length]; + + Assert.IsTrue(span3d.TryCopyTo(target)); + Assert.IsFalse(span3d.TryCopyTo(Span.Empty)); + + int[] expected = Enumerable.Range(1, 12).ToArray(); + + CollectionAssert.AreEqual(target, expected); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_TryCopyTo3D() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + int[,,] target = new int[2, 2, 3]; + + Assert.IsTrue(span3d.TryCopyTo(target)); + Assert.IsFalse(span3d.TryCopyTo(Span3D.Empty)); + + CollectionAssert.AreEqual(target, array); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_GetPinnableReference() + { + // Here we test that a ref from an empty ReadOnlySpan3D returns a null ref + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref Unsafe.AsRef(in ReadOnlySpan3D.Empty.GetPinnableReference()))); + + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + ref int r0 = ref Unsafe.AsRef(in span3d.GetPinnableReference()); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0, 0])); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_DangerousGetReference() + { + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref ReadOnlySpan3D.Empty.DangerousGetReference())); + + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + ref int r0 = ref span3d.DangerousGetReference(); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0, 0])); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Index_Indexer_1() + { + int[,,] array = new int[4, 4, 4]; + + ReadOnlySpan3D span3d = new(array); + + ref int arrayRef = ref array[1, 2, 3]; + ref readonly int span3dRef = ref span3d[1, 2, ^1]; + + Assert.IsTrue(Unsafe.AreSame(ref arrayRef, ref Unsafe.AsRef(in span3dRef))); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Index_Indexer_2() + { + int[,,] array = new int[4, 4, 4]; + + ReadOnlySpan3D span3d = new(array); + + ref int arrayRef = ref array[2, 1, 0]; + ref readonly int span3dRef = ref span3d[^2, ^3, ^4]; + + Assert.IsTrue(Unsafe.AreSame(ref arrayRef, ref Unsafe.AsRef(in span3dRef))); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Index_Indexer_Fail() + { + int[,,] array = new int[4, 4, 4]; + + _ = Assert.ThrowsExactly(() => + { + ReadOnlySpan3D span3d = new(array); + + ref readonly int span3dRef = ref span3d[^6, 2, 1]; + }); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Range_Indexer_1() + { + int[,,] array = new int[4, 4, 4]; + + ReadOnlySpan3D span3d = new(array); + ReadOnlySpan3D slice = span3d[1.., 1.., 1..]; + + Assert.AreEqual(27, slice.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[1, 1, 1], ref Unsafe.AsRef(in slice[0, 0, 0]))); + Assert.IsTrue(Unsafe.AreSame(ref array[3, 3, 3], ref Unsafe.AsRef(in slice[2, 2, 2]))); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Range_Indexer_2() + { + int[,,] array = new int[4, 4, 4]; + + ReadOnlySpan3D span3d = new(array); + ReadOnlySpan3D slice = span3d[0..^2, 1..^1, 0..^2]; + + Assert.AreEqual(8, slice.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0, 1, 0], ref Unsafe.AsRef(in slice[0, 0, 0]))); + Assert.IsTrue(Unsafe.AreSame(ref array[1, 2, 1], ref Unsafe.AsRef(in slice[1, 1, 1]))); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Range_Indexer_Fail() + { + int[,,] array = new int[4, 4, 4]; + + _ = Assert.ThrowsExactly(() => + { + ReadOnlySpan3D span3d = new(array); + + _ = span3d[0..6, 2..^1, 0..2]; + }); + } +#endif + + [TestMethod] + public void Test_ReadOnlySpan3DT_Slice_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + ReadOnlySpan3D slice1 = span3d.Slice(1, 0, 1, 1, 2, 2); + + Assert.AreEqual(4, slice1.Length); + Assert.AreEqual(1, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(8, slice1[0, 0, 0]); + Assert.AreEqual(12, slice1[0, 1, 1]); + + ReadOnlySpan3D slice2 = span3d.Slice(0, 1, 0, 2, 1, 3); + + Assert.AreEqual(6, slice2.Length); + Assert.AreEqual(2, slice2.Depth); + Assert.AreEqual(1, slice2.Height); + Assert.AreEqual(3, slice2.Width); + Assert.AreEqual(4, slice2[0, 0, 0]); + Assert.AreEqual(10, slice2[1, 0, 0]); + Assert.AreEqual(12, slice2[1, 0, 2]); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(-1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(0, 0, 0, 0, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(0, 0, 0, 1, 0, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(0, 0, 0, 1, 1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(10, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(0, 10, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).Slice(0, 0, 10, 1, 1, 1)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_Slice_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + ReadOnlySpan3D slice1 = span3d.Slice(0, 0, 0, 2, 2, 2); + + Assert.AreEqual(8, slice1.Length); + Assert.AreEqual(2, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(1, slice1[0, 0, 0]); + Assert.AreEqual(11, slice1[1, 1, 1]); + + ReadOnlySpan3D slice2 = slice1.Slice(1, 0, 1, 1, 2, 1); + + Assert.AreEqual(2, slice2.Length); + Assert.AreEqual(1, slice2.Depth); + Assert.AreEqual(2, slice2.Height); + Assert.AreEqual(1, slice2.Width); + Assert.AreEqual(8, slice2[0, 0, 0]); + Assert.AreEqual(11, slice2[0, 1, 0]); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_ReadOnlySpan3DT_GetRowSpan() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + ReadOnlySpan span = span3d.GetRowSpan(1, 0); + + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(in span[0]), + ref array[1, 0, 0])); + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(in span[2]), + ref array[1, 0, 2])); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRowSpan(-1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRowSpan(0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRowSpan(5, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRowSpan(0, 5)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_GetSliceSpan() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + ReadOnlySpan2D slice = span3d.GetSliceSpan(1); + + Assert.AreEqual(2, slice.Height); + Assert.AreEqual(3, slice.Width); + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(in slice[0, 0]), + ref array[1, 0, 0])); + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(in slice[1, 2]), + ref array[1, 1, 2])); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetSliceSpan(-1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetSliceSpan(5)); + } +#endif + + [TestMethod] + public void Test_ReadOnlySpan3DT_TryGetSpan_From1DArray_1() + { + int[] array = Enumerable.Range(1, 24).ToArray(); + + ReadOnlySpan3D span3d = new(array, 2, 3, 4); + + bool success = span3d.TryGetSpan(out ReadOnlySpan span); + + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0], ref Unsafe.AsRef(in span[0]))); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_TryGetSpan_From1DArray_2() + { + int[] array = Enumerable.Range(1, 24).ToArray(); + + ReadOnlySpan3D span3d = new ReadOnlySpan3D(array, 2, 3, 4).Slice(1, 0, 0, 1, 3, 4); + + bool success = span3d.TryGetSpan(out ReadOnlySpan span); + + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[12], ref Unsafe.AsRef(in span[0]))); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_TryGetSpan_From1DArray_3() + { + int[] array = Enumerable.Range(1, 24).ToArray(); + + ReadOnlySpan3D span3d = new ReadOnlySpan3D(array, 2, 3, 4).Slice(0, 1, 1, 2, 2, 2); + + bool success = span3d.TryGetSpan(out ReadOnlySpan span); + + Assert.IsFalse(success); + Assert.AreEqual(0, span.Length); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3947 + [TestMethod] + public void Test_ReadOnlySpan3DT_TryGetSpan_From1DArray_4() + { + int[] array = new int[128]; + ReadOnlySpan3D span3d = new(array, 2, 4, 16); + + bool success = span3d.TryGetSpan(out ReadOnlySpan span); + + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0], ref Unsafe.AsRef(in span[0]))); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_TryGetSpan_From3DArray_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + bool success = span3d.TryGetSpan(out ReadOnlySpan span); + +#if NETFRAMEWORK + // Can't get a ReadOnlySpan over a T[,,] array on .NET Standard 2.0 + Assert.IsFalse(success); + Assert.AreEqual(0, span.Length); +#else + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); +#endif + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_TryGetSpan_From3DArray_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + bool success = span3d.TryGetSpan(out ReadOnlySpan span); + + Assert.IsFalse(success); + Assert.IsTrue(span.IsEmpty); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_ToArray_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array); + + int[,,] copy = span3d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + Assert.AreEqual(copy.GetLength(2), array.GetLength(2)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_ToArray_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlySpan3D span3d = new(array, 0, 0, 0, 1, 2, 2); + + int[,,] copy = span3d.ToArray(); + + Assert.AreEqual(1, copy.GetLength(0)); + Assert.AreEqual(2, copy.GetLength(1)); + Assert.AreEqual(2, copy.GetLength(2)); + + int[,,] expected = + { + { + { 1, 2 }, + { 4, 5 } + } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_Equals() + { + int[,,] array = new int[1, 1, 1]; + + _ = Assert.ThrowsExactly(() => + { + ReadOnlySpan3D span3d = new(array); + + _ = span3d.Equals(null); + }); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_GetHashCode() + { + int[,,] array = new int[1, 1, 1]; + + _ = Assert.ThrowsExactly(() => + { + ReadOnlySpan3D span3d = new(array); + + _ = span3d.GetHashCode(); + }); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_ToString() + { + int[,,] array = new int[2, 2, 3]; + + ReadOnlySpan3D span3d = new(array); + + string text = span3d.ToString(); + + const string expected = "CommunityToolkit.HighPerformance.ReadOnlySpan3D[2, 2, 3]"; + + Assert.AreEqual(expected, text); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_opEquals() + { + int[,,] array = new int[2, 2, 3]; + + ReadOnlySpan3D span3d_1 = new(array); + ReadOnlySpan3D span3d_2 = new(array); + + Assert.IsTrue(span3d_1 == span3d_2); + Assert.IsFalse(span3d_1 == ReadOnlySpan3D.Empty); + Assert.IsTrue(ReadOnlySpan3D.Empty == ReadOnlySpan3D.Empty); + + ReadOnlySpan3D span3d_3 = new(array, 0, 0, 0, 1, 2, 3); + + Assert.IsFalse(span3d_1 == span3d_3); + Assert.IsFalse(span3d_3 == ReadOnlySpan3D.Empty); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_ImplicitCast() + { + int[,,] array = new int[2, 2, 3]; + + ReadOnlySpan3D span3d_1 = array; + ReadOnlySpan3D span3d_2 = new(array); + + Assert.IsTrue(span3d_1 == span3d_2); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_GetRow() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan3D(array).GetRow(1, 0)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in value), ref array[1, 0, i++])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan3D(array).GetRow(1, 0); + + int[] expected = { 7, 8, 9 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRow(-1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRow(0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRow(2, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetRow(0, 2)); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Pointer_GetRow() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + 10, 11, 12 + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetRow(1, 1)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in value), ref array[9 + i++])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetRow(1, 1); + + int[] expected = { 10, 11, 12 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetRow(-1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetRow(2, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetRow(0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetRow(0, 2)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_GetColumn() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan3D(array).GetColumn(0, 2); + + int[] expected = { 3, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetColumn(0, 3)); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Pointer_GetColumn() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + 10, 11, 12 + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(0, 1)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in value), ref array[(i++ * 3) + 1])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(1, 2); + + int[] expected = { 9, 12 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(0, 3)); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_GetDepthColumn() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan3D(array).GetDepthColumn(1, 1); + + int[] expected = { 5, 11 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetDepthColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetDepthColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetDepthColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array).GetDepthColumn(0, 3)); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Pointer_GetDepthColumn() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + 10, 11, 12 + }; + + int i = 0; + foreach (ref readonly int value in new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetDepthColumn(1, 1)) + { + Assert.IsTrue(Unsafe.AreSame(ref Unsafe.AsRef(in value), ref array[(i++ * (2 * 3)) + (1 * 3) + 1])); + } + + ReadOnlyRefEnumerable enumerable = new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetDepthColumn(1, 2); + + int[] expected = { 6, 12 }; + + CollectionAssert.AreEqual(expected, enumerable.ToArray()); + + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new ReadOnlySpan3D(array, 2, 2, 3, 0, 0).GetColumn(0, 3)); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_GetEnumerator() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + int[] result = new int[4]; + int i = 0; + + foreach (int item in new ReadOnlySpan3D(array, 0, 1, 1, 2, 1, 2)) + { + result[i++] = item; + } + + int[] expected = { 5, 6, 11, 12 }; + + CollectionAssert.AreEqual(expected, result); + } + + [TestMethod] + public unsafe void Test_ReadOnlySpan3DT_Pointer_GetEnumerator() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + 10, 11, 12 + }; + + int[] result = new int[4]; + int i = 0; + + foreach (int item in new ReadOnlySpan3D(array + 4, 2, 1, 2, 1, 3)) + { + result[i++] = item; + } + + int[] expected = { 5, 6, 11, 12 }; + + CollectionAssert.AreEqual(expected, result); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_GetEnumerator_Empty() + { + ReadOnlySpan3D.Enumerator enumerator = ReadOnlySpan3D.Empty.GetEnumerator(); + + Assert.IsFalse(enumerator.MoveNext()); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_ReadOnlyRefEnumerable_Misc() + { + int[,,] array1 = + { + { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 }, { 13, 14, 15, 16 } }, + { { 17, 18, 19, 20 }, { 21, 22, 23, 24 }, { 25, 26, 27, 28 }, { 29, 30, 31, 32 } } + }; + + ReadOnlySpan3D span1 = array1; + + int[,,] array2 = new int[2, 4, 4]; + + // Copy to enumerable with source step == 1, destination step == 1 + span1.GetRow(0, 0).CopyTo(array2.GetRow(0, 0)); + + // Copy enumerable with source step == 1, destination step != 1 + span1.GetRow(0, 1).CopyTo(array2.GetColumn(0, 1)); + + // Copy enumerable with source step != 1, destination step == 1 + span1.GetColumn(0, 2).CopyTo(array2.GetRow(0, 2)); + + // Copy enumerable with source step != 1, destination step != 1 + span1.GetColumn(0, 3).CopyTo(array2.GetColumn(0, 3)); + + // Copy from second slice to first slice in array2 + span1.GetRow(1, 0).CopyTo(array2.GetRow(1, 0)); + span1.GetColumn(1, 1).CopyTo(array2.GetColumn(1, 2)); + + // Copy depth column at position (1, 0) to row at depth 0, row 1 + span1.GetDepthColumn(1, 0).CopyTo(array2.GetRow(0, 1)); + + // Copy depth column at position (2, 1) to column at depth 1, column 1 + span1.GetDepthColumn(2, 1).CopyTo(array2.GetColumn(1, 1)); + + int[,,] result = + { + { { 1, 5, 3, 4 }, { 5, 21, 0, 8 }, { 3, 7, 11, 12 }, { 0, 8, 0, 16 } }, + { { 17, 10, 18, 20 }, { 0, 26, 22, 0 }, { 0, 0, 26, 0 }, { 0, 0, 30, 0 } } + }; + + CollectionAssert.AreEqual(array2, result); + + // Test a valid and an invalid TryCopyTo call with the RefEnumerable overload + bool shouldBeTrue = span1.GetRow(0, 0).TryCopyTo(array2.GetColumn(0, 0)); + bool shouldBeFalse = span1.GetRow(0, 0).TryCopyTo(default(RefEnumerable)); + + result = new[,,] + { + { { 1, 5, 3, 4 }, { 2, 21, 0, 8 }, { 3, 7, 11, 12 }, { 4, 8, 0, 16 } }, + { { 17, 10, 18, 20 }, { 0, 26, 22, 0 }, { 0, 0, 26, 0 }, { 0, 0, 30, 0 } } + }; + + CollectionAssert.AreEqual(array2, result); + + Assert.IsTrue(shouldBeTrue); + Assert.IsFalse(shouldBeFalse); + } + + [TestMethod] + public void Test_ReadOnlySpan3DT_ReadOnlyRefEnumerable_Cast() + { + int[,,] array1 = + { + { + { 1, 2, 3, 4 }, + { 5, 6, 7, 8 }, + { 9, 10, 11, 12 }, + { 13, 14, 15, 16 } + }, + { + { 17, 18, 19, 20 }, + { 21, 22, 23, 24 }, + { 25, 26, 27, 28 }, + { 29, 30, 31, 32 } + } + }; + + int[] result = { 5, 6, 7, 8 }; + + // Cast a RefEnumerable to a readonly one and verify the contents + int[] row = ((ReadOnlyRefEnumerable)array1.GetRow(0, 1)).ToArray(); + + CollectionAssert.AreEqual(result, row); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_ReadOnlySpan3DT_FromMemoryManager_Indexing() + { + const int w = 10; + const int h = 10; + const int d = 5; + const int l = w * h * d; + + byte[] b = new byte[l]; + short[] s = new short[l]; + + for (int i = 0; i < l; ++i) + { + b[i] = (byte)(i % 256); + s[i] = (short)i; + } + + Memory3DTester byteTester = new(d, w, h, b); + Span3D byteSpan3DFromArray = byteTester.GetMemory3DFromArray().Span; + + Assert.AreEqual(11, byteSpan3DFromArray[0, 0, 0]); + + Span3D byteSpan3DFromMemoryManager = byteTester.GetMemory3DFromMemoryManager().Span; + + Assert.AreEqual(11, byteSpan3DFromMemoryManager[0, 0, 0]); + + Memory3DTester shortTester = new(d, w, h, s); + Span3D shortSpan3DFromArray = shortTester.GetMemory3DFromArray().Span; + Span3D shortSpan3DFromMemoryManager = shortTester.GetMemory3DFromMemoryManager().Span; + + Assert.AreEqual(11, shortSpan3DFromArray[0, 0, 0]); + Assert.AreEqual(11, shortSpan3DFromMemoryManager[0, 0, 0]); + } +#endif +} + +#if NET6_0_OR_GREATER +public sealed class Memory3DTester : MemoryManager + where T : unmanaged +{ + private readonly T[] data; + + public Memory3DTester(int d, int w, int h, T[] data) + { + if (d < 2 || w < 2 || h < 2) + { + throw new ArgumentException("The 'd', 'w', and 'h' arguments must be at least 2."); + } + + this.data = data; + + Width = w; + Height = h; + Depth = d; + } + + public int Width { get; } + + public int Height { get; } + + public int Depth { get; } + + public Memory3D GetMemory3DFromMemoryManager() + { + return new(this, Width + 1, Depth - 1, Height - 1, Width - 1, 1, 1); + } + + public Memory3D GetMemory3DFromArray() + { + return new(this.data, Width + 1, Depth - 1, Height - 1, Width - 1, 1, 1); + } + + /// + public override Span GetSpan() + { + return new(this.data); + } + + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + return default; + } + + /// + public override void Unpin() + { + } + + /// + protected override void Dispose(bool disposing) + { + } +} +#endif \ No newline at end of file diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Span3D{T}.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Span3D{T}.cs new file mode 100644 index 00000000..55ef7dfc --- /dev/null +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Memory/Test_Span3D{T}.cs @@ -0,0 +1,1324 @@ +// 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.Linq; +using System.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.Enumerables; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.HighPerformance.UnitTests; + +[TestClass] +public class Test_Span3DT +{ + [TestMethod] + public void Test_Span3DT_Empty() + { + // Like in the tests for Memory3D, here we validate a number of empty spans + Span3D empty1 = default; + + Assert.IsTrue(empty1.IsEmpty); + Assert.AreEqual(0, empty1.Length); + Assert.AreEqual(0, empty1.Depth); + Assert.AreEqual(0, empty1.Height); + Assert.AreEqual(0, empty1.Width); + + Span3D empty2 = Span3D.Empty; + + Assert.IsTrue(empty2.IsEmpty); + Assert.AreEqual(0, empty2.Length); + Assert.AreEqual(0, empty2.Depth); + Assert.AreEqual(0, empty2.Height); + Assert.AreEqual(0, empty2.Width); + + Span3D empty3 = new int[0, 2, 3]; + + Assert.IsTrue(empty3.IsEmpty); + Assert.AreEqual(0, empty3.Length); + Assert.AreEqual(0, empty3.Depth); + Assert.AreEqual(2, empty3.Height); + Assert.AreEqual(3, empty3.Width); + + Span3D empty4 = new int[2, 0, 3]; + + Assert.IsTrue(empty4.IsEmpty); + Assert.AreEqual(0, empty4.Length); + Assert.AreEqual(2, empty4.Depth); + Assert.AreEqual(0, empty4.Height); + Assert.AreEqual(3, empty4.Width); + + Span3D empty5 = new int[2, 3, 0]; + + Assert.IsTrue(empty5.IsEmpty); + Assert.AreEqual(0, empty5.Length); + Assert.AreEqual(2, empty5.Depth); + Assert.AreEqual(3, empty5.Height); + Assert.AreEqual(0, empty5.Width); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public unsafe void Test_Span3DT_RefConstructor() + { + Span span = stackalloc[] + { + 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12 + }; + + // Test for a Span3D instance created from a target reference. This is only supported + // on runtimes with fast Span support (as we need the API to power this with just a ref). + Span3D span3d = Span3D.DangerousCreate(ref span[0], 2, 2, 3, 0, 0); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(12, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(3, span3d.Width); + + span3d[0, 0, 0] = 99; + span3d[1, 1, 2] = 101; + + // Validate that those values were mapped to the right spot in the target span + Assert.AreEqual(99, span[0]); + Assert.AreEqual(101, span[11]); + + // A few cases with invalid indices + _ = Assert.ThrowsExactly(() => Span3D.DangerousCreate(ref Unsafe.AsRef(null), -1, 0, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => Span3D.DangerousCreate(ref Unsafe.AsRef(null), 1, -2, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => Span3D.DangerousCreate(ref Unsafe.AsRef(null), 1, 0, -5, 0, 0)); + _ = Assert.ThrowsExactly(() => Span3D.DangerousCreate(ref Unsafe.AsRef(null), 1, 0, 0, -1, 0)); + _ = Assert.ThrowsExactly(() => Span3D.DangerousCreate(ref Unsafe.AsRef(null), 1, 0, 0, 0, -1)); + } +#endif + + [TestMethod] + public unsafe void Test_Span3DT_PtrConstructor() + { + int* ptr = stackalloc[] + { + 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12 + }; + + // Same as above, but creating a Span3D from a raw pointer + Span3D span3d = new(ptr, 2, 2, 3, 0, 0); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(12, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(3, span3d.Width); + + span3d[0, 0, 0] = 99; + span3d[1, 1, 2] = 101; + + Assert.AreEqual(99, ptr[0]); + Assert.AreEqual(101, ptr[11]); + + _ = Assert.ThrowsExactly(() => new Span3D((void*)0, -1, 0, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => new Span3D((void*)0, 1, -2, 0, 0, 0)); + _ = Assert.ThrowsExactly(() => new Span3D((void*)0, 1, 0, -5, 0, 0)); + _ = Assert.ThrowsExactly(() => new Span3D((void*)0, 1, 0, 0, -1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D((void*)0, 1, 0, 0, 0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D((void*)0, 1, 1, 1, 0, 0)); + } + + [TestMethod] + public void Test_Span3DT_Array1DConstructor() + { + int[] array = Enumerable.Range(1, 20).ToArray(); + + // Same as above, but wrapping a 1D array with data in row-major order + Span3D span3d = new(array, 1, 2, 2, 2, 2, 1); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(8, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(2, span3d.Width); + + span3d[0, 0, 0] = 99; + span3d[1, 1, 1] = 101; + + Assert.AreEqual(99, array[1]); + Assert.AreEqual(101, array[13]); + + // The first check fails due to the array covariance test mentioned in the Memory3D tests. + // The others just validate a number of cases with invalid arguments (e.g. out of range). + _ = Assert.ThrowsExactly(() => new Span3D(new string[1], 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, -1, 1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, -1, 1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 1, -1, 1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 1, 1, -1, 0, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 1, 1, 1, -1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 1, 1, 1, 0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 4, 4, 4, 0, 0)); + } + + [TestMethod] + public void Test_Span3DT_Array3DConstructor_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, but directly wrapping a 3D array + Span3D span3d = new(array); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(12, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(3, span3d.Width); + + span3d[0, 1, 2] = 99; + span3d[1, 0, 1] = 101; + + Assert.AreEqual(99, array[0, 1, 2]); + Assert.AreEqual(101, array[1, 0, 1]); + + _ = Assert.ThrowsExactly(() => new Span3D(new string[1, 1, 1])); + } + + [TestMethod] + public void Test_Span3DT_Array3DConstructor_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, but with a custom slicing over the target 3D array + Span3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + Assert.IsFalse(span3d.IsEmpty); + Assert.AreEqual(8, span3d.Length); + Assert.AreEqual(2, span3d.Depth); + Assert.AreEqual(2, span3d.Height); + Assert.AreEqual(2, span3d.Width); + + span3d[1, 1, 1] = 101; + + Assert.AreEqual(101, array[1, 1, 2]); + + _ = Assert.ThrowsExactly(() => new Span3D(new string[1, 1, 1], 0, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, -1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 0, 0, 3, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 0, 0, 1, 3, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 0, 0, 1, 1, 5)); + } + + [TestMethod] + public void Test_Span3DT_FillAndClear_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Tests for the Fill and Clear APIs for Span3D. These should fill + // or clear the entire wrapped 3D array (just like e.g. Span.Fill). + Span3D span3d = new(array); + + span3d.Fill(42); + + Assert.IsTrue(array.Cast().All(n => n == 42)); + + span3d.Clear(); + + Assert.IsTrue(array.Cast().All(n => n == 0)); + } + + [TestMethod] + public void Test_Span3DT_Fill_Empty() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, but with an initial slicing as well to ensure + // these method work correctly with different internal offsets + Span3D span3d = new(array, 0, 0, 0, 0, 0, 0); + + span3d.Fill(42); + CollectionAssert.AreEqual(array, array); + + span3d.Clear(); + CollectionAssert.AreEqual(array, array); + } + + [TestMethod] + public void Test_Span3DT_FillAndClear_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 10, 20, 30 }, + { 40, 50, 60 } + } + }; + + // Same as above, just with different slicing to a target smaller 3D area + Span3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + span3d.Fill(42); + + int[,,] filled = + { + { + { 1, 42, 42 }, + { 4, 42, 42 } + }, + { + { 10, 42, 42 }, + { 40, 42, 42 } + } + }; + + CollectionAssert.AreEqual(array, filled); + + span3d.Clear(); + + int[,,] cleared = + { + { + { 1, 0, 0 }, + { 4, 0, 0 } + }, + { + { 10, 0, 0 }, + { 40, 0, 0 } + } + }; + + CollectionAssert.AreEqual(array, cleared); + } + + [TestMethod] + public void Test_Span3DT_CopyTo_Empty() + { + Span3D span3d = Span3D.Empty; + + int[] target = new int[0]; + + // Copying an empty Span3D to an empty array is just a no-op + span3d.CopyTo(target); + } + + [TestMethod] + public void Test_Span3DT_CopyTo_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + int[] target = new int[array.Length]; + + // Here we copy a Span3D to a target Span mapping an array. + // This is valid, and the data will just be copied in row-major order, + // one slice at a time. + span3d.CopyTo(target); + + int[] expected = Enumerable.Range(1, 12).ToArray(); + + CollectionAssert.AreEqual(expected, target); + + // Exception due to the target span being too small for the source Span3D instance + _ = Assert.ThrowsExactly(() => new Span3D(array).CopyTo(Span.Empty)); + } + + [TestMethod] + public void Test_Span3DT_CopyTo_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but with different initial slicing + Span3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + int[] target = new int[8]; + + span3d.CopyTo(target); + + int[] expected = { 2, 3, 5, 6, 8, 9, 11, 12 }; + + CollectionAssert.AreEqual(target, expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array).CopyTo(Span.Empty)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 0, 0, 1, 2, 2, 2).CopyTo(Span.Empty)); + } + + [TestMethod] + public void Test_Span3DT_CopyTo3D_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + int[,,] target = new int[2, 2, 3]; + + // Same as above, but copying to a target Span3D instead. Note + // that this method uses the implicit T[,,] to Span3D conversion. + span3d.CopyTo(target); + + CollectionAssert.AreEqual(array, target); + + _ = Assert.ThrowsExactly(() => new Span3D(array).CopyTo(Span3D.Empty)); + } + + [TestMethod] + public void Test_Span3DT_CopyTo3D_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but with extra initial slicing + Span3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + int[,,] target = new int[2, 2, 2]; + + span3d.CopyTo(target); + + int[,,] expected = + { + { + { 2, 3 }, + { 5, 6 } + }, + { + { 8, 9 }, + { 11, 12 } + } + }; + + CollectionAssert.AreEqual(target, expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array).CopyTo(new Span3D(target))); + } + + [TestMethod] + public void Test_Span3DT_TryCopyTo() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + int[] target = new int[array.Length]; + + // Here we test the safe TryCopyTo method, which will fail gracefully + Assert.IsTrue(span3d.TryCopyTo(target)); + Assert.IsFalse(span3d.TryCopyTo(Span.Empty)); + + int[] expected = Enumerable.Range(1, 12).ToArray(); + + CollectionAssert.AreEqual(target, expected); + } + + [TestMethod] + public void Test_Span3DT_TryCopyTo3D() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but copying to a 3D array with the safe TryCopyTo method + Span3D span3d = new(array); + + int[,,] target = new int[2, 2, 3]; + + Assert.IsTrue(span3d.TryCopyTo(target)); + Assert.IsFalse(span3d.TryCopyTo(Span3D.Empty)); + + CollectionAssert.AreEqual(target, array); + } + + [TestMethod] + public unsafe void Test_Span3DT_GetPinnableReference() + { + // Here we test that a ref from an empty Span3D returns a null ref + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref Span3D.Empty.GetPinnableReference())); + + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + ref int r0 = ref span3d.GetPinnableReference(); + + // Here we test that GetPinnableReference returns a ref to the first array element + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0, 0])); + } + + [TestMethod] + public unsafe void Test_Span3DT_DangerousGetReference() + { + // Same as above, but using DangerousGetReference instead (faster, no conditional check) + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(null), + ref Span3D.Empty.DangerousGetReference())); + + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + ref int r0 = ref span3d.DangerousGetReference(); + + Assert.IsTrue(Unsafe.AreSame(ref r0, ref array[0, 0, 0])); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public unsafe void Test_Span3DT_Index_Indexer_1() + { + int[,,] array = new int[4, 4, 4]; + + Span3D span3d = new(array); + + ref int arrayRef = ref array[1, 2, 3]; + ref int span3dRef = ref span3d[1, 2, ^1]; + + Assert.IsTrue(Unsafe.AreSame(ref arrayRef, ref span3dRef)); + } + + [TestMethod] + public unsafe void Test_Span3DT_Index_Indexer_2() + { + int[,,] array = new int[4, 4, 4]; + + Span3D span3d = new(array); + + ref int arrayRef = ref array[2, 1, 0]; + ref int span3dRef = ref span3d[^2, ^3, ^4]; + + Assert.IsTrue(Unsafe.AreSame(ref arrayRef, ref span3dRef)); + } + + [TestMethod] + public unsafe void Test_Span3DT_Index_Indexer_Fail() + { + int[,,] array = new int[4, 4, 4]; + + _ = Assert.ThrowsExactly(() => + { + Span3D span3d = new(array); + + ref int span3dRef = ref span3d[^6, 2, 1]; + }); + } + + [TestMethod] + public unsafe void Test_Span3DT_Range_Indexer_1() + { + int[,,] array = new int[4, 4, 4]; + + Span3D span3d = new(array); + Span3D slice = span3d[1.., 1.., 1..]; + + Assert.AreEqual(27, slice.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[1, 1, 1], ref slice[0, 0, 0])); + Assert.IsTrue(Unsafe.AreSame(ref array[3, 3, 3], ref slice[2, 2, 2])); + } + + [TestMethod] + public unsafe void Test_Span3DT_Range_Indexer_2() + { + int[,,] array = new int[4, 4, 4]; + + Span3D span3d = new(array); + Span3D slice = span3d[0..^2, 1..^1, 0..^2]; + + Assert.AreEqual(8, slice.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0, 1, 0], ref slice[0, 0, 0])); + Assert.IsTrue(Unsafe.AreSame(ref array[1, 2, 1], ref slice[1, 1, 1])); + } + + [TestMethod] + public unsafe void Test_Span3DT_Range_Indexer_Fail() + { + int[,,] array = new int[4, 4, 4]; + + _ = Assert.ThrowsExactly(() => + { + Span3D span3d = new(array); + + _ = span3d[0..6, 2..^1, 0..2]; + }); + } +#endif + + [TestMethod] + public void Test_Span3DT_Slice_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Here we have a number of tests that just take an initial 3D array, create a Span3D, + // perform a number of slicing operations and then validate the parameters for the resulting + // instances, and that the indexer works correctly and maps to the right original elements. + Span3D span3d = new(array); + + Span3D slice1 = span3d.Slice(1, 0, 1, 1, 2, 2); + + Assert.AreEqual(4, slice1.Length); + Assert.AreEqual(1, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(8, slice1[0, 0, 0]); + Assert.AreEqual(12, slice1[0, 1, 1]); + + Span3D slice2 = span3d.Slice(0, 1, 0, 2, 1, 3); + + Assert.AreEqual(6, slice2.Length); + Assert.AreEqual(2, slice2.Depth); + Assert.AreEqual(1, slice2.Height); + Assert.AreEqual(3, slice2.Width); + Assert.AreEqual(4, slice2[0, 0, 0]); + Assert.AreEqual(10, slice2[1, 0, 0]); + Assert.AreEqual(12, slice2[1, 0, 2]); + + // Some checks for invalid arguments + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(-1, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(0, -1, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(0, 0, -1, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(0, 0, 0, 0, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(0, 0, 0, 1, 0, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(0, 0, 0, 1, 1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(10, 0, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(0, 10, 0, 1, 1, 1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).Slice(0, 0, 10, 1, 1, 1)); + } + + [TestMethod] + public void Test_Span3DT_Slice_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + // Same as above, but with some different slicing + Span3D slice1 = span3d.Slice(0, 0, 0, 2, 2, 2); + + Assert.AreEqual(8, slice1.Length); + Assert.AreEqual(2, slice1.Depth); + Assert.AreEqual(2, slice1.Height); + Assert.AreEqual(2, slice1.Width); + Assert.AreEqual(1, slice1[0, 0, 0]); + Assert.AreEqual(11, slice1[1, 1, 1]); + + Span3D slice2 = slice1.Slice(1, 0, 1, 1, 2, 1); + + Assert.AreEqual(2, slice2.Length); + Assert.AreEqual(1, slice2.Depth); + Assert.AreEqual(2, slice2.Height); + Assert.AreEqual(1, slice2.Width); + Assert.AreEqual(8, slice2[0, 0, 0]); + Assert.AreEqual(11, slice2[0, 1, 0]); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_Span3DT_GetRowSpan() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + // Here we create a Span3D from a 3D array and want to get a Span from + // a specific row. This is only supported on runtimes with fast Span support + // for the same reason mentioned in the Memory3D tests (we need the Span + // constructor that only takes a target ref). Then we just get some references + // to items in this span and compare them against references into the original + // 3D array to ensure they match and point to the correct elements from there. + Span span = span3d.GetRowSpan(1, 0); + + Assert.IsTrue(Unsafe.AreSame( + ref span[0], + ref array[1, 0, 0])); + Assert.IsTrue(Unsafe.AreSame( + ref span[2], + ref array[1, 0, 2])); + + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRowSpan(-1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRowSpan(0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRowSpan(5, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRowSpan(0, 5)); + } + + [TestMethod] + public void Test_Span3DT_GetSliceSpan() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + // Here we create a Span3D from a 3D array and want to get a Span from + // a specific slice. This is only supported on runtimes with fast Span support + // for the same reason mentioned in the Memory3D tests (we need the Span + // constructor that only takes a target ref). Then we just get some references + // to items in this span and compare them against references into the original + // 3D array to ensure they match and point to the correct elements from there. + Span2D slice = span3d.GetSliceSpan(1); + + Assert.AreEqual(2, slice.Height); + Assert.AreEqual(3, slice.Width); + Assert.IsTrue(Unsafe.AreSame( + ref slice[0, 0], + ref array[1, 0, 0])); + Assert.IsTrue(Unsafe.AreSame( + ref slice[1, 2], + ref array[1, 1, 2])); + + _ = Assert.ThrowsExactly(() => new Span3D(array).GetSliceSpan(-1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetSliceSpan(5)); + } +#endif + + [TestMethod] + public void Test_Span3DT_TryGetSpan_From1DArray_1() + { + int[] array = Enumerable.Range(1, 24).ToArray(); + + Span3D span3d = new(array, 2, 3, 4); + + bool success = span3d.TryGetSpan(out Span span); + + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0], ref span[0])); + } + + [TestMethod] + public void Test_Span3DT_TryGetSpan_From1DArray_2() + { + int[] array = Enumerable.Range(1, 24).ToArray(); + + Span3D span3d = new Span3D(array, 2, 3, 4).Slice(1, 0, 0, 1, 3, 4); + + bool success = span3d.TryGetSpan(out Span span); + + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[12], ref span[0])); + } + + [TestMethod] + public void Test_Span3DT_TryGetSpan_From1DArray_3() + { + int[] array = Enumerable.Range(1, 24).ToArray(); + + Span3D span3d = new Span3D(array, 2, 3, 4).Slice(0, 1, 1, 2, 2, 2); + + bool success = span3d.TryGetSpan(out Span span); + + Assert.IsFalse(success); + Assert.AreEqual(0, span.Length); + } + + // See https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/3947 + [TestMethod] + public void Test_Span3DT_TryGetSpan_From1DArray_4() + { + int[] array = new int[128]; + Span3D span3d = new(array, 2, 4, 16); + + bool success = span3d.TryGetSpan(out Span span); + + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); + Assert.IsTrue(Unsafe.AreSame(ref array[0], ref span[0])); + } + + [TestMethod] + public void Test_Span3DT_TryGetSpan_From3DArray_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + Span3D span3d = new(array); + + // This API tries to get a Span for the entire contents of Span3D. + // This only works on runtimes if the underlying data is contiguous + // and of a size that can fit into a single Span. In this specific test, + // this is not expected to work on .NET Standard 2.0 because it can't create a + // Span from a 3D array (reasons explained in the comments for the test above). + bool success = span3d.TryGetSpan(out Span span); + +#if NETFRAMEWORK + // Can't get a Span over a T[,,] array on .NET Standard 2.0 + Assert.IsFalse(success); + Assert.AreEqual(0, span.Length); +#else + Assert.IsTrue(success); + Assert.AreEqual(span.Length, span3d.Length); +#endif + } + + [TestMethod] + public void Test_Span3DT_TryGetSpan_From3DArray_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but this will always fail because we're creating + // a Span3D wrapping non-contiguous data (the pitch is not 0). + Span3D span3d = new(array, 0, 0, 1, 2, 2, 2); + + bool success = span3d.TryGetSpan(out Span span); + + Assert.IsFalse(success); + Assert.IsTrue(span.IsEmpty); + } + + [TestMethod] + public void Test_Span3DT_ToArray_1() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Here we create a Span3D and verify that ToArray() produces + // a 3D array that is identical to the original one being wrapped. + Span3D span3d = new(array); + + int[,,] copy = span3d.ToArray(); + + Assert.AreEqual(copy.GetLength(0), array.GetLength(0)); + Assert.AreEqual(copy.GetLength(1), array.GetLength(1)); + Assert.AreEqual(copy.GetLength(2), array.GetLength(2)); + + CollectionAssert.AreEqual(array, copy); + } + + [TestMethod] + public void Test_Span3DT_ToArray_2() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but with extra initial slicing + Span3D span3d = new(array, 0, 0, 0, 1, 2, 2); + + int[,,] copy = span3d.ToArray(); + + Assert.AreEqual(1, copy.GetLength(0)); + Assert.AreEqual(2, copy.GetLength(1)); + Assert.AreEqual(2, copy.GetLength(2)); + + int[,,] expected = + { + { + { 1, 2 }, + { 4, 5 } + } + }; + + CollectionAssert.AreEqual(expected, copy); + } + + [TestMethod] + public void Test_Span3DT_Equals() + { + int[,,] array = new int[1, 1, 1]; + + // Span3D.Equals always throw (this mirrors the behavior of Span.Equals) + _ = Assert.ThrowsExactly(() => + { + Span3D span3d = new(array); + + _ = span3d.Equals(null); + }); + } + + [TestMethod] + public void Test_Span3DT_GetHashCode() + { + int[,,] array = new int[1, 1, 1]; + + // Same as above, this always throws + _ = Assert.ThrowsExactly(() => + { + Span3D span3d = new(array); + + _ = span3d.GetHashCode(); + }); + } + + [TestMethod] + public void Test_Span3DT_ToString() + { + int[,,] array = new int[2, 2, 3]; + + Span3D span3d = new(array); + + // Verify that we get the nicely formatted string + string text = span3d.ToString(); + + const string expected = "CommunityToolkit.HighPerformance.Span3D[2, 2, 3]"; + + Assert.AreEqual(expected, text); + } + + [TestMethod] + public void Test_Span3DT_opEquals() + { + int[,,] array = new int[2, 2, 3]; + + // Create two Span3D instances wrapping the same array with the same + // parameters, and verify that the equality operators work correctly. + Span3D span3d_1 = new(array); + Span3D span3d_2 = new(array); + + Assert.IsTrue(span3d_1 == span3d_2); + Assert.IsFalse(span3d_1 == Span3D.Empty); + Assert.IsTrue(Span3D.Empty == Span3D.Empty); + + // Same as above, but verify that a sliced span is not reported as equal + Span3D span3d_3 = new(array, 0, 0, 0, 1, 2, 3); + + Assert.IsFalse(span3d_1 == span3d_3); + Assert.IsFalse(span3d_3 == Span3D.Empty); + } + + [TestMethod] + public void Test_Span3DT_ImplicitCast() + { + int[,,] array = new int[2, 2, 3]; + + // Verify that an explicit constructor and the implicit conversion + // operator generate an identical Span3D instance from the array. + Span3D span3d_1 = array; + Span3D span3d_2 = new(array); + + Assert.IsTrue(span3d_1 == span3d_2); + } + + [TestMethod] + public void Test_Span3DT_GetRow() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Get a target row and verify the contents match with our data + RefEnumerable enumerable = new Span3D(array).GetRow(1, 0); + + int[] expected = { 7, 8, 9 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRow(-1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRow(0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRow(2, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetRow(0, 2)); + } + + [TestMethod] + public unsafe void Test_Span3DT_Pointer_GetRow() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + 10, 11, 12 + }; + + // Same as above, but with a Span3D wrapping a raw pointer + RefEnumerable enumerable = new Span3D(array, 2, 2, 3, 0, 0).GetRow(1, 0); + + int[] expected = { 7, 8, 9 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetRow(-1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetRow(0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetRow(2, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetRow(0, 2)); + } + + [TestMethod] + public void Test_Span3DT_GetColumn() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but getting a column instead + RefEnumerable enumerable = new Span3D(array).GetColumn(0, 2); + + int[] expected = { 3, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array).GetColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetColumn(0, 3)); + } + + [TestMethod] + public unsafe void Test_Span3DT_Pointer_GetColumn() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + 10, 11, 12 + }; + + // Same as above, but wrapping a raw pointer + RefEnumerable enumerable = new Span3D(array, 2, 2, 3, 0, 0).GetColumn(0, 2); + + int[] expected = { 3, 6 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetColumn(0, 3)); + } + + [TestMethod] + public void Test_Span3DT_GetDepthColumn() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but getting a column along depth instead + RefEnumerable enumerable = new Span3D(array).GetDepthColumn(1, 1); + + int[] expected = { 5, 11 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array).GetDepthColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetDepthColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetDepthColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array).GetDepthColumn(0, 3)); + } + + [TestMethod] + public unsafe void Test_Span3DT_Pointer_GetDepthColumn() + { + int* array = stackalloc[] + { + 1, 2, 3, + 4, 5, 6, + 7, 8, 9, + 10, 11, 12 + }; + + // Same as above, but wrapping a raw pointer + RefEnumerable enumerable = new Span3D(array, 2, 2, 3, 0, 0).GetDepthColumn(0, 2); + + int[] expected = { 3, 9 }; + + CollectionAssert.AreEqual(enumerable.ToArray(), expected); + + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetDepthColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetDepthColumn(0, -1)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetDepthColumn(2, 0)); + _ = Assert.ThrowsExactly(() => new Span3D(array, 2, 2, 3, 0, 0).GetDepthColumn(0, 3)); + } + + [TestMethod] + public unsafe void Test_Span3DT_GetEnumerator() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + int[] result = new int[8]; + int i = 0; + + // Here we want to test the Span3D enumerator. We create a Span3D instance over + // a given section of the initial 3D array, then iterate over it and store the items + // into a temporary array. We then just compare the contents to ensure they match. + foreach (ref int item in new Span3D(array, 0, 0, 1, 2, 2, 2)) + { + int slice = i / 4; + int row = (i % 4) / 2; + int column = (i % 2) + 1; + + // Check the reference to ensure it points to the right original item + Assert.IsTrue(Unsafe.AreSame( + ref array[slice, row, column], + ref item)); + + // Also store the value to compare it later (redundant, but just in case) + result[i++] = item; + } + + int[] expected = { 2, 3, 5, 6, 8, 9, 11, 12 }; + + CollectionAssert.AreEqual(result, expected); + } + + [TestMethod] + public unsafe void Test_Span3DT_Pointer_GetEnumerator() + { + int* array = stackalloc[] + { + 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12 + }; + + int[] result = new int[8]; + int i = 0; + + // Same test as above, but wrapping a raw pointer + foreach (ref int item in new Span3D(array + 1, 2, 2, 2, 0, 1)) + { + int slice = i / 4; + int row = (i % 4) / 2; + int column = (i % 2); + + int index = ((slice * 6) + (row * 3) + column) + 1; + + // Check the reference again + Assert.IsTrue(Unsafe.AreSame( + ref Unsafe.AsRef(&array[index]), + ref item)); + + result[i++] = item; + } + + int[] expected = { 2, 3, 5, 6, 8, 9, 11, 12 }; + + CollectionAssert.AreEqual(result, expected); + } + + [TestMethod] + public void Test_Span3DT_GetEnumerator_Empty() + { + Span3D.Enumerator enumerator = Span3D.Empty.GetEnumerator(); + + // Ensure that an enumerator from an empty Span3D can't move next + Assert.IsFalse(enumerator.MoveNext()); + } +} \ No newline at end of file From 7ebdd43530ecdd29619ce4e94ad464dc81f95a57 Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Sat, 14 Feb 2026 00:25:38 +0100 Subject: [PATCH 8/9] Add some more array extensions for 3D operations * To complement the new functionality of Memory3D and Span3D --- .../Extensions/ArrayExtensions.3D.cs | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs b/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs index 16808755..606c660b 100644 --- a/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs +++ b/src/CommunityToolkit.HighPerformance/Extensions/ArrayExtensions.3D.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.Enumerables; using CommunityToolkit.HighPerformance.Helpers; #if NETSTANDARD2_1_OR_GREATER using System.Runtime.InteropServices; @@ -78,6 +79,186 @@ public static ref T DangerousGetReferenceAt(this T[,,] array, int i, int j, i #endif } + /// + /// Returns a over a row in a given 3D array instance. + /// + /// The type of elements in the input 3D array instance. + /// The input array instance. + /// The target slice to retrieve (0-based index). + /// The target row to retrieve (0-based index). + /// A with the items from the target row within . + /// The returned value shouldn't be used directly: use this extension in a loop. + /// Thrown when one of the input parameters is out of range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RefEnumerable GetRow(this T[,,] array, int slice, int row) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + int depth = array.GetLength(0); + int height = array.GetLength(1); + + if ((uint)slice >= (uint)depth) + { + ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)height) + { + ThrowArgumentOutOfRangeExceptionForRow(); + } + + int width = array.GetLength(2); + +#if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref array.DangerousGetReferenceAt(slice, row, 0); + + return new(ref r0, width, 1); +#else + ref T r0 = ref array.DangerousGetReferenceAt(slice, row, 0); + IntPtr offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref r0); + + return new(array, offset, width, 1); +#endif + } + + /// + /// Returns a that returns the items from a given column in a given depth slice of a 3D array instance. + /// This extension should be used directly within a loop: + /// + /// int[,,] cube = + /// { + /// { + /// { 1, 2, 3 }, + /// { 4, 5, 6 }, + /// { 7, 8, 9 } + /// }, + /// { + /// { 10, 11, 12 }, + /// { 13, 14, 15 }, + /// { 16, 17, 18 } + /// } + /// }; + /// + /// foreach (ref int number in cube.GetColumn(0, 1)) + /// { + /// // Access the current number in column 1 of depth slice 0 by reference here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of elements in the input 3D array instance. + /// The input array instance. + /// The target depth slice to use (0-based index). + /// The target column to retrieve (0-based index). + /// A wrapper type that will handle the column enumeration for . + /// The returned value shouldn't be used directly: use this extension in a loop. + /// Thrown when one of the input parameters is out of range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RefEnumerable GetColumn(this T[,,] array, int slice, int column) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + int depth = array.GetLength(0); + int height = array.GetLength(1); + int width = array.GetLength(2); + + if ((uint)slice >= (uint)depth) + { + ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)column >= (uint)width) + { + ThrowArgumentOutOfRangeExceptionForColumn(); + } + +#if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref array.DangerousGetReferenceAt(slice, 0, column); + + return new(ref r0, height, width); +#else + ref T r0 = ref array.DangerousGetReferenceAt(slice, 0, column); + IntPtr offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref r0); + + return new(array, offset, height, width); +#endif + } + + /// + /// Returns a that returns the items from a given depth column in a 3D array instance. + /// A depth column consists of all elements with the same row and column coordinates across all depth slices. + /// This extension should be used directly within a loop: + /// + /// int[,,] cube = + /// { + /// { + /// { 1, 2, 3 }, + /// { 4, 5, 6 }, + /// { 7, 8, 9 } + /// }, + /// { + /// { 10, 11, 12 }, + /// { 13, 14, 15 }, + /// { 16, 17, 18 } + /// } + /// }; + /// + /// foreach (ref int number in cube.GetDepthColumn(1, 2)) + /// { + /// // Access elements [0,1,2], [1,1,2], etc. by reference here... + /// } + /// + /// The compiler will take care of properly setting up the loop with the type returned from this method. + /// + /// The type of elements in the input 3D array instance. + /// The input array instance. + /// The target row coordinate (0-based index). + /// The target column coordinate (0-based index). + /// A wrapper type that will handle the depth column enumeration for . + /// The returned value shouldn't be used directly: use this extension in a loop. + /// Thrown when one of the input parameters is out of range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static RefEnumerable GetDepthColumn(this T[,,] array, int row, int column) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + int depth = array.GetLength(0); + int height = array.GetLength(1); + int width = array.GetLength(2); + + if ((uint)row >= (uint)height) + { + ThrowArgumentOutOfRangeExceptionForRow(); + } + + if ((uint)column >= (uint)width) + { + ThrowArgumentOutOfRangeExceptionForColumn(); + } + + #if NETSTANDARD2_1_OR_GREATER + ref T r0 = ref array.DangerousGetReferenceAt(0, row, column); + + // Step size is height * width (size of one depth slice) + return new(ref r0, depth, height * width); + #else + ref T r0 = ref array.DangerousGetReferenceAt(0, row, column); + IntPtr offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref r0); + + // Step size is height * width (size of one depth slice) + return new(array, offset, depth, height * width); + #endif + } + #if NETSTANDARD2_1_OR_GREATER /// /// Creates a new over an input 3D array. @@ -185,6 +366,77 @@ public static Memory AsMemory(this T[,,] array, int depth) return new RawObjectMemoryManager(array, offset, length).Memory; } + + /// + /// Returns a over a row in a given 3D array instance. + /// + /// The type of elements in the input 3D array instance. + /// The input array instance. + /// The target slice to retrieve (0-based index). + /// The target row in the slice to retrieve (0-based index). + /// A with the items from the target row within . + /// Thrown when doesn't match . + /// + /// Thrown when either or is invalid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span GetRowSpan(this T[,,] array, int slice, int row) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + if ((uint)slice >= (uint)array.GetLength(0)) + { + ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)array.GetLength(1)) + { + ThrowArgumentOutOfRangeExceptionForRow(); + } + + ref T r0 = ref array.DangerousGetReferenceAt(slice, row, 0); + + return MemoryMarshal.CreateSpan(ref r0, array.GetLength(2)); + } + + /// + /// Returns a over a row in a given 3D array instance. + /// + /// The type of elements in the input 3D array instance. + /// The input array instance. + /// The target slice to retrieve (0-based index). + /// The target row in the slice to retrieve (0-based index). + /// A with the items from the target row within . + /// Thrown when doesn't match . + /// + /// Thrown when either or is invalid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Memory GetRowMemory(this T[,,] array, int slice, int row) + { + if (array.IsCovariant()) + { + ThrowArrayTypeMismatchException(); + } + + if ((uint)slice >= (uint)array.GetLength(0)) + { + ThrowArgumentOutOfRangeExceptionForSlice(); + } + + if ((uint)row >= (uint)array.GetLength(1)) + { + ThrowArgumentOutOfRangeExceptionForRow(); + } + + ref T r0 = ref array.DangerousGetReferenceAt(slice, row, 0); + IntPtr offset = ObjectMarshal.DangerousGetObjectDataByteOffset(array, ref r0); + + return new RawObjectMemoryManager(array, offset, array.GetLength(2)).Memory; + } #endif /// From 89d6647a1f45e91aa0784b5143f184faef6f44cf Mon Sep 17 00:00:00 2001 From: Mario Guerra Date: Sat, 14 Feb 2026 00:26:49 +0100 Subject: [PATCH 9/9] Expand tests for 3D array extensions * To be more in line with what the tests for 2D array extensions do --- .../Extensions/Test_ArrayExtensions.3D.cs | 579 +++++++++++++++++- 1 file changed, 578 insertions(+), 1 deletion(-) diff --git a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ArrayExtensions.3D.cs b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ArrayExtensions.3D.cs index 63b68d80..c6136c04 100644 --- a/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ArrayExtensions.3D.cs +++ b/tests/CommunityToolkit.HighPerformance.UnitTests/Extensions/Test_ArrayExtensions.3D.cs @@ -2,7 +2,9 @@ // 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.Runtime.CompilerServices; +using CommunityToolkit.HighPerformance.Enumerables; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace CommunityToolkit.HighPerformance.UnitTests.Extensions; @@ -53,4 +55,579 @@ public void Test_ArrayExtensions_3D_DangerousGetReferenceAt_Index() Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); } -} + + [TestMethod] + public void Test_ArrayExtensions_3D_AsSpan3DAndFillArrayMid() + { + bool[,,] test = new bool[3, 3, 3]; + + // To fill an array we now go through the Span3D type, which includes all + // the necessary logic to perform the operation. In these tests we just create + // one through the extension, slice it and then fill it. For instance in this + // one, we're creating a Span3D from coordinates (1, 1, 1), with a depth of + // 2, a height of 2, and a width of 2, and then filling it. + // Then we just compare the results. + test.AsSpan3D(1, 1, 1, 1, 2, 2).Fill(true); + + bool[,,]? expected = new[,,] + { + { + { false, false, false }, + { false, false, false }, + { false, false, false }, + }, + { + { false, false, false }, + { false, true, true }, + { false, true, true }, + }, + { + { false, false, false }, + { false, false, false }, + { false, false, false }, + } + }; + + CollectionAssert.AreEqual(expected, test); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_AsSpan3DAndFillArrayTwice() + { + bool[,,] test = new bool[3, 3, 3]; + + test.AsSpan3D(0, 0, 0, 1, 1, 2).Fill(true); + test.AsSpan3D(2, 1, 1, 1, 2, 2).Fill(true); + + bool[,,]? expected = new[,,] + { + { + { true, true, false }, + { false, false, false }, + { false, false, false }, + }, + { + { false, false, false }, + { false, false, false }, + { false, false, false }, + }, + { + { false, false, false }, + { false, true, true }, + { false, true, true }, + } + }; + + CollectionAssert.AreEqual(expected, test); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_AsSpan3DAndFillArrayBottomEdgeBoundary() + { + bool[,,] test = new bool[3, 4, 4]; + + test.AsSpan3D(1, 2, 1, 2, 2, 3).Fill(true); + + bool[,,]? expected = new[,,] + { + { + { false, false, false, false }, + { false, false, false, false }, + { false, false, false, false }, + { false, false, false, false }, + }, + { + { false, false, false, false }, + { false, false, false, false }, + { false, true, true, true }, + { false, true, true, true }, + }, + { + { false, false, false, false }, + { false, false, false, false }, + { false, true, true, true }, + { false, true, true, true }, + } + }; + + CollectionAssert.AreEqual(expected, test); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_AsSpan3DAndFillArrayBottomRightCornerBoundary() + { + bool[,,] test = new bool[2, 2, 2]; + + test.AsSpan3D(1, 1, 1, 1, 1, 1).Fill(true); + + bool[,,]? expected = new[,,] + { + { + { false, false }, + { false, false }, + }, + { + { false, false }, + { false, true }, + } + }; + + CollectionAssert.AreEqual(expected, test); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_GetRow_Rectangle() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Here we use the enumerator on the RefEnumerator type to traverse items in a row + // by reference. For each one, we check that the reference does in fact point to the + // item we expect in the underlying array (in this case, items on row 1). + int j = 0; + foreach (ref int value in array.GetRow(1, 0)) + { + Assert.IsTrue(Unsafe.AreSame(ref value, ref array[1, 0, j++])); + } + + // Check that RefEnumerable.ToArray() works correctly + CollectionAssert.AreEqual(array.GetRow(1, 0).ToArray(), new[] { 7, 8, 9 }); + + // Test an empty array + Assert.AreSame(new int[1, 1, 0].GetRow(0, 0).ToArray(), Array.Empty()); + + _ = Assert.ThrowsExactly(() => array.GetRow(-1, 0)); + _ = Assert.ThrowsExactly(() => array.GetRow(2, 0)); + _ = Assert.ThrowsExactly(() => array.GetRow(20, 0)); + + _ = Assert.ThrowsExactly(() => array.GetRow(0, -1)); + _ = Assert.ThrowsExactly(() => array.GetRow(0, 2)); + _ = Assert.ThrowsExactly(() => array.GetRow(0, 20)); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_GetColumn_Rectangle() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but this time we iterate a column instead (so non-contiguous items) + int i = 0; + foreach (ref int value in array.GetColumn(0, 1)) + { + Assert.IsTrue(Unsafe.AreSame(ref value, ref array[0, i++, 1])); + } + + CollectionAssert.AreEqual(array.GetColumn(0, 1).ToArray(), new[] { 2, 5 }); + + _ = Assert.ThrowsExactly(() => array.GetColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => array.GetColumn(2, 0)); + _ = Assert.ThrowsExactly(() => array.GetColumn(20, 0)); + + _ = Assert.ThrowsExactly(() => array.GetColumn(0, -1)); + _ = Assert.ThrowsExactly(() => array.GetColumn(0, 3)); + _ = Assert.ThrowsExactly(() => array.GetColumn(0, 20)); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_GetDepthColumn_Rectangle() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 } + }, + { + { 7, 8, 9 }, + { 10, 11, 12 } + } + }; + + // Same as above, but this time we iterate a depth column instead (so non-contiguous items) + int i = 0; + foreach (ref int value in array.GetDepthColumn(1, 2)) + { + Assert.IsTrue(Unsafe.AreSame(ref value, ref array[i++, 1, 2])); + } + + CollectionAssert.AreEqual(array.GetDepthColumn(1, 2).ToArray(), new[] { 6, 12 }); + + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(-1, 0)); + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(2, 0)); + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(20, 0)); + + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(0, -1)); + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(0, 3)); + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(0, 20)); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_GetRow_Empty() + { + int[,,] array = new int[0, 0, 0]; + + // Try to get a row from an empty array (the row index isn't in range) + _ = Assert.ThrowsExactly(() => array.GetRow(0, 0).ToArray()); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_GetColumn_Empty() + { + int[,,] array = new int[0, 0, 0]; + + // Try to get a column from an empty array (the row index isn't in range) + _ = Assert.ThrowsExactly(() => array.GetColumn(0, 0).ToArray()); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_GetDepthColumn_Empty() + { + int[,,] array = new int[0, 0, 0]; + + // Try to get a depth column from an empty array (the row index isn't in range) + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(0, 0).ToArray()); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_GetRowOrColumn_Helpers() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 } + }, + { + { 10, 11, 12 }, + { 13, 14, 15 }, + { 16, 17, 18 } + }, + { + { 19, 20, 21 }, + { 22, 23, 24 }, + { 25, 26, 27 } + } + }; + + // Get a row and test the Clear method. Note that the Span3D here is sliced + // starting from the second column, so this method should clear the row from index 1. + array.AsSpan3D(1, 1, 1, 2, 2, 2).GetRow(0, 0).Clear(); + + int[,,] expected = + { + { + { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 } + }, + { + { 10, 11, 12 }, + { 13, 0, 0 }, + { 16, 17, 18 } + }, + { + { 19, 20, 21 }, + { 22, 23, 24 }, + { 25, 26, 27 } + } + }; + + CollectionAssert.AreEqual(array, expected); + + // Same as before, but this time we fill a column with a value + array.GetColumn(2, 1).Fill(42); + + expected = new[,,] + { + { + { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 } + }, + { + { 10, 11, 12 }, + { 13, 0, 0 }, + { 16, 17, 18 } + }, + { + { 19, 42, 21 }, + { 22, 42, 24 }, + { 25, 42, 27 } + } + }; + + CollectionAssert.AreEqual(array, expected); + + int[] copy = new int[3]; + + // Get a row and copy items to a target span (in this case, wrapping an array) + array.GetRow(0, 2).CopyTo(copy); + + int[] result = { 7, 8, 9 }; + + CollectionAssert.AreEqual(copy, result); + + // Same as above, but copying from a depth column (so we test non-contiguous sequences too) + array.GetDepthColumn(1, 2).CopyTo(copy); + + result = new[] { 6, 0, 24 }; + + CollectionAssert.AreEqual(copy, result); + + // Some invalid attempts to copy to an empty span or sequence + _ = Assert.ThrowsExactly(() => array.GetRow(0, 0).CopyTo(default(RefEnumerable))); + _ = Assert.ThrowsExactly(() => array.GetRow(0, 0).CopyTo(default(Span))); + + _ = Assert.ThrowsExactly(() => array.GetColumn(0, 0).CopyTo(default(RefEnumerable))); + _ = Assert.ThrowsExactly(() => array.GetColumn(0, 0).CopyTo(default(Span))); + + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(0, 0).CopyTo(default(RefEnumerable))); + _ = Assert.ThrowsExactly(() => array.GetDepthColumn(0, 0).CopyTo(default(Span))); + + // Same as CopyTo, but this will fail gracefully with an invalid target + Assert.IsTrue(array.GetRow(0, 2).TryCopyTo(copy)); + Assert.IsFalse(array.GetRow(0, 0).TryCopyTo(default(Span))); + + result = new[] { 7, 8, 9 }; + + CollectionAssert.AreEqual(copy, result); + + // Also fill a row and then further down clear a column (trying out all possible combinations) + array.GetRow(0, 1).Fill(99); + + expected = new[,,] + { + { + { 1, 2, 3 }, + { 99, 99, 99 }, + { 7, 8, 9 } + }, + { + { 10, 11, 12 }, + { 13, 0, 0 }, + { 16, 17, 18 } + }, + { + { 19, 42, 21 }, + { 22, 42, 24 }, + { 25, 42, 27 } + } + }; + + CollectionAssert.AreEqual(array, expected); + + array.GetDepthColumn(2, 0).Clear(); + + expected = new[,,] + { + { + { 1, 2, 3 }, + { 99, 99, 99 }, + { 0, 8, 9 } + }, + { + { 10, 11, 12 }, + { 13, 0, 0 }, + { 0, 17, 18 } + }, + { + { 19, 42, 21 }, + { 22, 42, 24 }, + { 0, 42, 27 } + } + }; + + CollectionAssert.AreEqual(array, expected); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_ReadOnlyGetRowOrColumn_Helpers() + { + int[,,] array = + { + { + { 1, 2, 3 }, + { 4, 5, 6 }, + { 7, 8, 9 } + }, + { + { 10, 11, 12 }, + { 13, 14, 15 }, + { 16, 17, 18 } + }, + { + { 19, 20, 21 }, + { 22, 23, 24 }, + { 25, 26, 27 } + } + }; + + // This test pretty much does the same things as the method above, but this time + // using a source ReadOnlySpan3D, so that the sequence type being tested is + // ReadOnlyRefEnumerable instead (which shares most features but is separate). + ReadOnlySpan3D span3D = array; + + int[] copy = new int[3]; + + span3D.GetRow(1, 2).CopyTo(copy); + + int[] result = { 16, 17, 18 }; + + CollectionAssert.AreEqual(copy, result); + + span3D.GetColumn(0, 1).CopyTo(copy); + + result = new[] { 2, 5, 8 }; + + CollectionAssert.AreEqual(copy, result); + + span3D.GetDepthColumn(0, 2).CopyTo(copy); + + result = new[] { 3, 12, 21 }; + + CollectionAssert.AreEqual(copy, result); + + _ = Assert.ThrowsExactly(() => ((ReadOnlySpan3D)array).GetRow(0, 0).CopyTo(default(RefEnumerable))); + _ = Assert.ThrowsExactly(() => ((ReadOnlySpan3D)array).GetRow(0, 0).CopyTo(default(Span))); + + _ = Assert.ThrowsExactly(() => ((ReadOnlySpan3D)array).GetColumn(0, 0).CopyTo(default(RefEnumerable))); + _ = Assert.ThrowsExactly(() => ((ReadOnlySpan3D)array).GetColumn(0, 0).CopyTo(default(Span))); + + _ = Assert.ThrowsExactly(() => ((ReadOnlySpan3D)array).GetDepthColumn(0, 0).CopyTo(default(RefEnumerable))); + _ = Assert.ThrowsExactly(() => ((ReadOnlySpan3D)array).GetDepthColumn(0, 0).CopyTo(default(Span))); + + Assert.IsTrue(span3D.GetRow(2, 1).TryCopyTo(copy)); + Assert.IsFalse(span3D.GetRow(2, 1).TryCopyTo(default(Span))); + + result = new[] { 22, 23, 24 }; + + CollectionAssert.AreEqual(copy, result); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_RefEnumerable_Misc() + { + int[,,] array1 = + { + { + { 1, 2 }, + { 3, 4 } + }, + { + { 5, 6 }, + { 7, 8 } + } + }; + + int[,,] array2 = new int[2, 2, 2]; + + // Copy to enumerable with source step == 1, destination step == 1 + array1.GetRow(0, 0).CopyTo(array2.GetRow(0, 0)); + + // Copy enumerable with source step == 1, destination step != 1 + array1.GetRow(0, 1).CopyTo(array2.GetColumn(0, 1)); + + // Copy enumerable with source step != 1, destination step == 1 + array1.GetColumn(1, 0).CopyTo(array2.GetRow(1, 1)); + + // Copy enumerable with source step != 1, destination step != 1 + array1.GetDepthColumn(0, 1).CopyTo(array2.GetDepthColumn(1, 0)); + + int[,,] result = + { + { + { 1, 3 }, + { 2, 4 } + }, + { + { 0, 0 }, + { 6, 7 } + } + }; + + CollectionAssert.AreEqual(array2, result); + + // Test a valid and an invalid TryCopyTo call with the RefEnumerable overload + bool shouldBeTrue = array1.GetRow(0, 0).TryCopyTo(array2.GetColumn(1, 1)); + bool shouldBeFalse = array1.GetRow(0, 0).TryCopyTo(default(RefEnumerable)); + + result = new[,,] + { + { + { 1, 3 }, + { 2, 4 } + }, + { + { 0, 1 }, + { 6, 2 } + } + }; + + CollectionAssert.AreEqual(array2, result); + + Assert.IsTrue(shouldBeTrue); + Assert.IsFalse(shouldBeFalse); + } + +#if NET6_0_OR_GREATER + [TestMethod] + public void Test_ArrayExtensions_3D_AsSpan_Empty() + { + int[,,] array = new int[0, 0, 0]; + + Span span = array.AsSpan(); + + // Check that the empty array was loaded properly + Assert.HasCount(span.Length, array); + Assert.IsTrue(span.IsEmpty); + } + + [TestMethod] + public void Test_ArrayExtensions_3D_AsSpan_Populated() + { + int[,,] array = + { + { + { 1, 2 }, + { 3, 4 } + }, + { + { 5, 6 }, + { 7, 8 } + } + }; + + Span span = array.AsSpan(); + + // Test the total length of the span + Assert.HasCount(span.Length, array); + + ref int r0 = ref array[0, 0, 0]; + ref int r1 = ref span[0]; + + // Similarly to the top methods, here we compare a given reference to + // ensure they point to the right element back in the original array. + Assert.IsTrue(Unsafe.AreSame(ref r0, ref r1)); + } +#endif +} \ No newline at end of file