Browse Source

Replaced VertexList<T> collection with ValueListSet<T> which is expected to improve performance and memory footprint

Vicente Penades 5 years ago
parent
commit
8dd2fb9c3a

+ 410 - 0
src/SharpGLTF.Toolkit/Collections/ValueListSet.cs

@@ -0,0 +1,410 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace SharpGLTF.Collections
+{
+    /// <summary>
+    /// Represents A specialised list that requires all elements to be unique.
+    /// </summary>
+    /// <remarks>
+    /// - This collection is based on <see cref="Dictionary{TKey, TValue}"/>
+    /// - Replaces <see cref="VertexList{T}"/>
+    /// - Designed to work with lists of vertices.
+    ///
+    /// This collection is:
+    /// - like a HashSet, in the sense that every element must be unique.
+    /// - like a list, because elements can be accessed by index: <see cref="this[int]"/>.
+    /// - <see cref="IndexOf(in T)"/> and <see cref="Use(in T)"/> are fast as in a HashSet.
+    /// </remarks>
+    /// <typeparam name="T">Any value type.</typeparam>
+    class ValueListSet<T> : IReadOnlyList<T>
+        where T : struct
+    {
+        #region constructors
+
+        public ValueListSet()
+            : this(0, null) { }
+
+        public ValueListSet(int capacity, IEqualityComparer<T> comparer = null)
+        {
+            if (capacity < 0) throw new ArgumentOutOfRangeException(nameof(capacity));
+            if (capacity > 0) _Initialize(capacity);
+            _Comparer = comparer ?? EqualityComparer<T>.Default;
+        }
+
+        #endregion
+
+        #region data
+
+        [DebuggerDisplay("Hash:{HashCode} Next:{Next} Value:{Value}")]
+        private struct _Entry
+        {
+            public int HashCode;    // Lower 31 bits of hash code, -1 if unused
+            public int Next;        // Index of next entry, -1 if last
+            public T Value;         // Value of entry
+        }
+
+        private IEqualityComparer<T> _Comparer;
+
+        private _Entry[] _Entries;
+        private int[] _Buckets;     // indices to the last entry with the given hash.
+        private int _Count;         // actual number of elements of the collection.
+
+        private int _Version;
+
+        #endregion
+
+        #region properties
+
+        public IEqualityComparer<T> Comparer => _Comparer;
+
+        public int Count => _Count;
+
+        public T this[int index]
+        {
+            get
+            {
+                if (index < 0 || index >= _Count) throw new ArgumentOutOfRangeException(nameof(index));
+                if (_Entries[index].HashCode == -1) throw new ArgumentException(nameof(index));
+                return _Entries[index].Value;
+            }
+        }
+
+        public IEnumerable<int> Indices => new _IndexCollection(this);
+
+        #endregion
+
+        #region API
+
+        public void Clear()
+        {
+            if (_Count <= 0) return;
+
+            _Entries.AsSpan().Fill(default);
+            _Buckets.AsSpan().Fill(-1);
+
+            _Count = 0;
+
+            _Version++;
+        }
+
+        public bool Exists(int index)
+        {
+            if (index < 0 || index >= _Count) return false;
+            if (_Entries[index].HashCode == -1) return false;
+            return true;
+        }
+
+        public int IndexOf(in T value)
+        {
+            return _Buckets == null ? -1 : _IndexOf(value);
+        }
+
+        public int Use(in T value)
+        {
+            var idx = _Buckets == null ? -1 : _IndexOf(value);
+            if (idx >= 0) return idx;
+            return _Insert(value);
+        }
+
+        public int Add(in T value)
+        {
+            if (_IndexOf(value) >= 0) throw new ArgumentException("${value} already exists", nameof(value));
+            return _Insert(value);
+        }
+
+        public bool Contains(in T item) { return IndexOf(item) >= 0; }
+
+        public void CopyTo(T[] array, int arrayIndex)
+        {
+            for (int i = 0; i < _Count; ++i)
+            {
+                var entry = _Entries[i];
+                if (entry.HashCode != -1) array[arrayIndex++] = entry.Value;
+            }
+        }
+
+        public void CopyTo(ValueListSet<T> dst)
+        {
+            if (_Count == 0) { dst.Clear(); return; }
+
+            if (dst._Buckets == null || dst._Buckets.Length < this._Buckets.Length) dst._Buckets = new int[this._Buckets.Length];
+            if (dst._Entries == null || dst._Entries.Length < this._Entries.Length) dst._Entries = new _Entry[this._Entries.Length];
+
+            dst._Count = this._Count;
+
+            this._Entries.AsSpan(0, _Count).CopyTo(dst._Entries);
+
+            if (this._Comparer == dst._Comparer)
+            {
+                this._Buckets.AsSpan(0).CopyTo(dst._Buckets);
+            }
+            else
+            {
+                // if comparer is different, we must rebuild hashes and buckets.
+                dst._Resize(dst._Count, true);
+            }
+
+            dst._Version++;
+        }
+
+        public IEnumerator<T> GetEnumerator() { return new _ValueEnumerator(this); }
+
+        IEnumerator IEnumerable.GetEnumerator() { return new _ValueEnumerator(this); }
+
+        public void ApplyTransform(Func<T, T> transformFunc)
+        {
+            for (int i = 0; i < _Count; ++i)
+            {
+                _Entries[i].Value = transformFunc(_Entries[i].Value);
+            }
+
+            // reconstruct hashes.
+            _Resize(_Count, true);
+        }
+
+        #endregion
+
+        #region core
+
+        private void _Initialize(int capacity)
+        {
+            int size = _PrimeNumberHelpers.GetPrime(capacity);
+
+            _Buckets = new int[size];
+            _Buckets.AsSpan().Fill(-1);
+            _Entries = new _Entry[size];
+            _Count = 0;
+        }
+
+        private int _IndexOf(in T value)
+        {
+            System.Diagnostics.Debug.Assert(_Buckets != null);
+
+            int hashCode = _Comparer.GetHashCode(value) & 0x7FFFFFFF;
+            int bucket = hashCode % _Buckets.Length;
+
+            for (int i = _Buckets[bucket]; i >= 0; i = _Entries[i].Next)
+            {
+                if (_Entries[i].HashCode == hashCode && _Comparer.Equals(_Entries[i].Value, value)) return i;
+            }
+
+            return -1;
+        }
+
+        private int _Insert(in T value)
+        {
+            if (_Buckets == null) _Initialize(0);
+            if (_Count == _Entries.Length) _Grow();
+
+            int hashCode = _Comparer.GetHashCode(value) & 0x7FFFFFFF;
+            int targetBucket = hashCode % _Buckets.Length;
+
+            int index = _Count;
+            _Count++;
+
+            _Entries[index].HashCode = hashCode;
+            _Entries[index].Next = _Buckets[targetBucket];
+            _Entries[index].Value = value;
+            _Buckets[targetBucket] = index;
+            _Version++;
+
+            System.Diagnostics.Debug.Assert(_Entries[index].Next < index);
+
+            return index;
+        }
+
+        private void _Grow()
+        {
+            int newCount = _PrimeNumberHelpers.ExpandPrime(_Count);
+            System.Diagnostics.Debug.Assert(newCount > _Count);
+            _Resize(newCount, false);
+        }
+
+        private void _Resize(int newSize, bool forceNewHashCodes)
+        {
+            if (newSize < _Entries.Length) newSize = _Entries.Length;
+
+            Array.Resize(ref _Entries, newSize);
+
+            if (forceNewHashCodes)
+            {
+                for (int index = 0; index < _Count; index++)
+                {
+                    if (_Entries[index].HashCode == -1) continue;
+
+                    _Entries[index].HashCode = _Comparer.GetHashCode(_Entries[index].Value) & 0x7FFFFFFF;
+                }
+            }
+
+            // reconstruct buckets & linked chain
+
+            if (_Buckets.Length != _Entries.Length) _Buckets = new int[_Entries.Length];
+            _Buckets.AsSpan().Fill(-1);
+
+            for (int index = 0; index < _Count; index++)
+            {
+                if (_Entries[index].HashCode < 0) continue;
+
+                int bucket = _Entries[index].HashCode % _Buckets.Length;
+                _Entries[index].Next = _Buckets[bucket];
+                _Buckets[bucket] = index;
+
+                System.Diagnostics.Debug.Assert(_Entries[index].Next < index);
+            }
+        }
+
+        #endregion
+
+        #region nested types
+
+        struct _ValueEnumerator : IEnumerator<T>
+        {
+            #region lifecycle
+
+            internal _ValueEnumerator(ValueListSet<T> source)
+            {
+                _Source = source;
+                _Version = source._Version;
+                _Index = 0;
+                _Current = default;
+            }
+
+            public void Dispose() { }
+
+            #endregion
+
+            #region data
+
+            private readonly ValueListSet<T> _Source;
+            private readonly int _Version;
+            private int _Index;
+            private T _Current;
+
+            #endregion
+
+            #region properties
+
+            public T Current => _Current;
+            object IEnumerator.Current => _Current;
+
+            #endregion
+
+            #region API
+
+            public bool MoveNext()
+            {
+                if (_Version != _Source._Version) throw new InvalidOperationException("collection changed");
+
+                // Use unsigned comparison since we set index to source.count+1 when the enumeration ends.
+                // dictionary.count+1 could be negative if dictionary.count is Int32.MaxValue
+                while ((uint)_Index < (uint)_Source._Count)
+                {
+                    if (_Source._Entries[_Index].HashCode >= 0)
+                    {
+                        _Current = _Source._Entries[_Index].Value;
+                        _Index++;
+                        return true;
+                    }
+
+                    _Index++;
+                }
+
+                _Index = _Source._Count + 1;
+                _Current = default;
+                return false;
+            }
+
+            void IEnumerator.Reset()
+            {
+                if (_Version != _Source._Version) throw new InvalidOperationException("collection changed");
+
+                _Index = 0;
+                _Current = default;
+            }
+
+            #endregion
+        }
+
+        struct _IndexCollection : IEnumerable<int>
+        {
+            public _IndexCollection(ValueListSet<T> source) { _Source = source; }
+
+            private readonly ValueListSet<T> _Source;
+            public IEnumerator<int> GetEnumerator() { return new _IndexEnumerator(_Source); }
+            IEnumerator IEnumerable.GetEnumerator() { return new _IndexEnumerator(_Source); }
+        }
+
+        struct _IndexEnumerator : IEnumerator<int>
+        {
+            #region lifecycle
+
+            internal _IndexEnumerator(ValueListSet<T> source)
+            {
+                _Source = source;
+                _Version = source._Version;
+                _Index = 0;
+                _Current = -1;
+            }
+
+            public void Dispose() { }
+
+            #endregion
+
+            #region data
+
+            private readonly ValueListSet<T> _Source;
+            private readonly int _Version;
+            private int _Index;
+            private int _Current;
+
+            #endregion
+
+            #region properties
+
+            public int Current => _Current;
+            object IEnumerator.Current => _Current;
+
+            #endregion
+
+            #region API
+
+            public bool MoveNext()
+            {
+                if (_Version != _Source._Version) throw new InvalidOperationException("collection changed");
+
+                // Use unsigned comparison since we set index to source.count+1 when the enumeration ends.
+                // dictionary.count+1 could be negative if dictionary.count is Int32.MaxValue
+                while ((uint)_Index < (uint)_Source._Count)
+                {
+                    if (_Source._Entries[_Index].HashCode >= 0)
+                    {
+                        _Current = _Index;
+                        _Index++;
+                        return true;
+                    }
+
+                    _Index++;
+                }
+
+                _Index = _Source._Count + 1;
+                _Current = default;
+                return false;
+            }
+
+            void IEnumerator.Reset()
+            {
+                if (_Version != _Source._Version) throw new InvalidOperationException("collection changed");
+
+                _Index = 0;
+                _Current = default;
+            }
+
+            #endregion
+        }
+
+        #endregion
+    }
+}

