Browse Source

Merge pull request #3315 from MarchingCube/alloc-scene-hittest

Rework deferred renderer scene hit testing
try-fix-3336
Steven Kirk 6 years ago
committed by GitHub
parent
commit
93b94a52bd
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 40
      src/Avalonia.Base/Collections/Pooled/ClearMode.cs
  2. 31
      src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs
  3. 21
      src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs
  4. 1531
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  5. 699
      src/Avalonia.Base/Collections/Pooled/PooledStack.cs
  6. 28
      src/Avalonia.Base/Collections/Pooled/StackDebugView.cs
  7. 691
      src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs
  8. 4
      src/Avalonia.Input/InputExtensions.cs
  9. 26
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  10. 12
      src/Avalonia.Visuals/Rendering/IRenderer.cs
  11. 5
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  12. 174
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  13. 7
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  14. 29
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  15. 4
      src/Windows/Avalonia.Win32/WindowImpl.cs
  16. 2
      tests/Avalonia.Benchmarks/NullRenderer.cs
  17. 3
      tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
  18. 2
      tests/Avalonia.LeakTests/ControlTests.cs

40
src/Avalonia.Base/Collections/Pooled/ClearMode.cs

@ -0,0 +1,40 @@
// This source file is adapted from the Collections.Pooled.
// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/)
namespace Avalonia.Collections.Pooled
{
/// <summary>
/// This enum allows control over how data is treated when internal
/// arrays are returned to the ArrayPool. Be careful to understand
/// what each option does before using anything other than the default
/// of Auto.
/// </summary>
public enum ClearMode
{
/// <summary>
/// <para><code>Auto</code> has different behavior depending on the host project's target framework.</para>
/// <para>.NET Core 2.1: Reference types and value types that contain reference types are cleared
/// when the internal arrays are returned to the pool. Value types that do not contain reference
/// types are not cleared when returned to the pool.</para>
/// <para>.NET Standard 2.0: All user types are cleared before returning to the pool, in case they
/// contain reference types.
/// For .NET Standard, Auto and Always have the same behavior.</para>
/// </summary>
Auto = 0,
/// <summary>
/// The <para><code>Always</code> setting has the effect of always clearing user types before returning to the pool.
/// This is the default behavior on .NET Standard.</para><para>You might want to turn this on in a .NET Core project
/// if you were concerned about sensitive data stored in value types leaking to other pars of your application.</para>
/// </summary>
Always = 1,
/// <summary>
/// <para><code>Never</code> will cause pooled collections to never clear user types before returning them to the pool.</para>
/// <para>You might want to use this setting in a .NET Standard project when you know that a particular collection stores
/// only value types and you want the performance benefit of not taking time to reset array items to their default value.</para>
/// <para>Be careful with this setting: if used for a collection that contains reference types, or value types that contain
/// reference types, this setting could cause memory issues by making the garbage collector unable to clean up instances
/// that are still being referenced by arrays sitting in the ArrayPool.</para>
/// </summary>
Never = 2
}
}

31
src/Avalonia.Base/Collections/Pooled/ICollectionDebugView.cs

