Skip to content

Commit

Permalink
feat: native queue list
Browse files Browse the repository at this point in the history
Introduce (internal) a custom `NativeQueueList<T>` implementation based on `NativeList<T>`, replacing the `com.unity.collections` `NativeQueue<T>` due to persistent bugs in Unity's implementation. This custom implementation offers significantly faster allocation and manipulation performance, though it is more memory-intensive. It will be incorporated in future commits to replace existing uses of `NativeQueue` throughout the codebase.
  • Loading branch information
andywiecko committed Dec 18, 2024
1 parent 5ab188f commit c050302
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 0 deletions.
50 changes: 50 additions & 0 deletions Runtime/Triangulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,56 @@ public static void DynamicInsertPoint(this UnsafeTriangulator<fp2> @this, Output
#endif
}

/// <summary>
/// Custom queue implementation which is a wrapper for <see cref="NativeList{T}"/>.
/// This implementation is memory <b>extensive</b>.
/// </summary>
internal struct NativeQueueList<T> : IDisposable where T : unmanaged
{
public readonly bool IsCreated => impl.IsCreated;
public readonly int Count => math.max(impl.Length - indexRef.Value, 0);

private NativeList<T> impl;
private NativeReference<int> indexRef;

public NativeQueueList(int capacity, Allocator allocator)
{
impl = new(capacity, allocator);
indexRef = new(0, allocator);
}

public NativeQueueList(Allocator allocator) : this(1, allocator) { }

public ReadOnlySpan<T> AsReadOnlySpan() => impl.AsReadOnly().AsReadOnlySpan()[indexRef.Value..];
public Span<T> AsSpan() => impl.AsArray().AsSpan()[indexRef.Value..];

public void Clear()
{
impl.Clear();
indexRef.Value = 0;
}

public void Dispose()
{
impl.Dispose();
indexRef.Dispose();
}

public void Enqueue(T item) => impl.Add(item);
public T Dequeue() => impl[indexRef.Value++];
public readonly bool IsEmpty() => Count == 0;
public bool TryDequeue(out T item)
{
var isEmpty = IsEmpty();
if (isEmpty)
{
Clear();
}
item = isEmpty ? default : Dequeue();
return !isEmpty;
}
}

[BurstCompile]
internal struct TriangulationJob<T, T2, TBig, TTransform, TUtils> : IJob
where T : unmanaged, IComparable<T>
Expand Down
134 changes: 134 additions & 0 deletions Tests/InternalUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using andywiecko.BurstTriangulator.LowLevel.Unsafe;
using NUnit.Framework;
using System;
using System.Linq;
using Unity.Collections;
using Unity.Mathematics;

namespace andywiecko.BurstTriangulator.Editor.Tests
Expand Down Expand Up @@ -194,4 +196,136 @@ public void Area2Test(double2 a, double2 b, double2 c, double expected)
[Test, TestCaseSource(nameof(pointInsideTriangleTestData))]
public bool PointInsideTriangleTest(double2 p, double2 a, double2 b, double2 c) => default(UtilsDouble).PointInsideTriangle(p, a, b, c);
}

public class NativeListQueueTests
{
[Test]
public void IsCreatedTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
Assert.That(queue.IsCreated, Is.True);
}

[Test]
public void CountTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);

var count0 = queue.Count;
queue.Enqueue(default);
queue.Enqueue(default);
queue.Enqueue(default);
var count3 = queue.Count;
queue.Dequeue();
queue.Dequeue();
var count1 = queue.Count;

Assert.That(count0, Is.EqualTo(0));
Assert.That(count3, Is.EqualTo(3));
Assert.That(count1, Is.EqualTo(1));
}

[Test]
public void AsReadOnlySpanTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Enqueue(0);
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
queue.Dequeue();

var span = queue.AsReadOnlySpan();
Assert.That(span.ToArray(), Is.EqualTo(new[] { 1, 2, 3 }));
}

[Test]
public void AsSpanTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Enqueue(0);
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
queue.Dequeue();

var span = queue.AsSpan();
Assert.That(span.ToArray(), Is.EqualTo(new[] { 1, 2, 3 }));
}

[Test]
public void ClearTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Enqueue(0);
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);

queue.Clear();

Assert.That(queue.Count, Is.EqualTo(0));
Assert.That(queue.AsReadOnlySpan().ToArray(), Is.Empty);
}

[Test]
public void DisposeTest()
{
var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Dispose();
Assert.That(queue.IsCreated, Is.False);
}

[Test]
public void EnqueueTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Enqueue(42);
Assert.That(queue.AsReadOnlySpan()[0], Is.EqualTo(42));
}

[Test]
public void DequeueTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Enqueue(1);
queue.Enqueue(2);
var el = queue.Dequeue();
Assert.That(el, Is.EqualTo(1));
}

[Test]
public void IsEmptyTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Enqueue(1);
queue.Dequeue();
Assert.That(queue.IsEmpty(), Is.True);
}

[Test]
public void TryDequeueTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
queue.Enqueue(0);
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);

using var tmp = new NativeList<int>(Allocator.Persistent);
while (queue.TryDequeue(out var el))
{
tmp.Add(el);
}

Assert.That(tmp.AsReadOnly(), Is.EqualTo(new[] { 0, 1, 2, 3 }));
}

[Test]
public void ThrowDequeueTest()
{
using var queue = new NativeQueueList<int>(Allocator.Persistent);
Assert.Throws<IndexOutOfRangeException>(() => queue.Dequeue());
}
}
}

0 comments on commit c050302

Please sign in to comment.