+ 1 - 1
src/SharpGLTF.Toolkit/Collections/VertexList.cs

@@ -78,7 +78,7 @@ namespace SharpGLTF.Collections
             return idx;
             return idx;
         }
         }
 
 
-        public void TransformVertices(Func<T, T> transformFunc)
+        public void ApplyTransform(Func<T, T> transformFunc)
         {
         {
             // although our "apparent" dictionary keys and values remain the same
             // although our "apparent" dictionary keys and values remain the same
             // we must reconstruct the VertexCache to regenerate the hashes.
             // we must reconstruct the VertexCache to regenerate the hashes.

+ 91 - 0
src/SharpGLTF.Toolkit/Collections/_PrimeNumberHelpers.cs

@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Collections
+{
+    /// <summary>
+    /// Lifted from <see href="https://referencesource.microsoft.com/#System.ServiceModel.Internals/System/Runtime/HashHelper.cs"/>
+    /// </summary>
+    internal static class _PrimeNumberHelpers
+    {
+        // This is the maximum prime smaller than Array.MaxArrayLength
+        private const Int32 _MaxPrimeArrayLength = 0x7FEFFFFD;
+
+        private const Int32 _HashPrime = 101;
+
+        // Table of prime numbers to use as hash table sizes.
+        // A typical resize algorithm would pick the smallest prime number in this array
+        // that is larger than twice the previous capacity.
+        // Suppose our Hashtable currently has capacity x and enough elements are added
+        // such that a resize needs to occur. Resizing first computes 2x then finds the
+        // first prime in the table greater than 2x, i.e. if primes are ordered
+        // p_1, p_2, ..., p_i, ..., it finds p_n such that p_n-1 < 2x < p_n.
+        // Doubling is important for preserving the asymptotic complexity of the
+        // hashtable operations such as add.  Having a prime guarantees that double
+        // hashing does not lead to infinite loops.  IE, your hash function will be
+        // h1(key) + i*h2(key), 0 <= i < size.  h2 and the size must be relatively prime.
+        private static readonly int[] _Primes = {
+            3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
+            1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
+            17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
+            187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
+            1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 };
+
+        public static bool IsPrime(int candidate)
+        {
+            if ((candidate & 1) != 0)
+            {
+                int limit = (int)Math.Sqrt(candidate);
+                for (int divisor = 3; divisor <= limit; divisor += 2)
+                {
+                    if ((candidate % divisor) == 0)
+                        return false;
+                }
+
+                return true;
+            }
+
+            return candidate == 2;
+        }
+
+        public static int GetPrime(int min)
+        {
+            if (min < 0) throw new ArgumentOutOfRangeException(nameof(min));
+
+            for (int i = 0; i < _Primes.Length; i++)
+            {
+                int prime = _Primes[i];
+                if (prime >= min) return prime;
+            }
+
+            // outside of our predefined table.
+            // compute the hard way.
+            for (int i = min | 1; i < Int32.MaxValue; i += 2)
+            {
+                if (IsPrime(i) && ((i - 1) % _HashPrime != 0))
+                    return i;
+            }
+
+            return min;
+        }
+
+        public static int GetMinPrime() { return _Primes[0]; }
+
+        // Returns size of hashtable to grow to.
+        public static int ExpandPrime(int oldSize)
+        {
+            int newSize = 2 * oldSize;
+
+            // Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow.
+            // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
+            if ((uint)newSize > _MaxPrimeArrayLength && oldSize < _MaxPrimeArrayLength)
+            {
+                System.Diagnostics.Debug.Assert(GetPrime(_MaxPrimeArrayLength) == _MaxPrimeArrayLength, "Invalid MaxPrimeArrayLength");
+                return _MaxPrimeArrayLength;
+            }
+
+            return GetPrime(newSize);
+        }
+    }
+}

+ 2 - 2
src/SharpGLTF.Toolkit/Geometry/PrimitiveBuilder.cs

@@ -328,7 +328,7 @@ namespace SharpGLTF.Geometry
         {
         {
             Guard.NotNull(vertexTransformFunc, nameof(vertexTransformFunc));
             Guard.NotNull(vertexTransformFunc, nameof(vertexTransformFunc));
 
 
-            _Vertices.TransformVertices(vertexTransformFunc);
+            _Vertices.ApplyTransform(vertexTransformFunc);
 
 
             TvG geoFunc(TvG g) => vertexTransformFunc(new VertexBuilder<TvG, TvM, TvS>(g, default, default(TvS))).Geometry;
             TvG geoFunc(TvG g) => vertexTransformFunc(new VertexBuilder<TvG, TvM, TvS>(g, default, default(TvS))).Geometry;
 
 
@@ -391,7 +391,7 @@ namespace SharpGLTF.Geometry
 
 
         #region helper types
         #region helper types
 
 
-        private sealed class VertexListWrapper : VertexList<VertexBuilder<TvG, TvM, TvS>>, IReadOnlyList<IVertexBuilder>
+        private sealed class VertexListWrapper : ValueListSet<VertexBuilder<TvG, TvM, TvS>>, IReadOnlyList<IVertexBuilder>
         {
         {
             #pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists
             #pragma warning disable SA1100 // Do not prefix calls with base unless local implementation exists
             IVertexBuilder IReadOnlyList<IVertexBuilder>.this[int index] => base[index];
             IVertexBuilder IReadOnlyList<IVertexBuilder>.this[int index] => base[index];

+ 27 - 0
src/SharpGLTF.Toolkit/Geometry/Primitives.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SharpGLTF.Geometry
+{
+    public readonly partial struct PointPrimitive<TVertex, Tmaterial>
+    {
+        public readonly TVertex A;
+        public readonly Tmaterial Material;
+    }
+
+    public readonly partial struct LinePrimitive<TVertex, Tmaterial>
+    {
+        public readonly TVertex A;
+        public readonly TVertex B;
+        public readonly Tmaterial Material;
+    }
+
+    public readonly partial struct TrianglePrimitive<TVertex, Tmaterial>
+    {
+        public readonly TVertex A;
+        public readonly TVertex B;
+        public readonly TVertex C;
+        public readonly Tmaterial Material;
+    }
+}

+ 38 - 1
tests/SharpGLTF.Tests/Collections/VertexListTests.cs

@@ -1,9 +1,12 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Text;
 
 
 using NUnit.Framework;
 using NUnit.Framework;
 
 
+using XYZ = System.Numerics.Vector3;
+
 namespace SharpGLTF.Collections
 namespace SharpGLTF.Collections
 {
 {
     [TestFixture]
     [TestFixture]
@@ -65,7 +68,41 @@ namespace SharpGLTF.Collections
 
 
         }
         }
 
 
-        
+        [Test]
+        public void TestValueListSet()
+        {
+            var a = new XYZ(1.1f);
+            var b = new XYZ(1.2f);
+            var c = new XYZ(1.3f);
+            var d = new XYZ(1.4f);
+
+            var vlist = new ValueListSet<XYZ>();
+
+            var idx0 = vlist.Use(a); Assert.AreEqual(0, idx0);
+            var idx1 = vlist.Use(b); Assert.AreEqual(1, idx1);
+            var idx2 = vlist.Use(a); Assert.AreEqual(0, idx2);
+
+            Assert.AreEqual(a, vlist[idx0]);
+            Assert.AreEqual(b, vlist[idx1]);
+            Assert.AreEqual(a, vlist[idx2]);
+
+            CollectionAssert.AreEqual(new[] { a, b }, vlist.ToArray());
+
+            vlist.Use(c);
+            vlist.Use(d);
+            CollectionAssert.AreEqual(new[] { a, b, c, d }, vlist.ToArray());
+
+            var vlist2 = new ValueListSet<XYZ>();
+            vlist.CopyTo(vlist2);
+
+            Assert.AreEqual(vlist[0], vlist2[0]);
+            Assert.AreEqual(vlist[1], vlist2[1]);
+            Assert.AreEqual(vlist[2], vlist2[2]);
+            Assert.AreEqual(vlist[3], vlist2[3]);
+
+        }
+
+
 
 
     }
     }
 }
 }