@ -0,0 +1,31 @@
// 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.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Collections.Pooled
{
internal sealed class ICollectionDebugView<T>
{
private readonly ICollection<T> _collection;
public ICollectionDebugView(ICollection<T> collection)
{
_collection = collection ?? throw new ArgumentNullException(nameof(collection));
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public T[] Items
{
get
{
T[] items = new T[_collection.Count];
_collection.CopyTo(items, 0);
return items;
}
}
}
}

21
src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs

@ -0,0 +1,21 @@
// This source file is adapted from the Collections.Pooled.
// (https://github.com/jtmueller/Collections.Pooled/tree/master/Collections.Pooled/)
using System;
using System.Collections.Generic;
namespace Avalonia.Collections.Pooled
{
/// <summary>
/// Represents a read-only collection of pooled elements that can be accessed by index
/// </summary>
/// <typeparam name="T">The type of elements in the read-only pooled list.</typeparam>
public interface IReadOnlyPooledList<T> : IReadOnlyList<T>
{
/// <summary>
/// Gets a <see cref="System.ReadOnlySpan{T}"/> for the items currently in the collection.
/// </summary>
ReadOnlySpan<T> Span { get; }
}
}

1531
src/Avalonia.Base/Collections/Pooled/PooledList.cs

File diff suppressed because it is too large

699
src/Avalonia.Base/Collections/Pooled/PooledStack.cs

@ -0,0 +1,699 @@
// 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.
/*=============================================================================
**
**
** Purpose: An array implementation of a generic stack.
**
**
=============================================================================*/
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Threading;
namespace Avalonia.Collections.Pooled
{
/// <summary>
/// A simple stack of objects. Internally it is implemented as an array,
/// so Push can be O(n). Pop is O(1).
/// </summary>
[DebuggerTypeProxy(typeof(StackDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class PooledStack<T> : IEnumerable<T>, ICollection, IReadOnlyCollection<T>, IDisposable, IDeserializationCallback
{
[NonSerialized]
private ArrayPool<T> _pool;
[NonSerialized]
private object _syncRoot;
private T[] _array; // Storage for stack elements. Do not rename (binary serialization)
private int _size; // Number of items in the stack. Do not rename (binary serialization)
private int _version; // Used to keep enumerator in sync w/ collection. Do not rename (binary serialization)
private readonly bool _clearOnFree;
private const int DefaultCapacity = 4;
#region Constructors
/// <summary>
/// Create a stack with the default initial capacity.
/// </summary>
public PooledStack() : this(ClearMode.Auto, ArrayPool<T>.Shared) { }
/// <summary>
/// Create a stack with the default initial capacity.
/// </summary>
public PooledStack(ClearMode clearMode) : this(clearMode, ArrayPool<T>.Shared) { }
/// <summary>
/// Create a stack with the default initial capacity.
/// </summary>
public PooledStack(ArrayPool<T> customPool) : this(ClearMode.Auto, customPool) { }
/// <summary>
/// Create a stack with the default initial capacity and a custom ArrayPool.
/// </summary>
public PooledStack(ClearMode clearMode, ArrayPool<T> customPool)
{
_pool = customPool ?? ArrayPool<T>.Shared;
_array = Array.Empty<T>();
_clearOnFree = ShouldClear(clearMode);
}
/// <summary>
/// Create a stack with a specific initial capacity. The initial capacity
/// must be a non-negative number.
/// </summary>
public PooledStack(int capacity) : this(capacity, ClearMode.Auto, ArrayPool<T>.Shared) { }
/// <summary>
/// Create a stack with a specific initial capacity. The initial capacity
/// must be a non-negative number.
/// </summary>
public PooledStack(int capacity, ClearMode clearMode) : this(capacity, clearMode, ArrayPool<T>.Shared) { }
/// <summary>
/// Create a stack with a specific initial capacity. The initial capacity
/// must be a non-negative number.
/// </summary>
public PooledStack(int capacity, ArrayPool<T> customPool) : this(capacity, ClearMode.Auto, customPool) { }
/// <summary>
/// Create a stack with a specific initial capacity. The initial capacity
/// must be a non-negative number.
/// </summary>
public PooledStack(int capacity, ClearMode clearMode, ArrayPool<T> customPool)
{
if (capacity < 0)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
_pool = customPool ?? ArrayPool<T>.Shared;
_array = _pool.Rent(capacity);
_clearOnFree = ShouldClear(clearMode);
}
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(IEnumerable<T> enumerable) : this(enumerable, ClearMode.Auto, ArrayPool<T>.Shared) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(IEnumerable<T> enumerable, ClearMode clearMode) : this(enumerable, clearMode, ArrayPool<T>.Shared) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(IEnumerable<T> enumerable, ArrayPool<T> customPool) : this(enumerable, ClearMode.Auto, customPool) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(IEnumerable<T> enumerable, ClearMode clearMode, ArrayPool<T> customPool)
{
_pool = customPool ?? ArrayPool<T>.Shared;
_clearOnFree = ShouldClear(clearMode);
switch (enumerable)
{
case null:
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.enumerable);
break;
case ICollection<T> collection:
if (collection.Count == 0)
{
_array = Array.Empty<T>();
}
else
{
_array = _pool.Rent(collection.Count);
collection.CopyTo(_array, 0);
_size = collection.Count;
}
break;
default:
using (var list = new PooledList<T>(enumerable))
{
_array = _pool.Rent(list.Count);
list.Span.CopyTo(_array);
_size = list.Count;
}
break;
}
}
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(T[] array) : this(array.AsSpan(), ClearMode.Auto, ArrayPool<T>.Shared) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(T[] array, ClearMode clearMode) : this(array.AsSpan(), clearMode, ArrayPool<T>.Shared) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(T[] array, ArrayPool<T> customPool) : this(array.AsSpan(), ClearMode.Auto, customPool) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(T[] array, ClearMode clearMode, ArrayPool<T> customPool) : this(array.AsSpan(), clearMode, customPool) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(ReadOnlySpan<T> span) : this(span, ClearMode.Auto, ArrayPool<T>.Shared) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(ReadOnlySpan<T> span, ClearMode clearMode) : this(span, clearMode, ArrayPool<T>.Shared) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(ReadOnlySpan<T> span, ArrayPool<T> customPool) : this(span, ClearMode.Auto, customPool) { }
/// <summary>
/// Fills a Stack with the contents of a particular collection. The items are
/// pushed onto the stack in the same order they are read by the enumerator.
/// </summary>
public PooledStack(ReadOnlySpan<T> span, ClearMode clearMode, ArrayPool<T> customPool)
{
_pool = customPool ?? ArrayPool<T>.Shared;
_clearOnFree = ShouldClear(clearMode);
_array = _pool.Rent(span.Length);
span.CopyTo(_array);
_size = span.Length;
}
#endregion
/// <summary>
/// The number of items in the stack.
/// </summary>
public int Count => _size;
/// <summary>
/// Returns the ClearMode behavior for the collection, denoting whether values are
/// cleared from internal arrays before returning them to the pool.
/// </summary>
public ClearMode ClearMode => _clearOnFree ? ClearMode.Always : ClearMode.Never;
bool ICollection.IsSynchronized => false;
object ICollection.SyncRoot
{
get
{
if (_syncRoot == null)
{
Interlocked.CompareExchange<object>(ref _syncRoot, new object(), null);
}
return _syncRoot;
}
}
/// <summary>
/// Removes all Objects from the Stack.
/// </summary>
public void Clear()
{
if (_clearOnFree)
{
Array.Clear(_array, 0, _size); // clear the elements so that the gc can reclaim the references.
}
_size = 0;
_version++;
}
/// <summary>
/// Compares items using the default equality comparer
/// </summary>
public bool Contains(T item)
{
// PERF: Internally Array.LastIndexOf calls
// EqualityComparer<T>.Default.LastIndexOf, which
// is specialized for different types. This
// boosts performance since instead of making a
// virtual method call each iteration of the loop,
// via EqualityComparer<T>.Default.Equals, we
// only make one virtual call to EqualityComparer.LastIndexOf.
return _size != 0 && Array.LastIndexOf(_array, item, _size - 1) != -1;
}
/// <summary>
/// This method removes all items which match the predicate.
/// The complexity is O(n).
/// </summary>
public int RemoveWhere(Func<T, bool> match)
{
if (match == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
int freeIndex = 0; // the first free slot in items array
// Find the first item which needs to be removed.
while (freeIndex < _size && !match(_array[freeIndex]))
freeIndex++;
if (freeIndex >= _size)
return 0;
int current = freeIndex + 1;
while (current < _size)
{
// Find the first item which needs to be kept.
while (current < _size && match(_array[current]))
current++;
if (current < _size)
{
// copy item to the free slot.
_array[freeIndex++] = _array[current++];
}
}
if (_clearOnFree)
{
// Clear the removed elements so that the gc can reclaim the references.
Array.Clear(_array, freeIndex, _size - freeIndex);
}
int result = _size - freeIndex;
_size = freeIndex;
_version++;
return result;
}
// Copies the stack into an array.
public void CopyTo(T[] array, int arrayIndex)
{
if (array == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
}
if (arrayIndex < 0 || arrayIndex > array.Length)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex);
}
if (array.Length - arrayIndex < _size)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_ArrayPlusOffTooSmall);
}
Debug.Assert(array != _array);
int srcIndex = 0;
int dstIndex = arrayIndex + _size;
while (srcIndex < _size)
{
array[--dstIndex] = _array[srcIndex++];
}
}
public void CopyTo(Span<T> span)
{
if (span.Length < _size)
{
ThrowHelper.ThrowArgumentException_DestinationTooShort();
}
int srcIndex = 0;
int dstIndex = _size;
while (srcIndex < _size)
{
span[--dstIndex] = _array[srcIndex++];
}
}
void ICollection.CopyTo(Array array, int arrayIndex)
{
if (array == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
}
if (array.Rank != 1)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_RankMultiDimNotSupported);
}
if (array.GetLowerBound(0) != 0)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Arg_NonZeroLowerBound, ExceptionArgument.array);
}
if (arrayIndex < 0 || arrayIndex > array.Length)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.arrayIndex);
}
if (array.Length - arrayIndex < _size)
{
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidOffLen);
}
try
{
Array.Copy(_array, 0, array, arrayIndex, _size);
Array.Reverse(array, arrayIndex, _size);
}
catch (ArrayTypeMismatchException)
{
ThrowHelper.ThrowArgumentException_Argument_InvalidArrayType();
}
}
/// <summary>
/// Returns an IEnumerator for this PooledStack.
/// </summary>
/// <returns></returns>
public Enumerator GetEnumerator()
=> new Enumerator(this);
/// <internalonly/>
IEnumerator<T> IEnumerable<T>.GetEnumerator()
=> new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator()
=> new Enumerator(this);
public void TrimExcess()
{
if (_size == 0)
{
ReturnArray(replaceWith: Array.Empty<T>());
_version++;
return;
}
int threshold = (int)(_array.Length * 0.9);
if (_size < threshold)
{
var newArray = _pool.Rent(_size);
if (newArray.Length < _array.Length)
{
Array.Copy(_array, newArray, _size);
ReturnArray(replaceWith: newArray);
_version++;
}
else
{
// The array from the pool wasn't any smaller than the one we already had,
// (we can only control minimum size) so return it and do nothing.
// If we create an exact-sized array not from the pool, we'll
// get an exception when returning it to the pool.
_pool.Return(newArray);
}
}
}
/// <summary>
/// Returns the top object on the stack without removing it. If the stack
/// is empty, Peek throws an InvalidOperationException.
/// </summary>
public T Peek()
{
int size = _size - 1;
T[] array = _array;
if ((uint)size >= (uint)array.Length)
{
ThrowForEmptyStack();
}
return array[size];
}
public bool TryPeek(out T result)
{
int size = _size - 1;
T[] array = _array;
if ((uint)size >= (uint)array.Length)
{
result = default;
return false;
}
result = array[size];
return true;
}
/// <summary>
/// Pops an item from the top of the stack. If the stack is empty, Pop
/// throws an InvalidOperationException.
/// </summary>
public T Pop()
{
int size = _size - 1;
T[] array = _array;
// if (_size == 0) is equivalent to if (size == -1), and this case
// is covered with (uint)size, thus allowing bounds check elimination
// https://github.com/dotnet/coreclr/pull/9773
if ((uint)size >= (uint)array.Length)
{
ThrowForEmptyStack();
}
_version++;
_size = size;
T item = array[size];
if (_clearOnFree)
{
array[size] = default; // Free memory quicker.
}
return item;
}
public bool TryPop(out T result)
{
int size = _size - 1;
T[] array = _array;
if ((uint)size >= (uint)array.Length)
{
result = default;
return false;
}
_version++;
_size = size;
result = array[size];
if (_clearOnFree)
{
array[size] = default; // Free memory quicker.
}
return true;
}
/// <summary>
/// Pushes an item to the top of the stack.
/// </summary>
public void Push(T item)
{
int size = _size;
T[] array = _array;
if ((uint)size < (uint)array.Length)
{
array[size] = item;
_version++;
_size = size + 1;
}
else
{
PushWithResize(item);
}
}
// Non-inline from Stack.Push to improve its code quality as uncommon path
[MethodImpl(MethodImplOptions.NoInlining)]
private void PushWithResize(T item)
{
var newArray = _pool.Rent((_array.Length == 0) ? DefaultCapacity : 2 * _array.Length);
Array.Copy(_array, newArray, _size);
ReturnArray(replaceWith: newArray);
_array[_size] = item;
_version++;
_size++;
}
/// <summary>
/// Copies the Stack to an array, in the same order Pop would return the items.
/// </summary>
public T[] ToArray()
{
if (_size == 0)
return Array.Empty<T>();
T[] objArray = new T[_size];
int i = 0;
while (i < _size)
{
objArray[i] = _array[_size - i - 1];
i++;
}
return objArray;
}
private void ThrowForEmptyStack()
{
Debug.Assert(_size == 0);
throw new InvalidOperationException("Stack was empty.");
}
private void ReturnArray(T[] replaceWith = null)
{
if (_array?.Length > 0)
{
try
{
_pool.Return(_array, clearArray: _clearOnFree);
}
catch (ArgumentException)
{
// oh well, the array pool didn't like our array
}
}
if (!(replaceWith is null))
{
_array = replaceWith;
}
}
private static bool ShouldClear(ClearMode mode)
{
#if NETCOREAPP2_1
return mode == ClearMode.Always
|| (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences<T>());
#else
return mode != ClearMode.Never;
#endif
}
public void Dispose()
{
ReturnArray(replaceWith: Array.Empty<T>());
_size = 0;
_version++;
}
void IDeserializationCallback.OnDeserialization(object sender)
{
// We can't serialize array pools, so deserialized PooledStacks will
// have to use the shared pool, even if they were using a custom pool
// before serialization.
_pool = ArrayPool<T>.Shared;
}
[SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes", Justification = "not an expected scenario")]
public struct Enumerator : IEnumerator<T>, IEnumerator
{
private readonly PooledStack<T> _stack;
private readonly int _version;
private int _index;
private T _currentElement;
internal Enumerator(PooledStack<T> stack)
{
_stack = stack;
_version = stack._version;
_index = -2;
_currentElement = default;
}
public void Dispose()
{
_index = -1;
}
public bool MoveNext()
{
bool retval;
if (_version != _stack._version)
throw new InvalidOperationException("Collection was modified during enumeration.");
if (_index == -2)
{ // First call to enumerator.
_index = _stack._size - 1;
retval = (_index >= 0);
if (retval)
_currentElement = _stack._array[_index];
return retval;
}
if (_index == -1)
{ // End of enumeration.
return false;
}
retval = (--_index >= 0);
if (retval)
_currentElement = _stack._array[_index];
else
_currentElement = default;
return retval;
}
public T Current
{
get
{
if (_index < 0)
ThrowEnumerationNotStartedOrEnded();
return _currentElement;
}
}
private void ThrowEnumerationNotStartedOrEnded()
{
Debug.Assert(_index == -1 || _index == -2);
throw new InvalidOperationException(_index == -2 ? "Enumeration was not started." : "Enumeration has ended.");
}
object IEnumerator.Current
{
get { return Current; }
}
void IEnumerator.Reset()
{
if (_version != _stack._version)
throw new InvalidOperationException("Collection was modified during enumeration.");
_index = -2;
_currentElement = default;
}
}
}
}

28
src/Avalonia.Base/Collections/Pooled/StackDebugView.cs

@ -0,0 +1,28 @@
// 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.Diagnostics;
namespace Avalonia.Collections.Pooled
{
internal sealed class StackDebugView<T>
{
private readonly PooledStack<T> _stack;
public StackDebugView(PooledStack<T> stack)
{
_stack = stack ?? throw new ArgumentNullException(nameof(stack));
}
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public T[] Items
{
get
{
return _stack.ToArray();
}
}
}
}

691
src/Avalonia.Base/Collections/Pooled/ThrowHelper.cs

@ -0,0 +1,691 @@
// 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.
// This file defines an internal class used to throw exceptions in BCL code.
// The main purpose is to reduce code size.
//
// The old way to throw an exception generates quite a lot IL code and assembly code.
// Following is an example:
// C# source
// throw new ArgumentNullException(nameof(key), SR.ArgumentNull_Key);
// IL code:
// IL_0003: ldstr "key"
// IL_0008: ldstr "ArgumentNull_Key"
// IL_000d: call string System.Environment::GetResourceString(string)
// IL_0012: newobj instance void System.ArgumentNullException::.ctor(string,string)
// IL_0017: throw
// which is 21bytes in IL.
//
// So we want to get rid of the ldstr and call to Environment.GetResource in IL.
// In order to do that, I created two enums: ExceptionResource, ExceptionArgument to represent the
// argument name and resource name in a small integer. The source code will be changed to
// ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key, ExceptionResource.ArgumentNull_Key);
//
// The IL code will be 7 bytes.
// IL_0008: ldc.i4.4
// IL_0009: ldc.i4.4
// IL_000a: call void System.ThrowHelper::ThrowArgumentNullException(valuetype System.ExceptionArgument)
// IL_000f: ldarg.0
//
// This will also reduce the Jitted code size a lot.
//
// It is very important we do this for generic classes because we can easily generate the same code
// multiple times for different instantiation.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
namespace Avalonia.Collections.Pooled
{
internal static class ThrowHelper
{
internal static void ThrowArrayTypeMismatchException()
{
throw new ArrayTypeMismatchException();
}
internal static void ThrowIndexOutOfRangeException()
{
throw new IndexOutOfRangeException();
}
internal static void ThrowArgumentOutOfRangeException()
{
throw new ArgumentOutOfRangeException();
}
internal static void ThrowArgumentException_DestinationTooShort()
{
throw new ArgumentException("Destination too short.");
}
internal static void ThrowArgumentException_OverlapAlignmentMismatch()
{
throw new ArgumentException("Overlap alignment mismatch.");
}
internal static void ThrowArgumentOutOfRange_IndexException()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.index,
ExceptionResource.ArgumentOutOfRange_Index);
}
internal static void ThrowIndexArgumentOutOfRange_NeedNonNegNumException()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.index,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
internal static void ThrowValueArgumentOutOfRange_NeedNonNegNumException()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.value,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
internal static void ThrowLengthArgumentOutOfRange_ArgumentOutOfRange_NeedNonNegNum()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.length,
ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
}
internal static void ThrowStartIndexArgumentOutOfRange_ArgumentOutOfRange_Index()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.startIndex,
ExceptionResource.ArgumentOutOfRange_Index);
}
internal static void ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count()
{
throw GetArgumentOutOfRangeException(ExceptionArgument.count,
ExceptionResource.ArgumentOutOfRange_Count);
}
internal static void ThrowWrongKeyTypeArgumentException<T>(T key, Type targetType)
{
// Generic key to move the boxing to the right hand side of throw
throw GetWrongKeyTypeArgumentException((object)key, targetType);
}
internal static void ThrowWrongValueTypeArgumentException<T>(T value, Type targetType)
{
// Generic key to move the boxing to the right hand side of throw
throw GetWrongValueTypeArgumentException((object)value, targetType);
}
private static ArgumentException GetAddingDuplicateWithKeyArgumentException(object key)
{
return new ArgumentException($"Error adding duplicate with key: {key}.");
}
internal static void ThrowAddingDuplicateWithKeyArgumentException<T>(T key)
{
// Generic key to move the boxing to the right hand side of throw
throw GetAddingDuplicateWithKeyArgumentException((object)key);
}
internal static void ThrowKeyNotFoundException<T>(T key)
{
// Generic key to move the boxing to the right hand side of throw
throw GetKeyNotFoundException((object)key);
}
internal static void ThrowArgumentException(ExceptionResource resource)
{
throw GetArgumentException(resource);
}
internal static void ThrowArgumentException(ExceptionResource resource, ExceptionArgument argument)
{
throw GetArgumentException(resource, argument);
}
private static ArgumentNullException GetArgumentNullException(ExceptionArgument argument)
{
return new ArgumentNullException(GetArgumentName(argument));
}
internal static void ThrowArgumentNullException(ExceptionArgument argument)
{
throw GetArgumentNullException(argument);
}
internal static void ThrowArgumentNullException(ExceptionResource resource)
{
throw new ArgumentNullException(GetResourceString(resource));
}
internal static void ThrowArgumentNullException(ExceptionArgument argument, ExceptionResource resource)
{
throw new ArgumentNullException(GetArgumentName(argument), GetResourceString(resource));
}
internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument)
{
throw new ArgumentOutOfRangeException(GetArgumentName(argument));
}
internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
{
throw GetArgumentOutOfRangeException(argument, resource);
}
internal static void ThrowArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource)
{
throw GetArgumentOutOfRangeException(argument, paramNumber, resource);
}
internal static void ThrowInvalidOperationException(ExceptionResource resource)
{
throw GetInvalidOperationException(resource);
}
internal static void ThrowInvalidOperationException(ExceptionResource resource, Exception e)
{
throw new InvalidOperationException(GetResourceString(resource), e);
}
internal static void ThrowSerializationException(ExceptionResource resource)
{
throw new SerializationException(GetResourceString(resource));
}
internal static void ThrowSecurityException(ExceptionResource resource)
{
throw new System.Security.SecurityException(GetResourceString(resource));
}
internal static void ThrowRankException(ExceptionResource resource)
{
throw new RankException(GetResourceString(resource));
}
internal static void ThrowNotSupportedException(ExceptionResource resource)
{
throw new NotSupportedException(GetResourceString(resource));
}
internal static void ThrowUnauthorizedAccessException(ExceptionResource resource)
{
throw new UnauthorizedAccessException(GetResourceString(resource));
}
internal static void ThrowObjectDisposedException(string objectName, ExceptionResource resource)
{
throw new ObjectDisposedException(objectName, GetResourceString(resource));
}
internal static void ThrowObjectDisposedException(ExceptionResource resource)
{
throw new ObjectDisposedException(null, GetResourceString(resource));
}
internal static void ThrowNotSupportedException()
{
throw new NotSupportedException();
}
internal static void ThrowAggregateException(List<Exception> exceptions)
{
throw new AggregateException(exceptions);
}
internal static void ThrowOutOfMemoryException()
{
throw new OutOfMemoryException();
}
internal static void ThrowArgumentException_Argument_InvalidArrayType()
{
throw new ArgumentException("Invalid array type.");
}
internal static void ThrowInvalidOperationException_InvalidOperation_EnumNotStarted()
{
throw new InvalidOperationException("Enumeration has not started.");
}
internal static void ThrowInvalidOperationException_InvalidOperation_EnumEnded()
{
throw new InvalidOperationException("Enumeration has ended.");
}
internal static void ThrowInvalidOperationException_EnumCurrent(int index)
{
throw GetInvalidOperationException_EnumCurrent(index);
}
internal static void ThrowInvalidOperationException_InvalidOperation_EnumFailedVersion()
{
throw new InvalidOperationException("Collection was modified during enumeration.");
}
internal static void ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen()
{
throw new InvalidOperationException("Invalid enumerator state: enumeration cannot proceed.");
}
internal static void ThrowInvalidOperationException_InvalidOperation_NoValue()
{
throw new InvalidOperationException("No value provided.");
}
internal static void ThrowInvalidOperationException_ConcurrentOperationsNotSupported()
{
throw new InvalidOperationException("Concurrent operations are not supported.");
}
internal static void ThrowInvalidOperationException_HandleIsNotInitialized()
{
throw new InvalidOperationException("Handle is not initialized.");
}
internal static void ThrowFormatException_BadFormatSpecifier()
{
throw new FormatException("Bad format specifier.");
}
private static ArgumentException GetArgumentException(ExceptionResource resource)
{
return new ArgumentException(GetResourceString(resource));
}
private static InvalidOperationException GetInvalidOperationException(ExceptionResource resource)
{
return new InvalidOperationException(GetResourceString(resource));
}
private static ArgumentException GetWrongKeyTypeArgumentException(object key, Type targetType)
{
return new ArgumentException($"Wrong key type. Expected {targetType}, got: '{key}'.", nameof(key));
}
private static ArgumentException GetWrongValueTypeArgumentException(object value, Type targetType)
{
return new ArgumentException($"Wrong value type. Expected {targetType}, got: '{value}'.", nameof(value));
}
private static KeyNotFoundException GetKeyNotFoundException(object key)
{
return new KeyNotFoundException($"Key not found: {key}");
}
private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, ExceptionResource resource)
{
return new ArgumentOutOfRangeException(GetArgumentName(argument), GetResourceString(resource));
}
private static ArgumentException GetArgumentException(ExceptionResource resource, ExceptionArgument argument)
{
return new ArgumentException(GetResourceString(resource), GetArgumentName(argument));
}
private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument, int paramNumber, ExceptionResource resource)
{
return new ArgumentOutOfRangeException(GetArgumentName(argument) + "[" + paramNumber.ToString() + "]", GetResourceString(resource));
}
private static InvalidOperationException GetInvalidOperationException_EnumCurrent(int index)
{
return new InvalidOperationException(
index < 0 ?
"Enumeration has not started" :
"Enumeration has ended");
}
// Allow nulls for reference types and Nullable<U>, but not for value types.
// Aggressively inline so the jit evaluates the if in place and either drops the call altogether
// Or just leaves null test and call to the Non-returning ThrowHelper.ThrowArgumentNullException
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void IfNullAndNullsAreIllegalThenThrow<T>(object value, ExceptionArgument argName)
{
// Note that default(T) is not equal to null for value types except when T is Nullable<U>.
if (!(default(T) == null) && value == null)
ThrowHelper.ThrowArgumentNullException(argName);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void ThrowForUnsupportedVectorBaseType<T>() where T : struct
{
if (typeof(T) != typeof(byte) && typeof(T) != typeof(sbyte) &&
typeof(T) != typeof(short) && typeof(T) != typeof(ushort) &&
typeof(T) != typeof(int) && typeof(T) != typeof(uint) &&
typeof(T) != typeof(long) && typeof(T) != typeof(ulong) &&
typeof(T) != typeof(float) && typeof(T) != typeof(double))
{
ThrowNotSupportedException(ExceptionResource.Arg_TypeNotSupported);
}
}
#if false // Reflection-based implementation does not work for CoreRT/ProjectN
// This function will convert an ExceptionArgument enum value to the argument name string.
[MethodImpl(MethodImplOptions.NoInlining)]
private static string GetArgumentName(ExceptionArgument argument)
{
Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument),
"The enum value is not defined, please check the ExceptionArgument Enum.");
return argument.ToString();
}
#endif
private static string GetArgumentName(ExceptionArgument argument)
{
switch (argument)
{
case ExceptionArgument.obj:
return "obj";
case ExceptionArgument.dictionary:
return "dictionary";
case ExceptionArgument.array:
return "array";
case ExceptionArgument.info:
return "info";
case ExceptionArgument.key:
return "key";
case ExceptionArgument.text:
return "text";
case ExceptionArgument.values:
return "values";
case ExceptionArgument.value:
return "value";
case ExceptionArgument.startIndex:
return "startIndex";
case ExceptionArgument.task:
return "task";
case ExceptionArgument.ch:
return "ch";
case ExceptionArgument.s:
return "s";
case ExceptionArgument.input:
return "input";
case ExceptionArgument.list:
return "list";
case ExceptionArgument.index:
return "index";
case ExceptionArgument.capacity:
return "capacity";
case ExceptionArgument.collection:
return "collection";
case ExceptionArgument.item:
return "item";
case ExceptionArgument.converter:
return "converter";
case ExceptionArgument.match:
return "match";
case ExceptionArgument.count:
return "count";
case ExceptionArgument.action:
return "action";
case ExceptionArgument.comparison:
return "comparison";
case ExceptionArgument.exceptions:
return "exceptions";
case ExceptionArgument.exception:
return "exception";
case ExceptionArgument.enumerable:
return "enumerable";
case ExceptionArgument.start:
return "start";
case ExceptionArgument.format:
return "format";
case ExceptionArgument.culture:
return "culture";
case ExceptionArgument.comparer:
return "comparer";
case ExceptionArgument.comparable:
return "comparable";
case ExceptionArgument.source:
return "source";
case ExceptionArgument.state:
return "state";
case ExceptionArgument.length:
return "length";
case ExceptionArgument.comparisonType:
return "comparisonType";
case ExceptionArgument.manager:
return "manager";
case ExceptionArgument.sourceBytesToCopy:
return "sourceBytesToCopy";
case ExceptionArgument.callBack:
return "callBack";
case ExceptionArgument.creationOptions:
return "creationOptions";
case ExceptionArgument.function:
return "function";
case ExceptionArgument.delay:
return "delay";
case ExceptionArgument.millisecondsDelay:
return "millisecondsDelay";
case ExceptionArgument.millisecondsTimeout:
return "millisecondsTimeout";
case ExceptionArgument.timeout:
return "timeout";
case ExceptionArgument.type:
return "type";
case ExceptionArgument.sourceIndex:
return "sourceIndex";
case ExceptionArgument.sourceArray:
return "sourceArray";
case ExceptionArgument.destinationIndex:
return "destinationIndex";
case ExceptionArgument.destinationArray:
return "destinationArray";
case ExceptionArgument.other:
return "other";
case ExceptionArgument.newSize:
return "newSize";
case ExceptionArgument.lowerBounds:
return "lowerBounds";
case ExceptionArgument.lengths:
return "lengths";
case ExceptionArgument.len:
return "len";
case ExceptionArgument.keys:
return "keys";
case ExceptionArgument.indices:
return "indices";
case ExceptionArgument.endIndex:
return "endIndex";
case ExceptionArgument.elementType:
return "elementType";
case ExceptionArgument.arrayIndex:
return "arrayIndex";
default:
Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum.");
return argument.ToString();
}
}
#if false // Reflection-based implementation does not work for CoreRT/ProjectN
// This function will convert an ExceptionResource enum value to the resource string.
[MethodImpl(MethodImplOptions.NoInlining)]
private static string GetResourceString(ExceptionResource resource)
{
Debug.Assert(Enum.IsDefined(typeof(ExceptionResource), resource),
"The enum value is not defined, please check the ExceptionResource Enum.");
return SR.GetResourceString(resource.ToString());
}
#endif
private static string GetResourceString(ExceptionResource resource)
{
switch (resource)
{
case ExceptionResource.ArgumentOutOfRange_Index:
return "Argument 'index' was out of the range of valid values.";
case ExceptionResource.ArgumentOutOfRange_Count:
return "Argument 'count' was out of the range of valid values.";
case ExceptionResource.Arg_ArrayPlusOffTooSmall:
return "Array plus offset too small.";
case ExceptionResource.NotSupported_ReadOnlyCollection:
return "This operation is not supported on a read-only collection.";
case ExceptionResource.Arg_RankMultiDimNotSupported:
return "Multi-dimensional arrays are not supported.";
case ExceptionResource.Arg_NonZeroLowerBound:
return "Arrays with a non-zero lower bound are not supported.";
case ExceptionResource.ArgumentOutOfRange_ListInsert:
return "Insertion index was out of the range of valid values.";
case ExceptionResource.ArgumentOutOfRange_NeedNonNegNum:
return "The number must be non-negative.";
case ExceptionResource.ArgumentOutOfRange_SmallCapacity:
return "The capacity cannot be set below the current Count.";
case ExceptionResource.Argument_InvalidOffLen:
return "Invalid offset length.";
case ExceptionResource.ArgumentOutOfRange_BiggerThanCollection:
return "The given value was larger than the size of the collection.";
case ExceptionResource.Serialization_MissingKeys:
return "Serialization error: missing keys.";
case ExceptionResource.Serialization_NullKey:
return "Serialization error: null key.";
case ExceptionResource.NotSupported_KeyCollectionSet:
return "The KeyCollection does not support modification.";
case ExceptionResource.NotSupported_ValueCollectionSet:
return "The ValueCollection does not support modification.";
case ExceptionResource.InvalidOperation_NullArray:
return "Null arrays are not supported.";
case ExceptionResource.InvalidOperation_HSCapacityOverflow:
return "Set hash capacity overflow. Cannot increase size.";
case ExceptionResource.NotSupported_StringComparison:
return "String comparison not supported.";
case ExceptionResource.ConcurrentCollection_SyncRoot_NotSupported:
return "SyncRoot not supported.";
case ExceptionResource.ArgumentException_OtherNotArrayOfCorrectLength:
return "The other array is not of the correct length.";
case ExceptionResource.ArgumentOutOfRange_EndIndexStartIndex:
return "The end index does not come after the start index.";
case ExceptionResource.ArgumentOutOfRange_HugeArrayNotSupported:
return "Huge arrays are not supported.";
case ExceptionResource.Argument_AddingDuplicate:
return "Duplicate item added.";
case ExceptionResource.Argument_InvalidArgumentForComparison:
return "Invalid argument for comparison.";
case ExceptionResource.Arg_LowerBoundsMustMatch:
return "Array lower bounds must match.";
case ExceptionResource.Arg_MustBeType:
return "Argument must be of type: ";
case ExceptionResource.InvalidOperation_IComparerFailed:
return "IComparer failed.";
case ExceptionResource.NotSupported_FixedSizeCollection:
return "This operation is not suppored on a fixed-size collection.";
case ExceptionResource.Rank_MultiDimNotSupported:
return "Multi-dimensional arrays are not supported.";
case ExceptionResource.Arg_TypeNotSupported:
return "Type not supported.";
default:
Debug.Assert(false,
"The enum value is not defined, please check the ExceptionResource Enum.");
return resource.ToString();
}
}
}
//
// The convention for this enum is using the argument name as the enum name
//
internal enum ExceptionArgument
{
obj,
dictionary,
array,
info,
key,
text,
values,
value,
startIndex,
task,
ch,
s,
input,
list,
index,
capacity,
collection,
item,
converter,
match,
count,
action,
comparison,
exceptions,
exception,
enumerable,
start,
format,
culture,
comparer,
comparable,
source,
state,
length,
comparisonType,
manager,
sourceBytesToCopy,
callBack,
creationOptions,
function,
delay,
millisecondsDelay,
millisecondsTimeout,
timeout,
type,
sourceIndex,
sourceArray,
destinationIndex,
destinationArray,
other,
newSize,
lowerBounds,
lengths,
len,
keys,
indices,
endIndex,
elementType,
arrayIndex
}
//
// The convention for this enum is using the resource name as the enum name
//
internal enum ExceptionResource
{
ArgumentOutOfRange_Index,
ArgumentOutOfRange_Count,
Arg_ArrayPlusOffTooSmall,
NotSupported_ReadOnlyCollection,
Arg_RankMultiDimNotSupported,
Arg_NonZeroLowerBound,
ArgumentOutOfRange_ListInsert,
ArgumentOutOfRange_NeedNonNegNum,
ArgumentOutOfRange_SmallCapacity,
Argument_InvalidOffLen,
ArgumentOutOfRange_BiggerThanCollection,
Serialization_MissingKeys,
Serialization_NullKey,
NotSupported_KeyCollectionSet,
NotSupported_ValueCollectionSet,
InvalidOperation_NullArray,
InvalidOperation_HSCapacityOverflow,
NotSupported_StringComparison,
ConcurrentCollection_SyncRoot_NotSupported,
ArgumentException_OtherNotArrayOfCorrectLength,
ArgumentOutOfRange_EndIndexStartIndex,
ArgumentOutOfRange_HugeArrayNotSupported,
Argument_AddingDuplicate,
Argument_InvalidArgumentForComparison,
Arg_LowerBoundsMustMatch,
Arg_MustBeType,
InvalidOperation_IComparerFailed,
NotSupported_FixedSizeCollection,
Rank_MultiDimNotSupported,
Arg_TypeNotSupported,
}
}

4
src/Avalonia.Input/InputExtensions.cs

@ -38,7 +38,9 @@ namespace Avalonia.Input
/// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns>
public static IInputElement InputHitTest(this IInputElement element, Point p)
{
return element.GetInputElementsAt(p).FirstOrDefault();
Contract.Requires<ArgumentNullException>(element != null);
return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement;
}
private static bool IsHitTestVisible(IVisual visual)

26
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -160,16 +160,23 @@ namespace Avalonia.Rendering
/// <inheritdoc/>
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter)
{
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
{
// When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene();
}
EnsureCanHitTest();
//It's safe to access _scene here without a lock since
//it's only changed from UI thread which we are currently on
return _scene?.Item.HitTest(p, root, filter) ?? Enumerable.Empty<IVisual>();
}
/// <inheritdoc/>
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
EnsureCanHitTest();
//It's safe to access _scene here without a lock since
//it's only changed from UI thread which we are currently on
return _scene?.Item.HitTestFirst(p, root, filter);
}
/// <inheritdoc/>
public void Paint(Rect rect)
{
@ -235,6 +242,15 @@ namespace Avalonia.Rendering
internal Scene UnitTestScene() => _scene.Item;
private void EnsureCanHitTest()
{
if (_renderLoop == null && (_dirty == null || _dirty.Count > 0))
{
// When unit testing the renderLoop may be null, so update the scene manually.
UpdateScene();
}
}
private void Render(bool forceComposite)
{
using (var l = _lock.TryLock())

12
src/Avalonia.Visuals/Rendering/IRenderer.cs

@ -50,6 +50,18 @@ namespace Avalonia.Rendering
/// <returns>The visuals at the specified point, topmost first.</returns>
IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter);
/// <summary>
/// Hit tests a location to find first visual at the specified point.
/// </summary>
/// <param name="p">The point, in client coordinates.</param>
/// <param name="root">The root of the subtree to search.</param>
/// <param name="filter">
/// A filter predicate. If the predicate returns false then the visual and all its
/// children will be excluded from the results.
/// </param>
/// <returns>The visual at the specified point, topmost first.</returns>
IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter);
/// <summary>
/// Informs the renderer that the z-ordering of a visual's children has changed.
/// </summary>

5
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -164,6 +164,11 @@ namespace Avalonia.Rendering
return HitTest(root, p, filter);
}
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
return HitTest(root, p, filter).FirstOrDefault();
}
/// <inheritdoc/>
public void RecalculateChildren(IVisual visual) => AddDirty(visual);

174
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -2,8 +2,10 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Collections.Pooled;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
@ -128,7 +130,20 @@ namespace Avalonia.Rendering.SceneGraph
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter)
{
var node = FindNode(root);
return (node != null) ? HitTest(node, p, null, filter) : Enumerable.Empty<IVisual>();
return (node != null) ? new HitTestEnumerable(node, filter, p, Root) : Enumerable.Empty<IVisual>();
}
/// <summary>
/// Gets the visual at a point in the scene.
/// </summary>
/// <param name="p">The point.</param>
/// <param name="root">The root of the subtree to search.</param>
/// <param name="filter">A filter. May be null.</param>
/// <returns>The visual at the specified point.</returns>
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter)
{
var node = FindNode(root);
return (node != null) ? HitTestFirst(node, p, filter) : null;
}
/// <summary>
@ -158,38 +173,157 @@ namespace Avalonia.Rendering.SceneGraph
return result;
}
private IEnumerable<IVisual> HitTest(IVisualNode node, Point p, Rect? clip, Func<IVisual, bool> filter)
private IVisual HitTestFirst(IVisualNode root, Point p, Func<IVisual, bool> filter)
{
if (filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree)
using var enumerator = new HitTestEnumerator(root, filter, p, Root);
enumerator.MoveNext();
return enumerator.Current;
}
private class HitTestEnumerable : IEnumerable<IVisual>
{
private readonly IVisualNode _root;
private readonly Func<IVisual, bool> _filter;
private readonly IVisualNode _sceneRoot;
private readonly Point _point;
public HitTestEnumerable(IVisualNode root, Func<IVisual, bool> filter, Point point, IVisualNode sceneRoot)
{
var clipped = false;
_root = root;
_filter = filter;
_point = point;
_sceneRoot = sceneRoot;
}
if (node.ClipToBounds)
{
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
clipped = !clip.Value.Contains(p);
}
public IEnumerator<IVisual> GetEnumerator()
{
return new HitTestEnumerator(_root, _filter, _point, _sceneRoot);
}
if (node.GeometryClip != null)
{
var controlPoint = Root.Visual.TranslatePoint(p, node.Visual);
clipped = !node.GeometryClip.FillContains(controlPoint.Value);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
private struct HitTestEnumerator : IEnumerator<IVisual>
{
private readonly PooledStack<Entry> _nodeStack;
private readonly Func<IVisual, bool> _filter;
private readonly IVisualNode _sceneRoot;
private IVisual _current;
private readonly Point _point;
public HitTestEnumerator(IVisualNode root, Func<IVisual, bool> filter, Point point, IVisualNode sceneRoot)
{
_nodeStack = new PooledStack<Entry>();
_nodeStack.Push(new Entry(root, false, null, true));
_filter = filter;
_point = point;
_sceneRoot = sceneRoot;
if (!clipped)
_current = null;
}
public bool MoveNext()
{
while (_nodeStack.Count > 0)
{
for (var i = node.Children.Count - 1; i >= 0; --i)
(var wasVisited, var isRoot, IVisualNode node, Rect? clip) = _nodeStack.Pop();
if (wasVisited && isRoot)
{
foreach (var h in HitTest(node.Children[i], p, clip, filter))
break;
}
var children = node.Children;
int childCount = children.Count;
if (childCount == 0 || wasVisited)
{
if ((wasVisited || FilterAndClip(node, ref clip)) && node.HitTest(_point))
{
yield return h;
_current = node.Visual;
return true;
}
}
else if (FilterAndClip(node, ref clip))
{
_nodeStack.Push(new Entry(node, true, null));
for (var i = 0; i < childCount; i++)
{
_nodeStack.Push(new Entry(children[i], false, clip));
}
}
}
return false;
}
public void Reset()
{
throw new NotSupportedException();
}
public IVisual Current => _current;
object IEnumerator.Current => Current;
public void Dispose()
{
_nodeStack.Dispose();
}
if (node.HitTest(p))
private bool FilterAndClip(IVisualNode node, ref Rect? clip)
{
if (_filter?.Invoke(node.Visual) != false && node.Visual.IsAttachedToVisualTree)
{
var clipped = false;
if (node.ClipToBounds)
{
yield return node.Visual;
clip = clip == null ? node.ClipBounds : clip.Value.Intersect(node.ClipBounds);
clipped = !clip.Value.Contains(_point);
}
if (node.GeometryClip != null)
{
var controlPoint = _sceneRoot.Visual.TranslatePoint(_point, node.Visual);
clipped = !node.GeometryClip.FillContains(controlPoint.Value);
}
return !clipped;
}
return false;
}
private readonly struct Entry
{
public readonly bool WasVisited;
public readonly bool IsRoot;
public readonly IVisualNode Node;
public readonly Rect? Clip;
public Entry(IVisualNode node, bool wasVisited, Rect? clip, bool isRoot = false)
{
Node = node;
WasVisited = wasVisited;
IsRoot = isRoot;
Clip = clip;
}
public void Deconstruct(out bool wasVisited, out bool isRoot, out IVisualNode node, out Rect? clip)
{
wasVisited = WasVisited;
isRoot = IsRoot;
node = Node;
clip = Clip;
}
}
}

7
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -280,8 +280,13 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public bool HitTest(Point p)
{
foreach (var operation in DrawOperations)
var drawOperations = DrawOperations;
var drawOperationsCount = drawOperations.Count;
for (var i = 0; i < drawOperationsCount; i++)
{
var operation = drawOperations[i];
if (operation?.Item?.HitTest(p) == true)
{
return true;

29
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -213,12 +213,37 @@ namespace Avalonia.VisualTree
/// </summary>
/// <param name="visual">The root visual to test.</param>
/// <param name="p">The point.</param>
/// <returns>The visuals at the requested point.</returns>
/// <returns>The visual at the requested point.</returns>
public static IVisual GetVisualAt(this IVisual visual, Point p)
{
Contract.Requires<ArgumentNullException>(visual != null);
return visual.GetVisualsAt(p).FirstOrDefault();
return visual.GetVisualAt(p, x => x.IsVisible);
}
/// <summary>
/// Gets the first visual in the visual tree whose bounds contain a point.
/// </summary>
/// <param name="visual">The root visual to test.</param>
/// <param name="p">The point.</param>
/// <param name="filter">
/// A filter predicate. If the predicate returns false then the visual and all its
/// children will be excluded from the results.
/// </param>
/// <returns>The visual at the requested point.</returns>
public static IVisual GetVisualAt(this IVisual visual, Point p, Func<IVisual, bool> filter)
{
Contract.Requires<ArgumentNullException>(visual != null);
var root = visual.GetVisualRoot();
var rootPoint = visual.TranslatePoint(p, root);
if (rootPoint.HasValue)
{
return root.Renderer.HitTestFirst(rootPoint.Value, visual, filter);
}
return null;
}
/// <summary>

4
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -795,9 +795,9 @@ namespace Avalonia.Win32
modifiers |= RawInputModifiers.RightMouseButton;
if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_MBUTTON))
modifiers |= RawInputModifiers.MiddleMouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON1))
if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_XBUTTON1))
modifiers |= RawInputModifiers.XButton1MouseButton;
if (keys.HasFlag(UnmanagedMethods.ModifierKeys.MK_XBUTTON2))
if (keys.HasFlagCustom(UnmanagedMethods.ModifierKeys.MK_XBUTTON2))
modifiers |= RawInputModifiers.XButton2MouseButton;
return modifiers;
}

2
tests/Avalonia.Benchmarks/NullRenderer.cs

@ -21,6 +21,8 @@ namespace Avalonia.Benchmarks
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter) => null;
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter) => null;
public void Paint(Rect rect)
{
}

3
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@ -238,6 +238,9 @@ namespace Avalonia.Input.UnitTests
{
renderer.Setup(x => x.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(new[] { hit });
renderer.Setup(x => x.HitTestFirst(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
.Returns(hit);
}
private IDisposable TestApplication(IRenderer renderer)

2
tests/Avalonia.LeakTests/ControlTests.cs

@ -397,6 +397,8 @@ namespace Avalonia.LeakTests
public IEnumerable<IVisual> HitTest(Point p, IVisual root, Func<IVisual, bool> filter) => null;
public IVisual HitTestFirst(Point p, IVisual root, Func<IVisual, bool> filter) => null;
public void Paint(Rect rect)
{
}

Loading…
Cancel
Save