Browse Source

Merge branch 'master' into fix_pull_pinch

pull/10027/head
Wiesław Šoltés 4 years ago
committed by GitHub
parent
commit
b058804c0a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.Desktop.slnf
  2. 7
      Avalonia.sln
  3. 1
      build/Base.props
  4. 15
      build/SharpDX.props
  5. 21
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  6. 83
      samples/ControlCatalog/Pages/ScreenPage.cs
  7. 8
      samples/GpuInterop/App.axaml
  8. 22
      samples/GpuInterop/App.axaml.cs
  9. 147
      samples/GpuInterop/D3DDemo/D3D11DemoControl.cs
  10. 110
      samples/GpuInterop/D3DDemo/D3D11Swapchain.cs
  11. 110
      samples/GpuInterop/D3DDemo/D3DContent.cs
  12. 47
      samples/GpuInterop/D3DDemo/MiniCube.fx
  13. 141
      samples/GpuInterop/DrawingSurfaceDemoBase.cs
  14. 32
      samples/GpuInterop/GpuDemo.axaml
  15. 116
      samples/GpuInterop/GpuDemo.axaml.cs
  16. 51
      samples/GpuInterop/GpuInterop.csproj
  17. 13
      samples/GpuInterop/MainWindow.axaml
  18. 21
      samples/GpuInterop/MainWindow.axaml.cs
  19. 15
      samples/GpuInterop/Program.cs
  20. 12
      samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile
  21. 42
      samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl
  22. BIN
      samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv
  23. 36
      samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl
  24. BIN
      samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv
  25. 47
      samples/GpuInterop/VulkanDemo/ByteString.cs
  26. 54
      samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs
  27. 80
      samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs
  28. 224
      samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs
  29. 829
      samples/GpuInterop/VulkanDemo/VulkanContent.cs
  30. 335
      samples/GpuInterop/VulkanDemo/VulkanContext.cs
  31. 101
      samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs
  32. 12
      samples/GpuInterop/VulkanDemo/VulkanExtensions.cs
  33. 276
      samples/GpuInterop/VulkanDemo/VulkanImage.cs
  34. 59
      samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs
  35. 58
      samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs
  36. 154
      samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs
  37. 4
      src/Avalonia.Base/AttachedProperty.cs
  38. 4
      src/Avalonia.Base/Avalonia.Base.csproj
  39. 16
      src/Avalonia.Base/AvaloniaObject.cs
  40. 8
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  41. 8
      src/Avalonia.Base/AvaloniaProperty.cs
  42. 59
      src/Avalonia.Base/Data/BindingOperations.cs
  43. 2
      src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs
  44. 2
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  45. 2
      src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs
  46. 2
      src/Avalonia.Base/Data/Core/ExpressionNode.cs
  47. 2
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  48. 2
      src/Avalonia.Base/Data/Core/IndexerNodeBase.cs
  49. 2
      src/Avalonia.Base/Data/Core/LogicalNotNode.cs
  50. 2
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  51. 28
      src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs
  52. 2
      src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs
  53. 2
      src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs
  54. 2
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  55. 2
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  56. 2
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  57. 4
      src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs
  58. 2
      src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs
  59. 2
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  60. 2
      src/Avalonia.Base/Data/Core/SettableNode.cs
  61. 2
      src/Avalonia.Base/Data/Core/StreamNode.cs
  62. 2
      src/Avalonia.Base/Data/Core/TypeCastNode.cs
  63. 43
      src/Avalonia.Base/Data/InstancedBinding.cs
  64. 13
      src/Avalonia.Base/DirectProperty.cs
  65. 6
      src/Avalonia.Base/DirectPropertyBase.cs
  66. 2
      src/Avalonia.Base/Logging/TraceLogSink.cs
  67. 2
      src/Avalonia.Base/Media/DrawingContext.cs
  68. 5
      src/Avalonia.Base/Media/DrawingGroup.cs
  69. 2
      src/Avalonia.Base/Media/FontManager.cs
  70. 74
      src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
  71. 559
      src/Avalonia.Base/Media/GlyphRun.cs
  72. 15
      src/Avalonia.Base/Media/GlyphRunMetrics.cs
  73. 2
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  74. 2
      src/Avalonia.Base/Media/TextDecoration.cs
  75. 268
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  76. 6
      src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs
  77. 84
      src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs
  78. 138
      src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs
  79. 36
      src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs
  80. 9
      src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs
  81. 230
      src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs
  82. 26
      src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs
  83. 11
      src/Avalonia.Base/Media/TextFormatting/SplitResult.cs
  84. 90
      src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs
  85. 6
      src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs
  86. 65
      src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs
  87. 339
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  88. 206
      src/Avalonia.Base/Media/TextFormatting/TextLayout.cs
  89. 100
      src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs
  90. 305
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  91. 8
      src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs
  92. 7
      src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  93. 8
      src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs
  94. 309
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  95. 94
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  96. 117
      src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs
  97. 24
      src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  98. 18
      src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs
  99. 220
      src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs
  100. 244
      src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

1
Avalonia.Desktop.slnf

@ -5,6 +5,7 @@
"packages\\Avalonia\\Avalonia.csproj",
"samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj",
"samples\\ControlCatalog\\ControlCatalog.csproj",
"samples\\GpuInterop\\GpuInterop.csproj",
"samples\\IntegrationTestApp\\IntegrationTestApp.csproj",
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\SampleControls\\ControlSamples.csproj",

7
Avalonia.sln

@ -231,6 +231,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -542,6 +544,10 @@ Global
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -606,6 +612,7 @@ Global
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

1
build/Base.props

@ -1,6 +1,7 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Condition="'$(TargetFramework)' != 'net6'">
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.6.0" />
</ItemGroup>

15
build/SharpDX.props

@ -1,9 +1,14 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<SharpDXPackageVersion>4.0.1</SharpDXPackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpDX" Version="4.0.1" />
<PackageReference Include="SharpDX.Direct2D1" Version="4.0.1" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.0.1" />
<PackageReference Include="SharpDX.DXGI" Version="4.0.1" />
<PackageReference Include="SharpDX.Direct3D9" Version="4.0.1" Condition="'$(UseDirect3D9)' == 'true'" />
<PackageReference Include="SharpDX" Version="$(SharpDXPackageVersion)" />
<PackageReference Include="SharpDX.Direct2D1" Version="$(SharpDXPackageVersion)" />
<PackageReference Include="SharpDX.Direct3D11" Version="$(SharpDXPackageVersion)" />
<PackageReference Include="SharpDX.DXGI" Version="$(SharpDXPackageVersion)" />
<PackageReference Include="SharpDX.Direct3D9" Version="$(SharpDXPackageVersion)" Condition="'$(UseDirect3D9)' == 'true'" />
<PackageReference Include="SharpDX.D3DCompiler" Version="$(SharpDXPackageVersion)" Condition="'$(UseD3DCompiler)' == 'true'" />
<PackageReference Include="SharpDX.Mathematics" Version="$(SharpDXPackageVersion)" Condition="'$(UseSharpDXMathematics)' == 'true'" />
</ItemGroup>
</Project>

21
samples/ControlCatalog/Pages/OpenGlPage.xaml.cs

@ -78,12 +78,7 @@ namespace ControlCatalog.Pages
get => _info;
private set => SetAndRaise(InfoProperty, ref _info, value);
}
static OpenGlPageControl()
{
AffectsRender<OpenGlPageControl>(YawProperty, PitchProperty, RollProperty, DiscoProperty);
}
private int _vertexShader;
private int _fragmentShader;
private int _shaderProgram;
@ -254,7 +249,7 @@ namespace ControlCatalog.Pages
Console.WriteLine(err);
}
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb)
protected override unsafe void OnOpenGlInit(GlInterface GL)
{
CheckError(GL);
@ -309,7 +304,7 @@ namespace ControlCatalog.Pages
}
protected override void OnOpenGlDeinit(GlInterface GL, int fb)
protected override void OnOpenGlDeinit(GlInterface GL)
{
// Unbind everything
GL.BindBuffer(GL_ARRAY_BUFFER, 0);
@ -366,7 +361,15 @@ namespace ControlCatalog.Pages
CheckError(GL);
if (_disco > 0.01)
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
RequestNextFrameRendering();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == YawProperty || change.Property == RollProperty || change.Property == PitchProperty ||
change.Property == DiscoProperty)
RequestNextFrameRendering();
base.OnPropertyChanged(change);
}
}
}

83
samples/ControlCatalog/Pages/ScreenPage.cs

@ -1,5 +1,7 @@
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@ -12,6 +14,11 @@ namespace ControlCatalog.Pages
public class ScreenPage : UserControl
{
private double _leftMost;
private double _topMost;
private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7");
private IBrush _defaultBrush = Brushes.LightGray;
private IPen _activePen = new Pen(Brushes.Black);
private IPen _defaultPen = new Pen(Brushes.DarkGray);
protected override bool BypassFlowDirectionPolicies => true;
@ -34,54 +41,88 @@ namespace ControlCatalog.Pages
var screens = w.Screens.All;
var scaling = ((IRenderRoot)w).RenderScaling;
var drawBrush = Brushes.Black;
Pen p = new Pen(drawBrush);
var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling)));
double maxBottom = 0;
foreach (Screen screen in screens)
for (int i = 0; i<screens.Count; i++ )
{
var screen = screens[i];
if (screen.Bounds.X / 10f < _leftMost)
{
_leftMost = screen.Bounds.X / 10f;
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
return;
}
if (screen.Bounds.Y / 10f < _topMost)
{
_topMost = screen.Bounds.Y / 10f;
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
return;
}
bool primary = screen.IsPrimary;
bool active = screen.Equals(activeScreen);
Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f, screen.Bounds.Width / 10f,
Rect boundsRect = new Rect(screen.Bounds.X / 10f + Math.Abs(_leftMost), screen.Bounds.Y / 10f+Math.Abs(_topMost), screen.Bounds.Width / 10f,
screen.Bounds.Height / 10f);
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f, screen.WorkingArea.Width / 10f,
Rect workingAreaRect = new Rect(screen.WorkingArea.X / 10f + Math.Abs(_leftMost), screen.WorkingArea.Y / 10f+Math.Abs(_topMost), screen.WorkingArea.Width / 10f,
screen.WorkingArea.Height / 10f);
context.DrawRectangle(p, boundsRect);
context.DrawRectangle(p, workingAreaRect);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, boundsRect);
context.DrawRectangle(primary ? _primaryBrush : _defaultBrush, active ? _activePen : _defaultPen, workingAreaRect);
var identifier = CreateScreenIdentifier((i+1).ToString(), primary);
var center = boundsRect.Center - new Point(identifier.Width / 2.0f, identifier.Height / 2.0f);
context.DrawText(identifier, center);
maxBottom = Math.Max(maxBottom, boundsRect.Bottom);
}
var formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height));
double currentHeight = maxBottom;
formattedText =
CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 20));
for(int i = 0; i< screens.Count; i++)
{
var screen = screens[i];
var formattedText = CreateFormattedText($"Screen {i+1}", 18);
context.DrawText(formattedText, new Point(0, currentHeight));
currentHeight += 25;
formattedText = CreateFormattedText($"Bounds: {screen.Bounds.Width}:{screen.Bounds.Height}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"WorkArea: {screen.WorkingArea.Width}:{screen.WorkingArea.Height}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"Scaling: {screen.Scaling * 100}%");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 40));
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText($"IsPrimary: {screen.IsPrimary}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 60));
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 20;
formattedText = CreateFormattedText( $"Current: {screen.Equals(activeScreen)}");
context.DrawText(formattedText, new Point(15, currentHeight));
currentHeight += 30;
formattedText =
CreateFormattedText(
$"Current: {screen.Equals(w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))))}");
context.DrawText(formattedText, boundsRect.Position.WithY(boundsRect.Size.Height + 80));
}
context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f, w.Bounds.Width / 10, w.Bounds.Height / 10));
context.DrawRectangle(_activePen, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f+Math.Abs(_topMost), w.Bounds.Width / 10, w.Bounds.Height / 10));
}
private static FormattedText CreateFormattedText(string textToFormat)
private static FormattedText CreateFormattedText(string textToFormat, double size = 12)
{
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
Typeface.Default, 12, Brushes.Green);
Typeface.Default, size, Brushes.Green);
}
private static FormattedText CreateScreenIdentifier(string textToFormat, bool primary)
{
return new FormattedText(textToFormat, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 20, primary ? Brushes.White : Brushes.Black);
}
}
}

8
samples/GpuInterop/App.axaml

@ -0,0 +1,8 @@
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GpuInterop.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

22
samples/GpuInterop/App.axaml.cs

@ -0,0 +1,22 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace GpuInterop
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
{
desktopLifetime.MainWindow = new MainWindow();
}
}
}
}

147
samples/GpuInterop/D3DDemo/D3D11DemoControl.cs

@ -0,0 +1,147 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
using SharpDX;
using SharpDX.Direct2D1;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.Mathematics.Interop;
using Buffer = SharpDX.Direct3D11.Buffer;
using DeviceContext = SharpDX.Direct2D1.DeviceContext;
using DxgiFactory1 = SharpDX.DXGI.Factory1;
using Matrix = SharpDX.Matrix;
using D3DDevice = SharpDX.Direct3D11.Device;
using DxgiResource = SharpDX.DXGI.Resource;
using FeatureLevel = SharpDX.Direct3D.FeatureLevel;
using Vector3 = SharpDX.Vector3;
namespace GpuInterop.D3DDemo;
public class D3D11DemoControl : DrawingSurfaceDemoBase
{
private D3DDevice _device;
private D3D11Swapchain _swapchain;
private SharpDX.Direct3D11.DeviceContext _context;
private Matrix _view;
private PixelSize _lastSize;
private Texture2D _depthBuffer;
private DepthStencilView _depthView;
private Matrix _proj;
private Buffer _constantBuffer;
private Stopwatch _st = Stopwatch.StartNew();
protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor,
CompositionDrawingSurface surface, ICompositionGpuInterop interop)
{
if (interop?.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes
.D3D11TextureGlobalSharedHandle) != true)
return (false, "DXGI shared handle import is not supported by the current graphics backend");
var factory = new DxgiFactory1();
using var adapter = factory.GetAdapter1(0);
_device = new D3DDevice(adapter, DeviceCreationFlags.None, new[]
{
FeatureLevel.Level_12_1,
FeatureLevel.Level_12_0,
FeatureLevel.Level_11_1,
FeatureLevel.Level_11_0,
FeatureLevel.Level_10_0,
FeatureLevel.Level_9_3,
FeatureLevel.Level_9_2,
FeatureLevel.Level_9_1,
});
_swapchain = new D3D11Swapchain(_device, interop, surface);
_context = _device.ImmediateContext;
_constantBuffer = D3DContent.CreateMesh(_device);
_view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY);
return (true, $"D3D11 ({_device.FeatureLevel}) {adapter.Description1.Description}");
}
protected override void FreeGraphicsResources()
{
_swapchain.DisposeAsync();
_swapchain = null!;
Utilities.Dispose(ref _depthView);
Utilities.Dispose(ref _depthBuffer);
Utilities.Dispose(ref _constantBuffer);
Utilities.Dispose(ref _context);
Utilities.Dispose(ref _device);
}
protected override bool SupportsDisco => true;
protected override void RenderFrame(PixelSize pixelSize)
{
if (pixelSize == default)
return;
if (pixelSize != _lastSize)
Resize(pixelSize);
using (_swapchain.BeginDraw(pixelSize, out var renderView))
{
_device.ImmediateContext.OutputMerger.SetTargets(_depthView, renderView);
var viewProj = Matrix.Multiply(_view, _proj);
var context = _device.ImmediateContext;
var now = _st.Elapsed.TotalSeconds * 5;
var scaleX = (float)(1f + Disco * (Math.Sin(now) + 1) / 6);
var scaleY = (float)(1f + Disco * (Math.Cos(now) + 1) / 8);
var colorOff =(float) (Math.Sin(now) + 1) / 2 * Disco;
// Clear views
context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0);
context.ClearRenderTargetView(renderView,
new RawColor4(1 - colorOff, colorOff, (float)0.5 + colorOff / 2, 1));
var ypr = Matrix4x4.CreateFromYawPitchRoll(Yaw, Pitch, Roll);
// Update WorldViewProj Matrix
var worldViewProj = Matrix.RotationX((float)Yaw) * Matrix.RotationY((float)Pitch)
* Matrix.RotationZ((float)Roll)
* Matrix.Scaling(new Vector3(scaleX, scaleY, 1))
* viewProj;
worldViewProj.Transpose();
context.UpdateSubresource(ref worldViewProj, _constantBuffer);
// Draw the cube
context.Draw(36, 0);
_context.Flush();
}
}
private void Resize(PixelSize size)
{
Utilities.Dispose(ref _depthBuffer);
_depthBuffer = new Texture2D(_device,
new Texture2DDescription()
{
Format = Format.D32_Float_S8X24_UInt,
ArraySize = 1,
MipLevels = 1,
Width = (int)size.Width,
Height = (int)size.Height,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Default,
BindFlags = BindFlags.DepthStencil,
CpuAccessFlags = CpuAccessFlags.None,
OptionFlags = ResourceOptionFlags.None
});
Utilities.Dispose(ref _depthView);
_depthView = new DepthStencilView(_device, _depthBuffer);
// Setup targets and viewport for rendering
_device.ImmediateContext.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f));
// Setup new projection matrix with correct aspect ratio
_proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f);
}
}

110
samples/GpuInterop/D3DDemo/D3D11Swapchain.cs

@ -0,0 +1,110 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using DxgiFactory1 = SharpDX.DXGI.Factory1;
using D3DDevice = SharpDX.Direct3D11.Device;
using DxgiResource = SharpDX.DXGI.Resource;
namespace GpuInterop.D3DDemo;
class D3D11Swapchain : SwapchainBase<D3D11SwapchainImage>
{
private readonly D3DDevice _device;
public D3D11Swapchain(D3DDevice device, ICompositionGpuInterop interop, CompositionDrawingSurface target)
: base(interop, target)
{
_device = device;
}
protected override D3D11SwapchainImage CreateImage(PixelSize size) => new(_device, size, Interop, Target);
public IDisposable BeginDraw(PixelSize size, out RenderTargetView view)
{
var rv = BeginDrawCore(size, out var image);
view = image.RenderTargetView;
return rv;
}
}
public class D3D11SwapchainImage : ISwapchainImage
{
public PixelSize Size { get; }
private readonly ICompositionGpuInterop _interop;
private readonly CompositionDrawingSurface _target;
private readonly Texture2D _texture;
private readonly KeyedMutex _mutex;
private readonly IntPtr _handle;
private PlatformGraphicsExternalImageProperties _properties;
private ICompositionImportedGpuImage? _imported;
public Task? LastPresent { get; private set; }
public RenderTargetView RenderTargetView { get; }
public D3D11SwapchainImage(D3DDevice device, PixelSize size,
ICompositionGpuInterop interop,
CompositionDrawingSurface target)
{
Size = size;
_interop = interop;
_target = target;
_texture = new Texture2D(device,
new Texture2DDescription
{
Format = Format.R8G8B8A8_UNorm,
Width = size.Width,
Height = size.Height,
ArraySize = 1,
MipLevels = 1,
SampleDescription = new SampleDescription { Count = 1, Quality = 0 },
CpuAccessFlags = default,
OptionFlags = ResourceOptionFlags.SharedKeyedmutex,
BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource
});
_mutex = _texture.QueryInterface<KeyedMutex>();
using (var res = _texture.QueryInterface<DxgiResource>())
_handle = res.SharedHandle;
_properties = new PlatformGraphicsExternalImageProperties
{
Width = size.Width, Height = size.Height, Format = PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm
};
RenderTargetView = new RenderTargetView(device, _texture);
}
public void BeginDraw()
{
_mutex.Acquire(0, int.MaxValue);
}
public void Present()
{
_mutex.Release(1);
_imported ??= _interop.ImportImage(
new PlatformHandle(_handle, KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle),
_properties);
LastPresent = _target.UpdateWithKeyedMutexAsync(_imported, 1, 0);
}
public async ValueTask DisposeAsync()
{
if (LastPresent != null)
try
{
await LastPresent;
}
catch
{
// Ignore
}
RenderTargetView.Dispose();
_mutex.Dispose();
_texture.Dispose();
}
}

110
samples/GpuInterop/D3DDemo/D3DContent.cs

@ -0,0 +1,110 @@
using SharpDX;
using SharpDX.D3DCompiler;
using SharpDX.Direct3D;
using System;
using System.Linq;
using System.Threading.Tasks;
using SharpDX.Direct2D1;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using SharpDX.Mathematics.Interop;
using Buffer = SharpDX.Direct3D11.Buffer;
using DeviceContext = SharpDX.Direct2D1.DeviceContext;
using DxgiFactory1 = SharpDX.DXGI.Factory1;
using Matrix = SharpDX.Matrix;
using D3DDevice = SharpDX.Direct3D11.Device;
using DxgiResource = SharpDX.DXGI.Resource;
using FeatureLevel = SharpDX.Direct3D.FeatureLevel;
using InputElement = SharpDX.Direct3D11.InputElement;
namespace GpuInterop.D3DDemo;
public class D3DContent
{
public static Buffer CreateMesh(D3DDevice device)
{
// Compile Vertex and Pixel shaders
var vertexShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "VS", "vs_4_0");
var vertexShader = new VertexShader(device, vertexShaderByteCode);
var pixelShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "PS", "ps_4_0");
var pixelShader = new PixelShader(device, pixelShaderByteCode);
var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode);
var inputElements = new[]
{
new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0),
new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0)
};
// Layout from VertexShader input signature
var layout = new InputLayout(
device,
signature,
inputElements);
// Instantiate Vertex buffer from vertex data
using var vertices = Buffer.Create(
device,
BindFlags.VertexBuffer,
new[]
{
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK
new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom
new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left
new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f),
new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right
new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f),
});
// Create Constant Buffer
var constantBuffer = new Buffer(device, Utilities.SizeOf<Matrix>(), ResourceUsage.Default,
BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0);
var context = device.ImmediateContext;
// Prepare All the stages
context.InputAssembler.InputLayout = layout;
context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
context.InputAssembler.SetVertexBuffers(0,
new VertexBufferBinding(vertices, Utilities.SizeOf<Vector4>() * 2, 0));
context.VertexShader.SetConstantBuffer(0, constantBuffer);
context.VertexShader.Set(vertexShader);
context.PixelShader.Set(pixelShader);
return constantBuffer;
}
}

47
samples/GpuInterop/D3DDemo/MiniCube.fx

@ -0,0 +1,47 @@
// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
struct VS_IN
{
float4 pos : POSITION;
float4 col : COLOR;
};
struct PS_IN
{
float4 pos : SV_POSITION;
float4 col : COLOR;
};
float4x4 worldViewProj;
PS_IN VS( VS_IN input )
{
PS_IN output = (PS_IN)0;
output.pos = mul(input.pos, worldViewProj);
output.col = input.col;
return output;
}
float4 PS( PS_IN input ) : SV_Target
{
return input.col;
}

141
samples/GpuInterop/DrawingSurfaceDemoBase.cs

@ -0,0 +1,141 @@
using System;
using System.Numerics;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree;
namespace GpuInterop;
public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo
{
private CompositionSurfaceVisual? _visual;
private Compositor? _compositor;
private Action _update;
private string _info;
private bool _updateQueued;
private bool _initialized;
protected CompositionDrawingSurface Surface { get; private set; }
public DrawingSurfaceDemoBase()
{
_update = UpdateFrame;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
Initialize();
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_initialized)
FreeGraphicsResources();
_initialized = false;
base.OnDetachedFromLogicalTree(e);
}
async void Initialize()
{
try
{
var selfVisual = ElementComposition.GetElementVisual(this)!;
_compositor = selfVisual.Compositor;
Surface = _compositor.CreateDrawingSurface();
_visual = _compositor.CreateSurfaceVisual();
_visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height);
_visual.Surface = Surface;
ElementComposition.SetElementChildVisual(this, _visual);
var (res, info) = await DoInitialize(_compositor, Surface);
_info = info;
if (ParentControl != null)
ParentControl.Info = info;
_initialized = res;
QueueNextFrame();
}
catch (Exception e)
{
if (ParentControl != null)
ParentControl.Info = e.ToString();
}
}
void UpdateFrame()
{
_updateQueued = false;
var root = this.GetVisualRoot();
if (root == null)
return;
_visual!.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height);
var size = PixelSize.FromSize(Bounds.Size, root.RenderScaling);
RenderFrame(size);
if (SupportsDisco && Disco > 0)
QueueNextFrame();
}
void QueueNextFrame()
{
if (_initialized && !_updateQueued && _compositor != null)
{
_updateQueued = true;
_compositor?.RequestCompositionUpdate(_update);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if(change.Property == BoundsProperty)
QueueNextFrame();
base.OnPropertyChanged(change);
}
async Task<(bool success, string info)> DoInitialize(Compositor compositor,
CompositionDrawingSurface compositionDrawingSurface)
{
var interop = await compositor.TryGetCompositionGpuInterop();
if (interop == null)
return (false, "Compositor doesn't support interop for the current backend");
return InitializeGraphicsResources(compositor, compositionDrawingSurface, interop);
}
protected abstract (bool success, string info) InitializeGraphicsResources(Compositor compositor,
CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop);
protected abstract void FreeGraphicsResources();
protected abstract void RenderFrame(PixelSize pixelSize);
protected virtual bool SupportsDisco => false;
public void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco)
{
ParentControl = parent;
if (ParentControl != null)
{
ParentControl.Info = _info;
ParentControl.DiscoVisible = true;
}
Yaw = yaw;
Pitch = pitch;
Roll = roll;
Disco = disco;
QueueNextFrame();
}
public GpuDemo? ParentControl { get; private set; }
public float Disco { get; private set; }
public float Roll { get; private set; }
public float Pitch { get; private set; }
public float Yaw { get; private set; }
}

32
samples/GpuInterop/GpuDemo.axaml

@ -0,0 +1,32 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="GpuInterop.GpuDemo"
xmlns:local="clr-namespace:GpuInterop;assembly=GpuInterop">
<Grid>
<ContentControl Content="{Binding $parent[local:GpuDemo].Demo}" />
<StackPanel>
<TextBlock Margin="0 40 0 0" Text="{Binding $parent[local:GpuDemo].Info}"/>
</StackPanel>
<Grid ColumnDefinitions="*,Auto" Margin="20">
<StackPanel Grid.Column="1" MinWidth="300">
<TextBlock>Yaw</TextBlock>
<Slider Value="{Binding $parent[local:GpuDemo].Yaw, Mode=TwoWay}" Maximum="10"/>
<TextBlock>Pitch</TextBlock>
<Slider Value="{Binding $parent[local:GpuDemo].Pitch, Mode=TwoWay}" Maximum="10"/>
<TextBlock>Roll</TextBlock>
<Slider Value="{Binding $parent[local:GpuDemo].Roll, Mode=TwoWay}" Maximum="10"/>
<StackPanel IsVisible="{Binding $parent[local:GpuDemo].DiscoVisible}">
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold" Foreground="#C000C0">D</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#00C090">I</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#90C000">S</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#C09000">C</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#00C090">O</TextBlock>
</StackPanel>
<Slider Value="{Binding $parent[local:GpuDemo].Disco, Mode=TwoWay}" Maximum="1"/>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</UserControl>

116
samples/GpuInterop/GpuDemo.axaml.cs

@ -0,0 +1,116 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace GpuInterop;
public class GpuDemo : UserControl
{
public GpuDemo()
{
AvaloniaXamlLoader.Load(this);
}
private float _yaw = 5;
public static readonly DirectProperty<GpuDemo, float> YawProperty =
AvaloniaProperty.RegisterDirect<GpuDemo, float>("Yaw", o => o.Yaw, (o, v) => o.Yaw = v);
public float Yaw
{
get => _yaw;
set => SetAndRaise(YawProperty, ref _yaw, value);
}
private float _pitch = 5;
public static readonly DirectProperty<GpuDemo, float> PitchProperty =
AvaloniaProperty.RegisterDirect<GpuDemo, float>("Pitch", o => o.Pitch, (o, v) => o.Pitch = v);
public float Pitch
{
get => _pitch;
set => SetAndRaise(PitchProperty, ref _pitch, value);
}
private float _roll = 5;
public static readonly DirectProperty<GpuDemo, float> RollProperty =
AvaloniaProperty.RegisterDirect<GpuDemo, float>("Roll", o => o.Roll, (o, v) => o.Roll = v);
public float Roll
{
get => _roll;
set => SetAndRaise(RollProperty, ref _roll, value);
}
private float _disco;
public static readonly DirectProperty<GpuDemo, float> DiscoProperty =
AvaloniaProperty.RegisterDirect<GpuDemo, float>("Disco", o => o.Disco, (o, v) => o.Disco = v);
public float Disco
{
get => _disco;
set => SetAndRaise(DiscoProperty, ref _disco, value);
}
private string _info = string.Empty;
public static readonly DirectProperty<GpuDemo, string> InfoProperty =
AvaloniaProperty.RegisterDirect<GpuDemo, string>("Info", o => o.Info, (o, v) => o.Info = v);
public string Info
{
get => _info;
set => SetAndRaise(InfoProperty, ref _info, value);
}
private bool _discoVisible;
public static readonly DirectProperty<GpuDemo, bool> DiscoVisibleProperty =
AvaloniaProperty.RegisterDirect<GpuDemo, bool>("DiscoVisible", o => o.DiscoVisible,
(o, v) => o._discoVisible = v);
public bool DiscoVisible
{
get => _discoVisible;
set => SetAndRaise(DiscoVisibleProperty, ref _discoVisible, value);
}
private IGpuDemo _demo;
public static readonly DirectProperty<GpuDemo, IGpuDemo> DemoProperty =
AvaloniaProperty.RegisterDirect<GpuDemo, IGpuDemo>("Demo", o => o.Demo,
(o, v) => o._demo = v);
public IGpuDemo Demo
{
get => _demo;
set => SetAndRaise(DemoProperty, ref _demo, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
if (change.Property == YawProperty
|| change.Property == PitchProperty
|| change.Property == RollProperty
|| change.Property == DiscoProperty
|| change.Property == DemoProperty
)
{
if (change.Property == DemoProperty)
((IGpuDemo)change.OldValue)?.Update(null, 0, 0, 0, 0);
_demo?.Update(this, Yaw, Pitch, Roll, Disco);
}
base.OnPropertyChanged(change);
}
}
public interface IGpuDemo
{
void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco);
}

51
samples/GpuInterop/GpuInterop.csproj

@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<UseD3DCompiler>true</UseD3DCompiler>
<UseSharpDXMathematics>true</UseSharpDXMathematics>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.ColorPicker\Avalonia.Controls.ColorPicker.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="D3DDemo\MiniCube.fx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Silk.NET.Vulkan" Version="2.16.0" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.16.0" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.16.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="VulkanDemo\Assets\Shaders\Assets" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.Base\Rendering\SwapchainBase.cs" />
<None Remove="VulkanDemo\Assets\Shaders\frag.spirv" />
<EmbeddedResource Include="VulkanDemo\Assets\Shaders\frag.spirv" />
<None Remove="VulkanDemo\Assets\Shaders\vert.spirv" />
<EmbeddedResource Include="VulkanDemo\Assets\Shaders\vert.spirv" />
<EmbeddedResource Include="../ControlCatalog/Pages/teapot.bin" />
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\SharpDX.props" />
</Project>

13
samples/GpuInterop/MainWindow.axaml

@ -0,0 +1,13 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:gpuInterop="clr-namespace:GpuInterop"
xmlns:d3DDemo="clr-namespace:GpuInterop.D3DDemo"
xmlns:vulkanDemo="clr-namespace:GpuInterop.VulkanDemo"
x:Class="GpuInterop.MainWindow">
<gpuInterop:GpuDemo>
<gpuInterop:GpuDemo.Demo>
<!--<d3DDemo:D3D11DemoControl/>-->
<vulkanDemo:VulkanDemoControl/>
</gpuInterop:GpuDemo.Demo>
</gpuInterop:GpuDemo>
</Window>

21
samples/GpuInterop/MainWindow.axaml.cs

@ -0,0 +1,21 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace GpuInterop
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.AttachDevTools();
this.Renderer.DrawFps = true;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

15
samples/GpuInterop/Program.cs

@ -0,0 +1,15 @@
global using System.Reactive.Disposables;
using Avalonia;
namespace GpuInterop
{
public class Program
{
static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
}

12
samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile

@ -0,0 +1,12 @@
#!/usr/bin/make -f
all: vert.spirv frag.spirv
.PHONY: all
vert.spirv: vert.glsl
glslc -fshader-stage=vert vert.glsl -o vert.spirv
frag.spirv: frag.glsl
glslc -fshader-stage=frag frag.glsl -o frag.spirv

42
samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl

@ -0,0 +1,42 @@
#version 450
layout(location = 0) in vec3 FragPos;
layout(location = 1) in vec3 VecPos;
layout(location = 2) in vec3 Normal;
layout(push_constant) uniform constants{
layout(offset = 0) float maxY;
layout(offset = 4) float minY;
layout(offset = 8) float time;
layout(offset = 12) float disco;
};
layout(location = 0) out vec4 outFragColor;
void main()
{
float y = (VecPos.y - minY) / (maxY - minY);
float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + time * 40.0 + y * 50.0);
float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - time * 20.0 - y * 30.0);
vec3 discoColor = vec3(
0.5 + abs(0.5 - y) * cos(time * 10.0),
0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)),
0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0))));
vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25);
objectColor = objectColor * (1.0 - disco) + discoColor * disco;
float ambientStrength = 0.3;
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 lightPos = vec3(maxY * 2.0, maxY * 2.0, maxY * 2.0);
vec3 ambient = ambientStrength * lightColor;
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
outFragColor = vec4(result, 1.0);
}

BIN
samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv

Binary file not shown.

36
samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl

@ -0,0 +1,36 @@
#version 450
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 0) out vec3 FragPos;
layout(location = 1) out vec3 VecPos;
layout(location = 2) out vec3 Normal;
layout(push_constant) uniform constants{
float maxY;
float minY;
float time;
float disco;
mat4 model;
};
layout(binding = 0) uniform UniformBufferObject {
mat4 projection;
} ubo;
void main()
{
float discoScale = sin(time * 10.0) / 10.0;
float distortionX = 1.0 + disco * cos(time * 20.0) / 10.0;
float scale = 1.0 + disco * discoScale;
vec3 scaledPos = aPos;
scaledPos.x = scaledPos.x * distortionX;
scaledPos *= scale;
gl_Position = ubo.projection * model * vec4(scaledPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
VecPos = aPos;
Normal = normalize(vec3(model * vec4(aNormal, 1.0)));
}

BIN
samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv

Binary file not shown.

47
samples/GpuInterop/VulkanDemo/ByteString.cs

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
namespace GpuInterop.VulkanDemo;
unsafe class ByteString : IDisposable
{
public IntPtr Pointer { get; }
public ByteString(string s)
{
Pointer = Marshal.StringToHGlobalAnsi(s);
}
public void Dispose()
{
Marshal.FreeHGlobal(Pointer);
}
public static implicit operator byte*(ByteString h) => (byte*)h.Pointer;
}
unsafe class ByteStringList : IDisposable
{
private List<ByteString> _inner;
private byte** _ptr;
public ByteStringList(IEnumerable<string> items)
{
_inner = items.Select(x => new ByteString(x)).ToList();
_ptr = (byte**)Marshal.AllocHGlobal(IntPtr.Size * _inner.Count + 1);
for (var c = 0; c < _inner.Count; c++)
_ptr[c] = (byte*)_inner[c].Pointer;
}
public int Count => _inner.Count;
public uint UCount => (uint)_inner.Count;
public void Dispose()
{
Marshal.FreeHGlobal(new IntPtr(_ptr));
}
public static implicit operator byte**(ByteStringList h) => h._ptr;
}

54
samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs

@ -0,0 +1,54 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using D3DDevice = SharpDX.Direct3D11.Device;
using DxgiFactory1 = SharpDX.DXGI.Factory1;
namespace GpuInterop.VulkanDemo;
public class D3DMemoryHelper
{
public static D3DDevice CreateDeviceByLuid(Span<byte> luid)
{
var factory = new DxgiFactory1();
var longLuid = MemoryMarshal.Cast<byte, long>(luid)[0];
for (var c = 0; c < factory.GetAdapterCount1(); c++)
{
using var adapter = factory.GetAdapter1(0);
if (adapter.Description1.Luid != longLuid)
continue;
return new D3DDevice(adapter, DeviceCreationFlags.None,
new[]
{
FeatureLevel.Level_12_1, FeatureLevel.Level_12_0, FeatureLevel.Level_11_1,
FeatureLevel.Level_11_0, FeatureLevel.Level_10_0, FeatureLevel.Level_9_3,
FeatureLevel.Level_9_2, FeatureLevel.Level_9_1,
});
}
throw new ArgumentException("Device with the corresponding LUID not found");
}
public static Texture2D CreateMemoryHandle(D3DDevice device, PixelSize size, Silk.NET.Vulkan.Format format)
{
if (format != Silk.NET.Vulkan.Format.R8G8B8A8Unorm)
throw new ArgumentException("Not supported format");
return new Texture2D(device,
new Texture2DDescription
{
Format = Format.R8G8B8A8_UNorm,
Width = size.Width,
Height = size.Height,
ArraySize = 1,
MipLevels = 1,
SampleDescription = new SampleDescription { Count = 1, Quality = 0 },
CpuAccessFlags = default,
OptionFlags = ResourceOptionFlags.SharedKeyedmutex,
BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource
});
}
}

80
samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs

@ -0,0 +1,80 @@
using System;
using System.Runtime.CompilerServices;
using Silk.NET.Vulkan;
using SilkNetDemo;
using Buffer = Silk.NET.Vulkan.Buffer;
using SystemBuffer = System.Buffer;
namespace GpuInterop.VulkanDemo;
static class VulkanBufferHelper
{
public unsafe static void AllocateBuffer<T>(VulkanContext vk,
BufferUsageFlags bufferUsageFlags,
out Buffer buffer, out DeviceMemory memory,
Span<T> initialData) where T:unmanaged
{
var api = vk.Api;
var device = vk.Device;
var size = Unsafe.SizeOf<T>() * initialData.Length;
var bufferInfo = new BufferCreateInfo()
{
SType = StructureType.BufferCreateInfo,
Size = (ulong)size,
Usage = bufferUsageFlags,
SharingMode = SharingMode.Exclusive
};
api.CreateBuffer(device, bufferInfo, null, out buffer).ThrowOnError();
api.GetBufferMemoryRequirements(device, buffer, out var memoryRequirements);
var physicalDevice = vk.PhysicalDevice;
var memoryAllocateInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = memoryRequirements.Size,
MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api,
physicalDevice,
memoryRequirements.MemoryTypeBits,
MemoryPropertyFlags.MemoryPropertyHostCoherentBit |
MemoryPropertyFlags.MemoryPropertyHostVisibleBit)
};
api.AllocateMemory(device, memoryAllocateInfo, null, out memory).ThrowOnError();
api.BindBufferMemory(device, buffer, memory, 0);
UpdateBufferMemory(vk, memory, initialData);
}
public static unsafe void UpdateBufferMemory<T>(VulkanContext vk, DeviceMemory memory,
Span<T> data) where T : unmanaged
{
var api = vk.Api;
var device = vk.Device;
var size = data.Length * Unsafe.SizeOf<T>();
void* pointer = null;
api.MapMemory(device, memory, 0, (ulong)size, 0, ref pointer);
data.CopyTo(new Span<T>(pointer, size));
api.UnmapMemory(device, memory);
}
private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits,
MemoryPropertyFlags flags)
{
api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties);
for (var i = 0; i < properties.MemoryTypeCount; i++)
{
var type = properties.MemoryTypes[i];
if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i;
}
return -1;
}
}

224
samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs

@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using Avalonia.Input;
using Silk.NET.Vulkan;
using SilkNetDemo;
namespace Avalonia.Vulkan
{
public class VulkanCommandBufferPool : IDisposable
{
private readonly Vk _api;
private readonly Device _device;
private readonly Queue _queue;
private readonly CommandPool _commandPool;
private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new();
private object _lock = new object();
public unsafe VulkanCommandBufferPool(Vk api, Device device, Queue queue, uint queueFamilyIndex)
{
_api = api;
_device = device;
_queue = queue;
var commandPoolCreateInfo = new CommandPoolCreateInfo
{
SType = StructureType.CommandPoolCreateInfo,
Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit,
QueueFamilyIndex = queueFamilyIndex
};
_api.CreateCommandPool(_device, commandPoolCreateInfo, null, out _commandPool)
.ThrowOnError();
}
public unsafe void Dispose()
{
lock (_lock)
{
FreeUsedCommandBuffers();
_api.DestroyCommandPool(_device, _commandPool, null);
}
}
private CommandBuffer AllocateCommandBuffer()
{
var commandBufferAllocateInfo = new CommandBufferAllocateInfo
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _commandPool,
CommandBufferCount = 1,
Level = CommandBufferLevel.Primary
};
lock (_lock)
{
_api.AllocateCommandBuffers(_device, commandBufferAllocateInfo, out var commandBuffer);
return commandBuffer;
}
}
public VulkanCommandBuffer CreateCommandBuffer()
{
return new(_api, _device, _queue, this);
}
public void FreeUsedCommandBuffers()
{
lock (_lock)
{
foreach (var usedCommandBuffer in _usedCommandBuffers) usedCommandBuffer.Dispose();
_usedCommandBuffers.Clear();
}
}
private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer)
{
lock (_lock)
{
_usedCommandBuffers.Add(commandBuffer);
}
}
public class VulkanCommandBuffer : IDisposable
{
private readonly VulkanCommandBufferPool _commandBufferPool;
private readonly Vk _api;
private readonly Device _device;
private readonly Queue _queue;
private readonly Fence _fence;
private bool _hasEnded;
private bool _hasStarted;
public IntPtr Handle => InternalHandle.Handle;
internal CommandBuffer InternalHandle { get; }
internal unsafe VulkanCommandBuffer(Vk api, Device device, Queue queue, VulkanCommandBufferPool commandBufferPool)
{
_api = api;
_device = device;
_queue = queue;
_commandBufferPool = commandBufferPool;
InternalHandle = _commandBufferPool.AllocateCommandBuffer();
var fenceCreateInfo = new FenceCreateInfo()
{
SType = StructureType.FenceCreateInfo,
Flags = FenceCreateFlags.FenceCreateSignaledBit
};
api.CreateFence(device, fenceCreateInfo, null, out _fence);
}
public unsafe void Dispose()
{
_api.WaitForFences(_device, 1, _fence, true, ulong.MaxValue);
lock (_commandBufferPool._lock)
{
_api.FreeCommandBuffers(_device, _commandBufferPool._commandPool, 1, InternalHandle);
}
_api.DestroyFence(_device, _fence, null);
}
public void BeginRecording()
{
if (!_hasStarted)
{
_hasStarted = true;
var beginInfo = new CommandBufferBeginInfo
{
SType = StructureType.CommandBufferBeginInfo,
Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit
};
_api.BeginCommandBuffer(InternalHandle, beginInfo);
}
}
public void EndRecording()
{
if (_hasStarted && !_hasEnded)
{
_hasEnded = true;
_api.EndCommandBuffer(InternalHandle);
}
}
public void Submit()
{
Submit(null, null, null, _fence);
}
public class KeyedMutexSubmitInfo
{
public ulong? AcquireKey { get; set; }
public ulong? ReleaseKey { get; set; }
public DeviceMemory DeviceMemory { get; set; }
}
public unsafe void Submit(
ReadOnlySpan<Semaphore> waitSemaphores,
ReadOnlySpan<PipelineStageFlags> waitDstStageMask = default,
ReadOnlySpan<Semaphore> signalSemaphores = default,
Fence? fence = null,
KeyedMutexSubmitInfo keyedMutex = null)
{
EndRecording();
if (!fence.HasValue)
fence = _fence;
ulong acquireKey = keyedMutex?.AcquireKey ?? 0, releaseKey = keyedMutex?.ReleaseKey ?? 0;
DeviceMemory devMem = keyedMutex?.DeviceMemory ?? default;
uint timeout = uint.MaxValue;
Win32KeyedMutexAcquireReleaseInfoKHR mutex = default;
if (keyedMutex != null)
mutex = new Win32KeyedMutexAcquireReleaseInfoKHR
{
SType = StructureType.Win32KeyedMutexAcquireReleaseInfoKhr,
AcquireCount = keyedMutex.AcquireKey.HasValue ? 1u : 0u,
ReleaseCount = keyedMutex.ReleaseKey.HasValue ? 1u : 0u,
PAcquireKeys = &acquireKey,
PReleaseKeys = &releaseKey,
PAcquireSyncs = &devMem,
PReleaseSyncs = &devMem,
PAcquireTimeouts = &timeout
};
fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores)
{
fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask)
{
var commandBuffer = InternalHandle;
var submitInfo = new SubmitInfo
{
PNext = keyedMutex != null ? &mutex : null,
SType = StructureType.SubmitInfo,
WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0,
PWaitSemaphores = pWaitSemaphores,
PWaitDstStageMask = pWaitDstStageMask,
CommandBufferCount = 1,
PCommandBuffers = &commandBuffer,
SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0,
PSignalSemaphores = pSignalSemaphores,
};
_api.ResetFences(_device, 1, fence.Value);
_api.QueueSubmit(_queue, 1, submitInfo, fence.Value);
}
}
_commandBufferPool.DisposeCommandBuffer(this);
}
}
}
}

829
samples/GpuInterop/VulkanDemo/VulkanContent.cs

@ -0,0 +1,829 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Threading;
using Silk.NET.Vulkan;
using SilkNetDemo;
using Buffer = System.Buffer;
using Image = Silk.NET.Vulkan.Image;
namespace GpuInterop.VulkanDemo;
unsafe class VulkanContent : IDisposable
{
private readonly VulkanContext _context;
private ShaderModule _vertShader;
private ShaderModule _fragShader;
private PipelineLayout _pipelineLayout;
private RenderPass _renderPass;
private Pipeline _pipeline;
private DescriptorSetLayout _descriptorSetLayout;
private Silk.NET.Vulkan.Buffer _vertexBuffer;
private DeviceMemory _vertexBufferMemory;
private Silk.NET.Vulkan.Buffer _indexBuffer;
private DeviceMemory _indexBufferMemory;
private Silk.NET.Vulkan.Buffer _uniformBuffer;
private DeviceMemory _uniformBufferMemory;
private Framebuffer _framebuffer;
private Image _depthImage;
private DeviceMemory _depthImageMemory;
private ImageView _depthImageView;
public VulkanContent(VulkanContext context)
{
_context = context;
var name = typeof(VulkanContent).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin"));
using (var sr = new BinaryReader(typeof(VulkanContent).Assembly.GetManifestResourceStream(name)))
{
var buf = new byte[sr.ReadInt32()];
sr.Read(buf, 0, buf.Length);
var points = new float[buf.Length / 4];
Buffer.BlockCopy(buf, 0, points, 0, buf.Length);
buf = new byte[sr.ReadInt32()];
sr.Read(buf, 0, buf.Length);
_indices = new ushort[buf.Length / 2];
Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length);
_points = new Vertex[points.Length / 3];
for (var primitive = 0; primitive < points.Length / 3; primitive++)
{
var srci = primitive * 3;
_points[primitive] = new Vertex
{
Position = new Vector3(points[srci], points[srci + 1], points[srci + 2])
};
}
for (int i = 0; i < _indices.Length; i += 3)
{
Vector3 a = _points[_indices[i]].Position;
Vector3 b = _points[_indices[i + 1]].Position;
Vector3 c = _points[_indices[i + 2]].Position;
var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b));
_points[_indices[i]].Normal += normal;
_points[_indices[i + 1]].Normal += normal;
_points[_indices[i + 2]].Normal += normal;
}
for (int i = 0; i < _points.Length; i++)
{
_points[i].Normal = Vector3.Normalize(_points[i].Normal);
_maxY = Math.Max(_maxY, _points[i].Position.Y);
_minY = Math.Min(_minY, _points[i].Position.Y);
}
}
var api = _context.Api;
var device = _context.Device;
var vertShaderData = GetShader(false);
var fragShaderData = GetShader(true);
fixed (byte* ptr = vertShaderData)
{
var shaderCreateInfo = new ShaderModuleCreateInfo()
{
SType = StructureType.ShaderModuleCreateInfo,
CodeSize = (nuint)vertShaderData.Length,
PCode = (uint*)ptr,
};
api.CreateShaderModule(device, shaderCreateInfo, null, out _vertShader);
}
fixed (byte* ptr = fragShaderData)
{
var shaderCreateInfo = new ShaderModuleCreateInfo()
{
SType = StructureType.ShaderModuleCreateInfo,
CodeSize = (nuint)fragShaderData.Length,
PCode = (uint*)ptr,
};
api.CreateShaderModule(device, shaderCreateInfo, null, out _fragShader);
}
CreateBuffers();
}
private byte[] GetShader(bool fragment)
{
var name = typeof(VulkanContent).Assembly.GetManifestResourceNames()
.First(x => x.Contains((fragment ? "frag" : "vert") + ".spirv"));
using (var sr = typeof(VulkanContent).Assembly.GetManifestResourceStream(name))
{
using (var mem = new MemoryStream())
{
sr.CopyTo(mem);
return mem.ToArray();
}
}
}
private PixelSize? _previousImageSize = PixelSize.Empty;
public void Render(VulkanImage image,
double yaw, double pitch, double roll, double disco)
{
var api = _context.Api;
if (image.Size != _previousImageSize)
CreateTemporalObjects(image.Size);
_previousImageSize = image.Size;
var model = Matrix4x4.CreateFromYawPitchRoll((float)yaw, (float)pitch, (float)roll);
var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0));
var projection =
Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)image.Size.Width / image.Size.Height),
0.01f, 1000);
var vertexConstant = new VertextPushConstant()
{
Disco = (float)disco,
MinY = _minY,
MaxY = _maxY,
Model = model,
Time = (float)St.Elapsed.TotalSeconds
};
var commandBuffer = _context.Pool.CreateCommandBuffer();
commandBuffer.BeginRecording();
_colorAttachment.TransitionLayout(commandBuffer.InternalHandle,
ImageLayout.Undefined, AccessFlags.None,
ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentWriteBit);
var commandBufferHandle = new CommandBuffer(commandBuffer.Handle);
api.CmdSetViewport(commandBufferHandle, 0, 1,
new Viewport()
{
Width = (float)image.Size.Width,
Height = (float)image.Size.Height,
MaxDepth = 1,
MinDepth = 0,
X = 0,
Y = 0
});
var scissor = new Rect2D
{
Extent = new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height)
};
api.CmdSetScissor(commandBufferHandle, 0, 1, &scissor);
var clearColor = new ClearValue(new ClearColorValue(1, 0, 0, 0.1f), new ClearDepthStencilValue(1, 0));
var clearValues = new[] { clearColor, clearColor };
fixed (ClearValue* clearValue = clearValues)
{
var beginInfo = new RenderPassBeginInfo()
{
SType = StructureType.RenderPassBeginInfo,
RenderPass = _renderPass,
Framebuffer = _framebuffer,
RenderArea = new Rect2D(new Offset2D(0, 0), new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height)),
ClearValueCount = 2,
PClearValues = clearValue
};
api.CmdBeginRenderPass(commandBufferHandle, beginInfo, SubpassContents.Inline);
}
api.CmdBindPipeline(commandBufferHandle, PipelineBindPoint.Graphics, _pipeline);
var dset = _descriptorSet;
api.CmdBindDescriptorSets(commandBufferHandle, PipelineBindPoint.Graphics,
_pipelineLayout,0,1, &dset, null);
api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.ShaderStageVertexBit | ShaderStageFlags.FragmentBit, 0,
(uint)Marshal.SizeOf<VertextPushConstant>(), &vertexConstant);
api.CmdBindVertexBuffers(commandBufferHandle, 0, 1, _vertexBuffer, 0);
api.CmdBindIndexBuffer(commandBufferHandle, _indexBuffer, 0, IndexType.Uint16);
api.CmdDrawIndexed(commandBufferHandle, (uint)_indices.Length, 1, 0, 0, 0);
api.CmdEndRenderPass(commandBufferHandle);
_colorAttachment.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit);
image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit);
var srcBlitRegion = new ImageBlit
{
SrcOffsets = new ImageBlit.SrcOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1),
},
DstOffsets = new ImageBlit.DstOffsetsBuffer
{
Element0 = new Offset3D(0, 0, 0),
Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1),
},
SrcSubresource =
new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ImageAspectColorBit,
BaseArrayLayer = 0,
LayerCount = 1,
MipLevel = 0
},
DstSubresource = new ImageSubresourceLayers
{
AspectMask = ImageAspectFlags.ImageAspectColorBit,
BaseArrayLayer = 0,
LayerCount = 1,
MipLevel = 0
}
};
api.CmdBlitImage(commandBuffer.InternalHandle, _colorAttachment.InternalHandle.Value,
ImageLayout.TransferSrcOptimal,
image.InternalHandle.Value, ImageLayout.TransferDstOptimal, 1, srcBlitRegion, Filter.Linear);
commandBuffer.Submit();
}
public unsafe void Dispose()
{
if (_isInit)
{
var api = _context.Api;
var device = _context.Device;
DestroyTemporalObjects();
api.DestroyShaderModule(device, _vertShader, null);
api.DestroyShaderModule(device, _fragShader, null);
api.DestroyBuffer(device, _vertexBuffer, null);
api.FreeMemory(device, _vertexBufferMemory, null);
api.DestroyBuffer(device, _indexBuffer, null);
api.FreeMemory(device, _indexBufferMemory, null);
}
_isInit = false;
}
public unsafe void DestroyTemporalObjects()
{
if (_isInit)
{
if (_renderPass.Handle != 0)
{
var api = _context.Api;
var device = _context.Device;
api.FreeDescriptorSets(_context.Device, _context.DescriptorPool, new[] { _descriptorSet });
api.DestroyImageView(device, _depthImageView, null);
api.DestroyImage(device, _depthImage, null);
api.FreeMemory(device, _depthImageMemory, null);
api.DestroyFramebuffer(device, _framebuffer, null);
api.DestroyPipeline(device, _pipeline, null);
api.DestroyPipelineLayout(device, _pipelineLayout, null);
api.DestroyRenderPass(device, _renderPass, null);
api.DestroyDescriptorSetLayout(device, _descriptorSetLayout, null);
api.DestroyBuffer(device, _uniformBuffer, null);
api.FreeMemory(device, _uniformBufferMemory, null);
_colorAttachment?.Dispose();
_colorAttachment = null;
_depthImage = default;
_depthImageView = default;
_depthImageView = default;
_framebuffer = default;
_pipeline = default;
_renderPass = default;
_pipelineLayout = default;
_descriptorSetLayout = default;
_uniformBuffer = default;
_uniformBufferMemory = default;
}
}
}
private unsafe void CreateDepthAttachment(PixelSize size)
{
var imageCreateInfo = new ImageCreateInfo
{
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.ImageType2D,
Format = Format.D32Sfloat,
Extent =
new Extent3D((uint?)size.Width,
(uint?)size.Height, 1),
MipLevels = 1,
ArrayLayers = 1,
Samples = SampleCountFlags.SampleCount1Bit,
Tiling = ImageTiling.Optimal,
Usage = ImageUsageFlags.ImageUsageDepthStencilAttachmentBit,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
Flags = ImageCreateFlags.ImageCreateMutableFormatBit
};
var api = _context.Api;
var device = _context.Device;
api
.CreateImage(device, imageCreateInfo, null, out _depthImage).ThrowOnError();
api.GetImageMemoryRequirements(device, _depthImage,
out var memoryRequirements);
var memoryAllocateInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
AllocationSize = memoryRequirements.Size,
MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api,
_context.PhysicalDevice,
memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit)
};
api.AllocateMemory(device, memoryAllocateInfo, null,
out _depthImageMemory).ThrowOnError();
api.BindImageMemory(device, _depthImage, _depthImageMemory, 0);
var componentMapping = new ComponentMapping(
ComponentSwizzle.R,
ComponentSwizzle.G,
ComponentSwizzle.B,
ComponentSwizzle.A);
var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectDepthBit,
0, 1, 0, 1);
var imageViewCreateInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = _depthImage,
ViewType = ImageViewType.ImageViewType2D,
Format = Format.D32Sfloat,
Components = componentMapping,
SubresourceRange = subresourceRange
};
api
.CreateImageView(device, imageViewCreateInfo, null, out _depthImageView)
.ThrowOnError();
}
private unsafe void CreateTemporalObjects(PixelSize size)
{
DestroyTemporalObjects();
var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0));
var projection =
Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)size.Width / size.Height),
0.01f, 1000);
_colorAttachment = new VulkanImage(_context, (uint)Format.R8G8B8A8Unorm, size, false);
CreateDepthAttachment(size);
var api = _context.Api;
var device = _context.Device;
// create renderpasses
var colorAttachment = new AttachmentDescription()
{
Format = Format.R8G8B8A8Unorm,
Samples = SampleCountFlags.SampleCount1Bit,
LoadOp = AttachmentLoadOp.Clear,
StoreOp = AttachmentStoreOp.Store,
InitialLayout = ImageLayout.Undefined,
FinalLayout = ImageLayout.ColorAttachmentOptimal,
StencilLoadOp = AttachmentLoadOp.DontCare,
StencilStoreOp = AttachmentStoreOp.DontCare
};
var depthAttachment = new AttachmentDescription()
{
Format = Format.D32Sfloat,
Samples = SampleCountFlags.SampleCount1Bit,
LoadOp = AttachmentLoadOp.Clear,
StoreOp = AttachmentStoreOp.DontCare,
InitialLayout = ImageLayout.Undefined,
FinalLayout = ImageLayout.DepthStencilAttachmentOptimal,
StencilLoadOp = AttachmentLoadOp.DontCare,
StencilStoreOp = AttachmentStoreOp.DontCare
};
var subpassDependency = new SubpassDependency()
{
SrcSubpass = Vk.SubpassExternal,
DstSubpass = 0,
SrcStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit,
SrcAccessMask = 0,
DstStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit,
DstAccessMask = AccessFlags.AccessColorAttachmentWriteBit
};
var colorAttachmentReference = new AttachmentReference()
{
Attachment = 0, Layout = ImageLayout.ColorAttachmentOptimal
};
var depthAttachmentReference = new AttachmentReference()
{
Attachment = 1, Layout = ImageLayout.DepthStencilAttachmentOptimal
};
var subpassDescription = new SubpassDescription()
{
PipelineBindPoint = PipelineBindPoint.Graphics,
ColorAttachmentCount = 1,
PColorAttachments = &colorAttachmentReference,
PDepthStencilAttachment = &depthAttachmentReference
};
var attachments = new[] { colorAttachment, depthAttachment };
fixed (AttachmentDescription* atPtr = attachments)
{
var renderPassCreateInfo = new RenderPassCreateInfo()
{
SType = StructureType.RenderPassCreateInfo,
AttachmentCount = (uint)attachments.Length,
PAttachments = atPtr,
SubpassCount = 1,
PSubpasses = &subpassDescription,
DependencyCount = 1,
PDependencies = &subpassDependency
};
api.CreateRenderPass(device, renderPassCreateInfo, null, out _renderPass).ThrowOnError();
// create framebuffer
var frameBufferAttachments = new[] { new ImageView(_colorAttachment.ViewHandle), _depthImageView };
fixed (ImageView* frAtPtr = frameBufferAttachments)
{
var framebufferCreateInfo = new FramebufferCreateInfo()
{
SType = StructureType.FramebufferCreateInfo,
RenderPass = _renderPass,
AttachmentCount = (uint)frameBufferAttachments.Length,
PAttachments = frAtPtr,
Width = (uint)size.Width,
Height = (uint)size.Height,
Layers = 1
};
api.CreateFramebuffer(device, framebufferCreateInfo, null, out _framebuffer).ThrowOnError();
}
}
// Create pipeline
var pname = Marshal.StringToHGlobalAnsi("main");
var vertShaderStageInfo = new PipelineShaderStageCreateInfo()
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.ShaderStageVertexBit,
Module = _vertShader,
PName = (byte*)pname,
};
var fragShaderStageInfo = new PipelineShaderStageCreateInfo()
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.ShaderStageFragmentBit,
Module = _fragShader,
PName = (byte*)pname,
};
var stages = new[] { vertShaderStageInfo, fragShaderStageInfo };
var bindingDescription = Vertex.VertexInputBindingDescription;
var attributeDescription = Vertex.VertexInputAttributeDescription;
fixed (VertexInputAttributeDescription* attrPtr = attributeDescription)
{
var vertextInputInfo = new PipelineVertexInputStateCreateInfo()
{
SType = StructureType.PipelineVertexInputStateCreateInfo,
VertexAttributeDescriptionCount = (uint)attributeDescription.Length,
VertexBindingDescriptionCount = 1,
PVertexAttributeDescriptions = attrPtr,
PVertexBindingDescriptions = &bindingDescription
};
var inputAssembly = new PipelineInputAssemblyStateCreateInfo()
{
SType = StructureType.PipelineInputAssemblyStateCreateInfo,
Topology = PrimitiveTopology.TriangleList,
PrimitiveRestartEnable = false
};
var viewport = new Viewport()
{
X = 0,
Y = 0,
Width = (float)size.Width,
Height = (float)size.Height,
MinDepth = 0,
MaxDepth = 1
};
var scissor = new Rect2D()
{
Offset = new Offset2D(0, 0), Extent = new Extent2D((uint)viewport.Width, (uint)viewport.Height)
};
var pipelineViewPortCreateInfo = new PipelineViewportStateCreateInfo()
{
SType = StructureType.PipelineViewportStateCreateInfo,
ViewportCount = 1,
PViewports = &viewport,
ScissorCount = 1,
PScissors = &scissor
};
var rasterizerStateCreateInfo = new PipelineRasterizationStateCreateInfo()
{
SType = StructureType.PipelineRasterizationStateCreateInfo,
DepthClampEnable = false,
RasterizerDiscardEnable = false,
PolygonMode = PolygonMode.Fill,
LineWidth = 1,
CullMode = CullModeFlags.CullModeNone,
DepthBiasEnable = false
};
var multisampleStateCreateInfo = new PipelineMultisampleStateCreateInfo()
{
SType = StructureType.PipelineMultisampleStateCreateInfo,
SampleShadingEnable = false,
RasterizationSamples = SampleCountFlags.SampleCount1Bit
};
var depthStencilCreateInfo = new PipelineDepthStencilStateCreateInfo()
{
SType = StructureType.PipelineDepthStencilStateCreateInfo,
StencilTestEnable = false,
DepthCompareOp = CompareOp.Less,
DepthTestEnable = true,
DepthWriteEnable = true,
DepthBoundsTestEnable = false,
};
var colorBlendAttachmentState = new PipelineColorBlendAttachmentState()
{
ColorWriteMask = ColorComponentFlags.ColorComponentABit |
ColorComponentFlags.ColorComponentRBit |
ColorComponentFlags.ColorComponentGBit |
ColorComponentFlags.ColorComponentBBit,
BlendEnable = false
};
var colorBlendState = new PipelineColorBlendStateCreateInfo()
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
LogicOpEnable = false,
AttachmentCount = 1,
PAttachments = &colorBlendAttachmentState
};
var dynamicStates = new DynamicState[] { DynamicState.Viewport, DynamicState.Scissor };
fixed (DynamicState* states = dynamicStates)
{
var dynamicStateCreateInfo = new PipelineDynamicStateCreateInfo()
{
SType = StructureType.PipelineDynamicStateCreateInfo,
DynamicStateCount = (uint)dynamicStates.Length,
PDynamicStates = states
};
var vertexPushConstantRange = new PushConstantRange()
{
Offset = 0,
Size = (uint)Marshal.SizeOf<VertextPushConstant>(),
StageFlags = ShaderStageFlags.ShaderStageVertexBit
};
var fragPushConstantRange = new PushConstantRange()
{
//Offset = vertexPushConstantRange.Size,
Size = (uint)Marshal.SizeOf<VertextPushConstant>(),
StageFlags = ShaderStageFlags.ShaderStageFragmentBit
};
var layoutBindingInfo = new DescriptorSetLayoutBinding
{
Binding = 0,
StageFlags = ShaderStageFlags.VertexBit,
DescriptorCount = 1,
DescriptorType = DescriptorType.UniformBuffer,
};
var layoutInfo = new DescriptorSetLayoutCreateInfo
{
SType = StructureType.DescriptorSetLayoutCreateInfo,
BindingCount = 1,
PBindings = &layoutBindingInfo
};
api.CreateDescriptorSetLayout(device, &layoutInfo, null, out _descriptorSetLayout).ThrowOnError();
var projView = view * projection;
VulkanBufferHelper.AllocateBuffer<UniformBuffer>(_context, BufferUsageFlags.UniformBufferBit,
out _uniformBuffer,
out _uniformBufferMemory, new[]
{
new UniformBuffer
{
Projection = projView
}
});
var descriptorSetLayout = _descriptorSetLayout;
var descriptorCreateInfo = new DescriptorSetAllocateInfo
{
SType = StructureType.DescriptorSetAllocateInfo,
DescriptorPool = _context.DescriptorPool,
DescriptorSetCount = 1,
PSetLayouts = &descriptorSetLayout
};
api.AllocateDescriptorSets(device, &descriptorCreateInfo, out _descriptorSet).ThrowOnError();
var descriptorBufferInfo = new DescriptorBufferInfo
{
Buffer = _uniformBuffer,
Range = (ulong)Unsafe.SizeOf<UniformBuffer>(),
};
var descriptorWrite = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstSet = _descriptorSet,
DescriptorType = DescriptorType.UniformBuffer,
DescriptorCount = 1,
PBufferInfo = &descriptorBufferInfo,
};
api.UpdateDescriptorSets(device, 1, &descriptorWrite, 0, null);
var constants = new[] { vertexPushConstantRange, fragPushConstantRange };
fixed (PushConstantRange* constant = constants)
{
var setLayout = _descriptorSetLayout;
var pipelineLayoutCreateInfo = new PipelineLayoutCreateInfo()
{
SType = StructureType.PipelineLayoutCreateInfo,
PushConstantRangeCount = (uint)constants.Length,
PPushConstantRanges = constant,
SetLayoutCount = 1,
PSetLayouts = &setLayout
};
api.CreatePipelineLayout(device, pipelineLayoutCreateInfo, null, out _pipelineLayout)
.ThrowOnError();
}
fixed (PipelineShaderStageCreateInfo* stPtr = stages)
{
var pipelineCreateInfo = new GraphicsPipelineCreateInfo()
{
SType = StructureType.GraphicsPipelineCreateInfo,
StageCount = 2,
PStages = stPtr,
PVertexInputState = &vertextInputInfo,
PInputAssemblyState = &inputAssembly,
PViewportState = &pipelineViewPortCreateInfo,
PRasterizationState = &rasterizerStateCreateInfo,
PMultisampleState = &multisampleStateCreateInfo,
PDepthStencilState = &depthStencilCreateInfo,
PColorBlendState = &colorBlendState,
PDynamicState = &dynamicStateCreateInfo,
Layout = _pipelineLayout,
RenderPass = _renderPass,
Subpass = 0,
BasePipelineHandle = _pipeline.Handle != 0 ? _pipeline : new Pipeline(),
BasePipelineIndex = _pipeline.Handle != 0 ? 0 : -1
};
api.CreateGraphicsPipelines(device, new PipelineCache(), 1, &pipelineCreateInfo, null,
out _pipeline).ThrowOnError();
}
}
}
Marshal.FreeHGlobal(pname);
_isInit = true;
}
private unsafe void CreateBuffers()
{
VulkanBufferHelper.AllocateBuffer<Vertex>(_context, BufferUsageFlags.VertexBufferBit, out _vertexBuffer,
out _vertexBufferMemory, _points);
VulkanBufferHelper.AllocateBuffer<ushort>(_context, BufferUsageFlags.IndexBufferBit, out _indexBuffer,
out _indexBufferMemory, _indices);
}
private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits,
MemoryPropertyFlags flags)
{
api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties);
for (var i = 0; i < properties.MemoryTypeCount; i++)
{
var type = properties.MemoryTypes[i];
if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i;
}
return -1;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
public static unsafe VertexInputBindingDescription VertexInputBindingDescription
{
get
{
return new VertexInputBindingDescription()
{
Binding = 0,
Stride = (uint)Marshal.SizeOf<Vertex>(),
InputRate = VertexInputRate.Vertex
};
}
}
public static unsafe VertexInputAttributeDescription[] VertexInputAttributeDescription
{
get
{
return new VertexInputAttributeDescription[]
{
new VertexInputAttributeDescription
{
Binding = 0,
Location = 0,
Format = Format.R32G32B32Sfloat,
Offset = (uint)Marshal.OffsetOf<Vertex>("Position")
},
new VertexInputAttributeDescription
{
Binding = 0,
Location = 1,
Format = Format.R32G32B32Sfloat,
Offset = (uint)Marshal.OffsetOf<Vertex>("Normal")
}
};
}
}
}
private readonly Vertex[] _points;
private readonly ushort[] _indices;
private readonly float _minY;
private readonly float _maxY;
static Stopwatch St = Stopwatch.StartNew();
private bool _isInit;
private VulkanImage _colorAttachment;
private DescriptorSet _descriptorSet;
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct VertextPushConstant
{
public float MaxY;
public float MinY;
public float Time;
public float Disco;
public Matrix4x4 Model;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct UniformBuffer
{
public Matrix4x4 Projection;
}
}

335
samples/GpuInterop/VulkanDemo/VulkanContext.cs

@ -0,0 +1,335 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
using Avalonia.Vulkan;
using Silk.NET.Core;
using Silk.NET.Core.Native;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Vulkan.Extensions.KHR;
using SilkNetDemo;
using SkiaSharp;
using D3DDevice = SharpDX.Direct3D11.Device;
using DxgiDevice = SharpDX.DXGI.Device;
namespace GpuInterop.VulkanDemo;
public unsafe class VulkanContext : IDisposable
{
public Vk Api { get; init; }
public Instance Instance { get; init; }
public PhysicalDevice PhysicalDevice { get; init; }
public Device Device { get; init; }
public Queue Queue { get; init; }
public uint QueueFamilyIndex { get; init; }
public VulkanCommandBufferPool Pool { get; init; }
public GRContext GrContext { get; init; }
public DescriptorPool DescriptorPool { get; init; }
public D3DDevice? D3DDevice { get; init; }
public static (VulkanContext? result, string info) TryCreate(ICompositionGpuInterop gpuInterop)
{
using var appName = new ByteString("GpuInterop");
using var engineName = new ByteString("Test");
var applicationInfo = new ApplicationInfo
{
SType = StructureType.ApplicationInfo,
PApplicationName = appName,
ApiVersion = new Version32(1, 1, 0),
PEngineName = appName,
EngineVersion = new Version32(1, 0, 0),
ApplicationVersion = new Version32(1, 0, 0)
};
var enabledExtensions = new List<string>()
{
"VK_KHR_get_physical_device_properties2",
"VK_KHR_external_memory_capabilities",
"VK_KHR_external_semaphore_capabilities"
};
var enabledLayers = new List<string>();
Vk api = Vk.GetApi();
enabledExtensions.Add("VK_EXT_debug_utils");
if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation"))
enabledLayers.Add("VK_LAYER_KHRONOS_validation");
Instance vkInstance = default;
Silk.NET.Vulkan.PhysicalDevice physicalDevice = default;
Device device = default;
DescriptorPool descriptorPool = default;
VulkanCommandBufferPool? pool = null;
GRContext? grContext = null;
try
{
using var pRequiredExtensions = new ByteStringList(enabledExtensions);
using var pEnabledLayers = new ByteStringList(enabledLayers);
api.CreateInstance(new InstanceCreateInfo
{
SType = StructureType.InstanceCreateInfo,
PApplicationInfo = &applicationInfo,
PpEnabledExtensionNames = pRequiredExtensions,
EnabledExtensionCount = pRequiredExtensions.UCount,
PpEnabledLayerNames = pEnabledLayers,
EnabledLayerCount = pEnabledLayers.UCount
}, null, out vkInstance).ThrowOnError();
if (api.TryGetInstanceExtension(vkInstance, out ExtDebugUtils debugUtils))
{
var debugCreateInfo = new DebugUtilsMessengerCreateInfoEXT
{
SType = StructureType.DebugUtilsMessengerCreateInfoExt,
MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityVerboseBitExt |
DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityWarningBitExt |
DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityErrorBitExt,
MessageType = DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeGeneralBitExt |
DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeValidationBitExt |
DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypePerformanceBitExt,
PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(LogCallback),
};
debugUtils.CreateDebugUtilsMessenger(vkInstance, debugCreateInfo, null, out var messenger);
}
var requireDeviceExtensions = new List<string>
{
"VK_KHR_external_memory",
"VK_KHR_external_semaphore"
};
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes
.D3D11TextureGlobalSharedHandle)
)
return (null, "Image sharing is not supported by the current backend");
requireDeviceExtensions.Add(KhrExternalMemoryWin32.ExtensionName);
requireDeviceExtensions.Add(KhrExternalSemaphoreWin32.ExtensionName);
requireDeviceExtensions.Add("VK_KHR_dedicated_allocation");
requireDeviceExtensions.Add("VK_KHR_get_memory_requirements2");
}
else
{
if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes
.VulkanOpaquePosixFileDescriptor)
|| !gpuInterop.SupportedSemaphoreTypes.Contains(KnownPlatformGraphicsExternalSemaphoreHandleTypes
.VulkanOpaquePosixFileDescriptor)
)
return (null, "Image sharing is not supported by the current backend");
requireDeviceExtensions.Add(KhrExternalMemoryFd.ExtensionName);
requireDeviceExtensions.Add(KhrExternalSemaphoreFd.ExtensionName);
}
uint count = 0;
api.EnumeratePhysicalDevices(vkInstance, ref count, null).ThrowOnError();
var physicalDevices = stackalloc PhysicalDevice[(int)count];
api.EnumeratePhysicalDevices(vkInstance, ref count, physicalDevices)
.ThrowOnError();
for (uint c = 0; c < count; c++)
{
if (requireDeviceExtensions.Any(ext => !api.IsDeviceExtensionPresent(physicalDevices[c], ext)))
continue;
var physicalDeviceIDProperties = new PhysicalDeviceIDProperties()
{
SType = StructureType.PhysicalDeviceIDProperties
};
var physicalDeviceProperties2 = new PhysicalDeviceProperties2()
{
SType = StructureType.PhysicalDeviceProperties2,
PNext = &physicalDeviceIDProperties
};
api.GetPhysicalDeviceProperties2(physicalDevices[c], &physicalDeviceProperties2);
if (gpuInterop.DeviceLuid != null && physicalDeviceIDProperties.DeviceLuidvalid)
{
if (!new Span<byte>(physicalDeviceIDProperties.DeviceLuid, 8)
.SequenceEqual(gpuInterop.DeviceLuid))
continue;
}
else if (gpuInterop.DeviceUuid != null)
{
if (!new Span<byte>(physicalDeviceIDProperties.DeviceUuid, 16)
.SequenceEqual(gpuInterop?.DeviceUuid))
continue;
}
physicalDevice = physicalDevices[c];
var name = Marshal.PtrToStringAnsi(new IntPtr(physicalDeviceProperties2.Properties.DeviceName))!;
uint queueFamilyCount = 0;
api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, null);
var familyProperties = stackalloc QueueFamilyProperties[(int)queueFamilyCount];
api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, familyProperties);
for (uint queueFamilyIndex = 0; queueFamilyIndex < queueFamilyCount; queueFamilyIndex++)
{
var family = familyProperties[c];
if (!family.QueueFlags.HasAllFlags(QueueFlags.GraphicsBit))
continue;
var queuePriorities = stackalloc float[(int)family.QueueCount];
for (var i = 0; i < family.QueueCount; i++)
queuePriorities[i] = 1f;
var features = new PhysicalDeviceFeatures();
var queueCreateInfo = new DeviceQueueCreateInfo
{
SType = StructureType.DeviceQueueCreateInfo,
QueueFamilyIndex = queueFamilyIndex,
QueueCount = family.QueueCount,
PQueuePriorities = queuePriorities
};
using var pEnabledDeviceExtensions = new ByteStringList(requireDeviceExtensions);
var deviceCreateInfo = new DeviceCreateInfo
{
SType = StructureType.DeviceCreateInfo,
QueueCreateInfoCount = 1,
PQueueCreateInfos = &queueCreateInfo,
PpEnabledExtensionNames = pEnabledDeviceExtensions,
EnabledExtensionCount = pEnabledDeviceExtensions.UCount,
PEnabledFeatures = &features
};
api.CreateDevice(physicalDevice, in deviceCreateInfo, null, out device)
.ThrowOnError();
api.GetDeviceQueue(device, queueFamilyIndex, 0, out var queue);
var descriptorPoolSize = new DescriptorPoolSize
{
Type = DescriptorType.UniformBuffer, DescriptorCount = 16
};
var descriptorPoolInfo = new DescriptorPoolCreateInfo
{
SType = StructureType.DescriptorPoolCreateInfo,
PoolSizeCount = 1,
PPoolSizes = &descriptorPoolSize,
MaxSets = 16,
Flags = DescriptorPoolCreateFlags.FreeDescriptorSetBit
};
api.CreateDescriptorPool(device, &descriptorPoolInfo, null, out descriptorPool)
.ThrowOnError();
pool = new VulkanCommandBufferPool(api, device, queue, queueFamilyIndex);
grContext = GRContext.CreateVulkan(new GRVkBackendContext
{
VkInstance = vkInstance.Handle,
VkDevice = device.Handle,
VkQueue = queue.Handle,
GraphicsQueueIndex = queueFamilyIndex,
VkPhysicalDevice = physicalDevice.Handle,
GetProcedureAddress = (proc, _, _) =>
{
var rv = api.GetDeviceProcAddr(device, proc);
if (rv != IntPtr.Zero)
return rv;
rv = api.GetInstanceProcAddr(vkInstance, proc);
if (rv != IntPtr.Zero)
return rv;
return api.GetInstanceProcAddr(default, proc);
}
});
D3DDevice? d3dDevice = null;
if (physicalDeviceIDProperties.DeviceLuidvalid &&
RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
d3dDevice = D3DMemoryHelper.CreateDeviceByLuid(
new Span<byte>(physicalDeviceIDProperties.DeviceLuid, 8));
var dxgiDevice = d3dDevice?.QueryInterface<DxgiDevice>();
return (new VulkanContext
{
Api = api,
Device = device,
Instance = vkInstance,
PhysicalDevice = physicalDevice,
Queue = queue,
QueueFamilyIndex = queueFamilyIndex,
Pool = pool,
DescriptorPool = descriptorPool,
GrContext = grContext,
D3DDevice = d3dDevice
}, name);
}
return (null, "No suitable device queue found");
}
return (null, "Suitable device not found");
}
catch (Exception e)
{
return (null, e.ToString());
}
finally
{
if (grContext == null && api != null)
{
pool?.Dispose();
if (descriptorPool.Handle != default)
api.DestroyDescriptorPool(device, descriptorPool, null);
if (device.Handle != default)
api.DestroyDevice(device, null);
}
}
}
private static unsafe bool IsLayerAvailable(Vk api, string layerName)
{
uint layerPropertiesCount;
api.EnumerateInstanceLayerProperties(&layerPropertiesCount, null).ThrowOnError();
var layerProperties = new LayerProperties[layerPropertiesCount];
fixed (LayerProperties* pLayerProperties = layerProperties)
{
api.EnumerateInstanceLayerProperties(&layerPropertiesCount, layerProperties).ThrowOnError();
for (var i = 0; i < layerPropertiesCount; i++)
{
var currentLayerName = Marshal.PtrToStringAnsi((IntPtr)pLayerProperties[i].LayerName);
if (currentLayerName == layerName) return true;
}
}
return false;
}
private static unsafe uint LogCallback(DebugUtilsMessageSeverityFlagsEXT messageSeverity, DebugUtilsMessageTypeFlagsEXT messageTypes, DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData)
{
if (messageSeverity != DebugUtilsMessageSeverityFlagsEXT.VerboseBitExt)
{
var message = Marshal.PtrToStringAnsi((nint)pCallbackData->PMessage);
Console.WriteLine(message);
}
return Vk.False;
}
public void Dispose()
{
D3DDevice?.Dispose();
GrContext.Dispose();
Pool.Dispose();
Api.DestroyDescriptorPool(Device, DescriptorPool, null);
Api.DestroyDevice(Device, null);
}
}

101
samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs

@ -0,0 +1,101 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.Composition;
using Silk.NET.Core;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using SilkNetDemo;
namespace GpuInterop.VulkanDemo;
public class VulkanDemoControl : DrawingSurfaceDemoBase
{
private Instance _vkInstance;
private Vk _api;
class VulkanResources : IAsyncDisposable
{
public VulkanContext Context { get; }
public VulkanSwapchain Swapchain { get; }
public VulkanContent Content { get; }
public VulkanResources(VulkanContext context, VulkanSwapchain swapchain, VulkanContent content)
{
Context = context;
Swapchain = swapchain;
Content = content;
}
public async ValueTask DisposeAsync()
{
Context.Pool.FreeUsedCommandBuffers();
Content.Dispose();
await Swapchain.DisposeAsync();
Context.Dispose();
}
}
protected override bool SupportsDisco => true;
private VulkanResources? _resources;
protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor,
CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop)
{
var (context, info) = VulkanContext.TryCreate(gpuInterop);
if (context == null)
return (false, info);
try
{
var content = new VulkanContent(context);
_resources = new VulkanResources(context,
new VulkanSwapchain(context, gpuInterop, compositionDrawingSurface), content);
return (true, info);
}
catch(Exception e)
{
return (false, e.ToString());
}
}
protected override void FreeGraphicsResources()
{
_resources?.DisposeAsync();
_resources = null;
}
protected override unsafe void RenderFrame(PixelSize pixelSize)
{
if (_resources == null)
return;
using (_resources.Swapchain.BeginDraw(pixelSize, out var image))
{
/*
var commandBuffer = _resources.Context.Pool.CreateCommandBuffer();
commandBuffer.BeginRecording();
image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.None);
var range = new ImageSubresourceRange
{
AspectMask = ImageAspectFlags.ColorBit,
LayerCount = 1,
LevelCount = 1,
BaseArrayLayer = 0,
BaseMipLevel = 0
};
var color = new ClearColorValue
{
Float32_0 = 1, Float32_1 = 0, Float32_2 = 0, Float32_3 = 1
};
_resources.Context.Api.CmdClearColorImage(commandBuffer.InternalHandle, image.InternalHandle.Value, ImageLayout.TransferDstOptimal,
&color, 1, &range);
commandBuffer.Submit();*/
_resources.Content.Render(image, Yaw, Pitch, Roll, Disco);
}
}
}

12
samples/GpuInterop/VulkanDemo/VulkanExtensions.cs

@ -0,0 +1,12 @@
using System;
using Silk.NET.Vulkan;
namespace SilkNetDemo;
public static class VulkanExtensions
{
public static void ThrowOnError(this Result result)
{
if (result != Result.Success) throw new Exception($"Unexpected API error \"{result}\".");
}
}

276
samples/GpuInterop/VulkanDemo/VulkanImage.cs

@ -0,0 +1,276 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Vulkan;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using SilkNetDemo;
using SkiaSharp;
namespace GpuInterop.VulkanDemo;
public unsafe class VulkanImage : IDisposable
{
private readonly VulkanContext _vk;
private readonly Instance _instance;
private readonly Device _device;
private readonly PhysicalDevice _physicalDevice;
private readonly VulkanCommandBufferPool _commandBufferPool;
private ImageLayout _currentLayout;
private AccessFlags _currentAccessFlags;
private ImageUsageFlags _imageUsageFlags { get; }
private ImageView? _imageView { get; set; }
private DeviceMemory _imageMemory { get; set; }
private SharpDX.Direct3D11.Texture2D? _d3dTexture2D;
private IntPtr _win32ShareHandle;
internal Image? InternalHandle { get; private set; }
internal Format Format { get; }
internal ImageAspectFlags AspectFlags { get; private set; }
public ulong Handle => InternalHandle?.Handle ?? 0;
public ulong ViewHandle => _imageView?.Handle ?? 0;
public uint UsageFlags => (uint) _imageUsageFlags;
public ulong MemoryHandle => _imageMemory.Handle;
public DeviceMemory DeviceMemory => _imageMemory;
public uint MipLevels { get; private set; }
public Vk Api { get; }
public PixelSize Size { get; }
public ulong MemorySize { get; private set; }
public uint CurrentLayout => (uint) _currentLayout;
public VulkanImage(VulkanContext vk, uint format, PixelSize size,
bool exportable, uint mipLevels = 0)
{
_vk = vk;
_instance = vk.Instance;
_device = vk.Device;
_physicalDevice = vk.PhysicalDevice;
_commandBufferPool = vk.Pool;
Format = (Format)format;
Api = vk.Api;
Size = size;
MipLevels = 1;//mipLevels;
_imageUsageFlags =
ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit |
ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit;
//MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2));
var handleType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit :
ExternalMemoryHandleTypeFlags.OpaqueFDBit;
var externalMemoryCreateInfo = new ExternalMemoryImageCreateInfo
{
SType = StructureType.ExternalMemoryImageCreateInfo,
HandleTypes = handleType
};
var imageCreateInfo = new ImageCreateInfo
{
PNext = exportable ? &externalMemoryCreateInfo : null,
SType = StructureType.ImageCreateInfo,
ImageType = ImageType.ImageType2D,
Format = Format,
Extent =
new Extent3D((uint?)Size.Width,
(uint?)Size.Height, 1),
MipLevels = MipLevels,
ArrayLayers = 1,
Samples = SampleCountFlags.SampleCount1Bit,
Tiling = Tiling,
Usage = _imageUsageFlags,
SharingMode = SharingMode.Exclusive,
InitialLayout = ImageLayout.Undefined,
Flags = ImageCreateFlags.ImageCreateMutableFormatBit
};
Api
.CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError();
InternalHandle = image;
Api.GetImageMemoryRequirements(_device, InternalHandle.Value,
out var memoryRequirements);
var fdExport = new ExportMemoryAllocateInfo
{
HandleTypes = handleType, SType = StructureType.ExportMemoryAllocateInfo
};
var dedicatedAllocation = new MemoryDedicatedAllocateInfoKHR
{
SType = StructureType.MemoryDedicatedAllocateInfoKhr,
Image = image
};
ImportMemoryWin32HandleInfoKHR handleImport = default;
if (exportable && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_d3dTexture2D = D3DMemoryHelper.CreateMemoryHandle(vk.D3DDevice, size, Format);
using var dxgi = _d3dTexture2D.QueryInterface<SharpDX.DXGI.Resource>();
_win32ShareHandle = dxgi.SharedHandle;
handleImport = new ImportMemoryWin32HandleInfoKHR
{
PNext = &dedicatedAllocation,
SType = StructureType.ImportMemoryWin32HandleInfoKhr,
HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit,
Handle = _win32ShareHandle,
};
}
var memoryAllocateInfo = new MemoryAllocateInfo
{
PNext =
exportable ? RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? &handleImport : &fdExport : null,
SType = StructureType.MemoryAllocateInfo,
AllocationSize = memoryRequirements.Size,
MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex(
Api,
_physicalDevice,
memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit)
};
Api.AllocateMemory(_device, memoryAllocateInfo, null,
out var imageMemory).ThrowOnError();
_imageMemory = imageMemory;
MemorySize = memoryRequirements.Size;
Api.BindImageMemory(_device, InternalHandle.Value, _imageMemory, 0).ThrowOnError();
var componentMapping = new ComponentMapping(
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity,
ComponentSwizzle.Identity);
AspectFlags = ImageAspectFlags.ImageAspectColorBit;
var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1);
var imageViewCreateInfo = new ImageViewCreateInfo
{
SType = StructureType.ImageViewCreateInfo,
Image = InternalHandle.Value,
ViewType = ImageViewType.ImageViewType2D,
Format = Format,
Components = componentMapping,
SubresourceRange = subresourceRange
};
Api
.CreateImageView(_device, imageViewCreateInfo, null, out var imageView)
.ThrowOnError();
_imageView = imageView;
_currentLayout = ImageLayout.Undefined;
TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr);
}
public int ExportFd()
{
if (!Api.TryGetDeviceExtension<KhrExternalMemoryFd>(_instance, _device, out var ext))
throw new InvalidOperationException();
var info = new MemoryGetFdInfoKHR
{
Memory = _imageMemory,
SType = StructureType.MemoryGetFDInfoKhr,
HandleType = ExternalMemoryHandleTypeFlags.OpaqueFDBit
};
ext.GetMemoryF(_device, info, out var fd).ThrowOnError();
return fd;
}
public IPlatformHandle Export() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
new PlatformHandle(_win32ShareHandle,
KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) :
new PlatformHandle(new IntPtr(ExportFd()),
KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor);
public ImageTiling Tiling => ImageTiling.Optimal;
internal void TransitionLayout(CommandBuffer commandBuffer,
ImageLayout fromLayout, AccessFlags fromAccessFlags,
ImageLayout destinationLayout, AccessFlags destinationAccessFlags)
{
VulkanMemoryHelper.TransitionLayout(Api, commandBuffer, InternalHandle.Value,
fromLayout,
fromAccessFlags,
destinationLayout, destinationAccessFlags,
MipLevels);
_currentLayout = destinationLayout;
_currentAccessFlags = destinationAccessFlags;
}
internal void TransitionLayout(CommandBuffer commandBuffer,
ImageLayout destinationLayout, AccessFlags destinationAccessFlags)
=> TransitionLayout(commandBuffer, _currentLayout, _currentAccessFlags, destinationLayout,
destinationAccessFlags);
internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags)
{
var commandBuffer = _commandBufferPool.CreateCommandBuffer();
commandBuffer.BeginRecording();
TransitionLayout(commandBuffer.InternalHandle, destinationLayout, destinationAccessFlags);
commandBuffer.EndRecording();
commandBuffer.Submit();
}
public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags)
{
TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags);
}
public unsafe void Dispose()
{
Api.DestroyImageView(_device, _imageView.Value, null);
Api.DestroyImage(_device, InternalHandle.Value, null);
Api.FreeMemory(_device, _imageMemory, null);
_imageView = default;
InternalHandle = default;
_imageMemory = default;
}
public void SaveTexture(string path)
{
_vk.GrContext.ResetContext();
var _image = this;
var imageInfo = new GRVkImageInfo()
{
CurrentQueueFamily = _vk.QueueFamilyIndex,
Format = (uint)_image.Format,
Image = _image.Handle,
ImageLayout = (uint)_image.CurrentLayout,
ImageTiling = (uint)_image.Tiling,
ImageUsageFlags = (uint)_image.UsageFlags,
LevelCount = _image.MipLevels,
SampleCount = 1,
Protected = false,
Alloc = new GRVkAlloc()
{
Memory = _image.MemoryHandle, Flags = 0, Offset = 0, Size = _image.MemorySize
}
};
using (var backendTexture = new GRBackendRenderTarget(_image.Size.Width, _image.Size.Height, 1,
imageInfo))
using (var surface = SKSurface.Create(_vk.GrContext, backendTexture,
GRSurfaceOrigin.TopLeft,
SKColorType.Rgba8888, SKColorSpace.CreateSrgb()))
{
using var snap = surface.Snapshot();
using var encoded = snap.Encode();
using (var s = File.Create(path))
encoded.SaveTo(s);
}
}
}

59
samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs

@ -0,0 +1,59 @@
using Silk.NET.Vulkan;
namespace GpuInterop.VulkanDemo;
internal static class VulkanMemoryHelper
{
internal static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits,
MemoryPropertyFlags flags)
{
api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties);
for (var i = 0; i < properties.MemoryTypeCount; i++)
{
var type = properties.MemoryTypes[i];
if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i;
}
return -1;
}
internal static unsafe void TransitionLayout(
Vk api,
CommandBuffer commandBuffer,
Image image,
ImageLayout sourceLayout,
AccessFlags sourceAccessMask,
ImageLayout destinationLayout,
AccessFlags destinationAccessMask,
uint mipLevels)
{
var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1);
var barrier = new ImageMemoryBarrier
{
SType = StructureType.ImageMemoryBarrier,
SrcAccessMask = sourceAccessMask,
DstAccessMask = destinationAccessMask,
OldLayout = sourceLayout,
NewLayout = destinationLayout,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = image,
SubresourceRange = subresourceRange
};
api.CmdPipelineBarrier(
commandBuffer,
PipelineStageFlags.PipelineStageAllCommandsBit,
PipelineStageFlags.PipelineStageAllCommandsBit,
0,
0,
null,
0,
null,
1,
barrier);
}
}

58
samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs

@ -0,0 +1,58 @@
using System;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
using SilkNetDemo;
namespace GpuInterop.VulkanDemo;
class VulkanSemaphorePair : IDisposable
{
private readonly VulkanContext _resources;
public unsafe VulkanSemaphorePair(VulkanContext resources, bool exportable)
{
_resources = resources;
var semaphoreExportInfo = new ExportSemaphoreCreateInfo
{
SType = StructureType.ExportSemaphoreCreateInfo,
HandleTypes = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit
};
var semaphoreCreateInfo = new SemaphoreCreateInfo
{
SType = StructureType.SemaphoreCreateInfo,
PNext = exportable ? &semaphoreExportInfo : null
};
resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out var semaphore).ThrowOnError();
ImageAvailableSemaphore = semaphore;
resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out semaphore).ThrowOnError();
RenderFinishedSemaphore = semaphore;
}
public int ExportFd(bool renderFinished)
{
if (!_resources.Api.TryGetDeviceExtension<KhrExternalSemaphoreFd>(_resources.Instance, _resources.Device,
out var ext))
throw new InvalidOperationException();
var info = new SemaphoreGetFdInfoKHR()
{
SType = StructureType.SemaphoreGetFDInfoKhr,
Semaphore = renderFinished ? RenderFinishedSemaphore : ImageAvailableSemaphore,
HandleType = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit
};
ext.GetSemaphoreF(_resources.Device, info, out var fd).ThrowOnError();
return fd;
}
internal Semaphore ImageAvailableSemaphore { get; }
internal Semaphore RenderFinishedSemaphore { get; }
public unsafe void Dispose()
{
_resources.Api.DestroySemaphore(_resources.Device, ImageAvailableSemaphore, null);
_resources.Api.DestroySemaphore(_resources.Device, RenderFinishedSemaphore, null);
}
}

154
samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs

@ -0,0 +1,154 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Vulkan;
using Metsys.Bson;
using Silk.NET.Vulkan;
using SkiaSharp;
namespace GpuInterop.VulkanDemo;
class VulkanSwapchain : SwapchainBase<VulkanSwapchainImage>
{
private readonly VulkanContext _vk;
public VulkanSwapchain(VulkanContext vk, ICompositionGpuInterop interop, CompositionDrawingSurface target) : base(interop, target)
{
_vk = vk;
}
protected override VulkanSwapchainImage CreateImage(PixelSize size)
{
return new VulkanSwapchainImage(_vk, size, Interop, Target);
}
public IDisposable BeginDraw(PixelSize size, out VulkanImage image)
{
_vk.Pool.FreeUsedCommandBuffers();
var rv = BeginDrawCore(size, out var swapchainImage);
image = swapchainImage.Image;
return rv;
}
}
class VulkanSwapchainImage : ISwapchainImage
{
private readonly VulkanContext _vk;
private readonly ICompositionGpuInterop _interop;
private readonly CompositionDrawingSurface _target;
private readonly VulkanImage _image;
private readonly VulkanSemaphorePair _semaphorePair;
private ICompositionImportedGpuSemaphore? _availableSemaphore, _renderCompletedSemaphore;
private ICompositionImportedGpuImage? _importedImage;
private Task? _lastPresent;
public VulkanImage Image => _image;
private bool _initial = true;
public VulkanSwapchainImage(VulkanContext vk, PixelSize size, ICompositionGpuInterop interop, CompositionDrawingSurface target)
{
_vk = vk;
_interop = interop;
_target = target;
Size = size;
_image = new VulkanImage(vk, (uint)Format.R8G8B8A8Unorm, size, true);
_semaphorePair = new VulkanSemaphorePair(vk, true);
}
public async ValueTask DisposeAsync()
{
if (LastPresent != null)
await LastPresent;
if (_importedImage != null)
await _importedImage.DisposeAsync();
if (_availableSemaphore != null)
await _availableSemaphore.DisposeAsync();
if (_renderCompletedSemaphore != null)
await _renderCompletedSemaphore.DisposeAsync();
_semaphorePair.Dispose();
_image.Dispose();
}
public PixelSize Size { get; }
public Task? LastPresent => _lastPresent;
public void BeginDraw()
{
var buffer = _vk.Pool.CreateCommandBuffer();
buffer.BeginRecording();
_image.TransitionLayout(buffer.InternalHandle,
ImageLayout.Undefined, AccessFlags.None,
ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessColorAttachmentReadBit);
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
buffer.Submit(null,null,null, null, new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo
{
AcquireKey = 0,
DeviceMemory = _image.DeviceMemory
});
else if (_initial)
{
_initial = false;
buffer.Submit();
}
else
buffer.Submit(new[] { _semaphorePair.ImageAvailableSemaphore },
new[]
{
PipelineStageFlags.AllGraphicsBit
});
}
public void Present()
{
var buffer = _vk.Pool.CreateCommandBuffer();
buffer.BeginRecording();
_image.TransitionLayout(buffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferWriteBit);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
buffer.Submit(null, null, null, null,
new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo
{
DeviceMemory = _image.DeviceMemory, ReleaseKey = 1
});
}
else
buffer.Submit(null, null, new[] { _semaphorePair.RenderFinishedSemaphore });
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_availableSemaphore ??= _interop.ImportSemaphore(new PlatformHandle(
new IntPtr(_semaphorePair.ExportFd(false)),
KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor));
_renderCompletedSemaphore ??= _interop.ImportSemaphore(new PlatformHandle(
new IntPtr(_semaphorePair.ExportFd(true)),
KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor));
}
_importedImage ??= _interop.ImportImage(_image.Export(),
new PlatformGraphicsExternalImageProperties
{
Format = PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm,
Width = Size.Width,
Height = Size.Height,
MemorySize = _image.MemorySize
});
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
_lastPresent = _target.UpdateWithKeyedMutexAsync(_importedImage, 1, 0);
else
_lastPresent = _target.UpdateWithSemaphoresAsync(_importedImage, _renderCompletedSemaphore, _availableSemaphore);
}
}

4
src/Avalonia.Base/AttachedProperty.cs

@ -24,11 +24,9 @@ namespace Avalonia
Func<TValue, bool>? validate = null)
: base(name, ownerType, metadata, inherits, validate)
{
IsAttached = true;
}
/// <inheritdoc/>
public override bool IsAttached => true;
/// <summary>
/// Attaches the property as a non-attached property on the specified type.
/// </summary>

4
src/Avalonia.Base/Avalonia.Base.csproj

@ -30,6 +30,7 @@
<InternalsVisibleTo Include="Avalonia.Desktop, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Controls, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Direct2D1, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.OpenGL, PublicKey=$(AvaloniaPublicKey)" />
@ -47,6 +48,9 @@
<InternalsVisibleTo Include="Avalonia.DesignerSupport, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Direct2D1.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.LeakTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Markup.Xaml.UnitTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.RenderTests, PublicKey=$(AvaloniaPublicKey)" />
<InternalsVisibleTo Include="Avalonia.Skia.UnitTests, PublicKey=$(AvaloniaPublicKey)" />

16
src/Avalonia.Base/AvaloniaObject.cs

@ -132,7 +132,7 @@ namespace Avalonia
switch (property)
{
case StyledPropertyBase<T> styled:
case StyledProperty<T> styled:
ClearValue(styled);
break;
case DirectPropertyBase<T> direct:
@ -147,7 +147,7 @@ namespace Avalonia
/// Clears a <see cref="AvaloniaProperty"/>'s local value.
/// </summary>
/// <param name="property">The property.</param>
public void ClearValue<T>(StyledPropertyBase<T> property)
public void ClearValue<T>(StyledProperty<T> property)
{
property = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
@ -220,7 +220,7 @@ namespace Avalonia
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <returns>The value.</returns>
public T GetValue<T>(StyledPropertyBase<T> property)
public T GetValue<T>(StyledProperty<T> property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
@ -243,7 +243,7 @@ namespace Avalonia
}
/// <inheritdoc/>
public Optional<T> GetBaseValue<T>(StyledPropertyBase<T> property)
public Optional<T> GetBaseValue<T>(StyledProperty<T> property)
{
_ = property ?? throw new ArgumentNullException(nameof(property));
VerifyAccess();
@ -309,7 +309,7 @@ namespace Avalonia
/// An <see cref="IDisposable"/> if setting the property can be undone, otherwise null.
/// </returns>
public IDisposable? SetValue<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
@ -373,7 +373,7 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<object?> source,
BindingPriority priority = BindingPriority.LocalValue)
{
@ -396,7 +396,7 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue)
{
@ -419,7 +419,7 @@ namespace Avalonia
/// A disposable which can be used to terminate the binding.
/// </returns>
public IDisposable Bind<T>(
StyledPropertyBase<T> property,
StyledProperty<T> property,
IObservable<BindingValue<T>> source,
BindingPriority priority = BindingPriority.LocalValue)
{

8
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -146,7 +146,7 @@ namespace Avalonia
return property switch
{
StyledPropertyBase<T> styled => target.Bind(styled, source, priority),
StyledProperty<T> styled => target.Bind(styled, source, priority),
DirectPropertyBase<T> direct => target.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
@ -170,7 +170,7 @@ namespace Avalonia
{
return property switch
{
StyledPropertyBase<T> styled => target.Bind(styled, source, priority),
StyledProperty<T> styled => target.Bind(styled, source, priority),
DirectPropertyBase<T> direct => target.Bind(direct, source),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type."),
};
@ -231,7 +231,7 @@ namespace Avalonia
return property switch
{
StyledPropertyBase<T> styled => target.GetValue(styled),
StyledProperty<T> styled => target.GetValue(styled),
DirectPropertyBase<T> direct => target.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};
@ -280,7 +280,7 @@ namespace Avalonia
return property switch
{
StyledPropertyBase<T> styled => target.GetBaseValue(styled),
StyledProperty<T> styled => target.GetBaseValue(styled),
DirectPropertyBase<T> direct => target.GetValue(direct),
_ => throw new NotSupportedException("Unsupported AvaloniaProperty type.")
};

8
src/Avalonia.Base/AvaloniaProperty.cs

@ -107,22 +107,22 @@ namespace Avalonia
/// <summary>
/// Gets a value indicating whether the property inherits its value.
/// </summary>
public virtual bool Inherits => false;
public bool Inherits { get; private protected set; }
/// <summary>
/// Gets a value indicating whether this is an attached property.
/// </summary>
public virtual bool IsAttached => false;
public bool IsAttached { get; private protected set; }
/// <summary>
/// Gets a value indicating whether this is a direct property.
/// </summary>
public virtual bool IsDirect => false;
public bool IsDirect { get; private protected set; }
/// <summary>
/// Gets a value indicating whether this is a readonly property.
/// </summary>
public virtual bool IsReadOnly => false;
public bool IsReadOnly { get; private protected set; }
/// <summary>
/// Gets an observable that is fired when this property changes on any

59
src/Avalonia.Base/Data/BindingOperations.cs

@ -41,54 +41,41 @@ namespace Avalonia.Data
{
case BindingMode.Default:
case BindingMode.OneWay:
if (binding.Observable is null)
throw new InvalidOperationException("InstancedBinding does not contain an observable.");
return target.Bind(property, binding.Observable, binding.Priority);
return target.Bind(property, binding.Source, binding.Priority);
case BindingMode.TwoWay:
if (binding.Observable is null)
throw new InvalidOperationException("InstancedBinding does not contain an observable.");
if (binding.Subject is null)
{
if (binding.Source is not IObserver<object?> observer)
throw new InvalidOperationException("InstancedBinding does not contain a subject.");
return new TwoWayBindingDisposable(
target.Bind(property, binding.Observable, binding.Priority),
target.GetObservable(property).Subscribe(binding.Subject));
target.Bind(property, binding.Source, binding.Priority),
target.GetObservable(property).Subscribe(observer));
}
case BindingMode.OneTime:
if (binding.Observable is {} source)
{
// Perf: Avoid allocating closure in the outer scope.
var targetCopy = target;
var propertyCopy = property;
var bindingCopy = binding;
return source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1)
.Subscribe(x => targetCopy.SetValue(
propertyCopy,
BindingNotification.ExtractValue(x),
bindingCopy.Priority));
}
else
{
target.SetValue(property, binding.Value, binding.Priority);
return Disposable.Empty;
}
{
// Perf: Avoid allocating closure in the outer scope.
var targetCopy = target;
var propertyCopy = property;
var bindingCopy = binding;
return binding.Source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1)
.Subscribe(x => targetCopy.SetValue(
propertyCopy,
BindingNotification.ExtractValue(x),
bindingCopy.Priority));
}
case BindingMode.OneWayToSource:
{
if (binding.Observable is null)
throw new InvalidOperationException("InstancedBinding does not contain an observable.");
if (binding.Subject is null)
if (binding.Source is not IObserver<object?> observer)
throw new InvalidOperationException("InstancedBinding does not contain a subject.");
// Perf: Avoid allocating closure in the outer scope.
var bindingCopy = binding;
return Observable.CombineLatest(
binding.Observable,
binding.Source,
target.GetObservable(property),
(_, v) => v)
.Subscribe(x => bindingCopy.Subject.OnNext(x));
.Subscribe(x => observer.OnNext(x));
}
default:

2
src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs

@ -3,7 +3,7 @@ using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
public class AvaloniaPropertyAccessorNode : SettableNode
internal class AvaloniaPropertyAccessorNode : SettableNode
{
private IDisposable? _subscription;
private readonly bool _enableValidation;

2
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -13,7 +13,7 @@ namespace Avalonia.Data.Core
/// that are sent and received.
/// </summary>
[RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)]
public class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription
internal class BindingExpression : LightweightObservableBase<object?>, IAvaloniaSubject<object?>, IDescription
{
private readonly ExpressionObserver _inner;
private readonly Type _targetType;

2
src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs

@ -1,6 +1,6 @@
namespace Avalonia.Data.Core
{
public class EmptyExpressionNode : ExpressionNode
internal class EmptyExpressionNode : ExpressionNode
{
public override string Description => ".";
}

2
src/Avalonia.Base/Data/Core/ExpressionNode.cs

@ -2,7 +2,7 @@ using System;
namespace Avalonia.Data.Core
{
public abstract class ExpressionNode
internal abstract class ExpressionNode
{
protected static readonly WeakReference<object?> UnsetReference =
new WeakReference<object?>(AvaloniaProperty.UnsetValue);

2
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -11,7 +11,7 @@ namespace Avalonia.Data.Core
/// <summary>
/// Observes and sets the value of an expression on an object.
/// </summary>
public class ExpressionObserver : LightweightObservableBase<object?>, IDescription
internal class ExpressionObserver : LightweightObservableBase<object?>, IDescription
{
/// <summary>
/// An ordered collection of property accessor plugins that can be used to customize

2
src/Avalonia.Base/Data/Core/IndexerNodeBase.cs

@ -7,7 +7,7 @@ using Avalonia.Utilities;
namespace Avalonia.Data.Core
{
public abstract class IndexerNodeBase : SettableNode,
internal abstract class IndexerNodeBase : SettableNode,
IWeakEventSubscriber<NotifyCollectionChangedEventArgs>,
IWeakEventSubscriber<PropertyChangedEventArgs>
{

2
src/Avalonia.Base/Data/Core/LogicalNotNode.cs

@ -3,7 +3,7 @@ using System.Globalization;
namespace Avalonia.Data.Core
{
public class LogicalNotNode : ExpressionNode, ITransformNode
internal class LogicalNotNode : ExpressionNode, ITransformNode
{
public override string Description => "!";

2
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -8,7 +8,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Reads a property from a <see cref="AvaloniaObject"/>.
/// </summary>
public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)]

28
src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs

@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace Avalonia.Data.Core.Plugins
{
/// <summary>
/// Holds a registry of plugins used for bindings.
/// </summary>
public static class BindingPlugins
{
/// <summary>
/// An ordered collection of property accessor plugins that can be used to customize
/// the reading and subscription of property values on a type.
/// </summary>
public static IList<IPropertyAccessorPlugin> PropertyAccessors => ExpressionObserver.PropertyAccessors;
/// <summary>
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
/// </summary>
public static IList<IDataValidationPlugin> DataValidators => ExpressionObserver.DataValidators;
/// <summary>
/// An ordered collection of stream plugins that can be used to customize the behavior
/// of the '^' stream binding operator.
/// </summary>
public static IList<IStreamPlugin> StreamHandlers => ExpressionObserver.StreamHandlers;
}
}

2
src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs

@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Validates properties on that have <see cref="ValidationAttribute"/>s.
/// </summary>
public class DataAnnotationsValidationPlugin : IDataValidationPlugin
internal class DataAnnotationsValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]

2
src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs

@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Validates properties that report errors by throwing exceptions.
/// </summary>
public class ExceptionValidationPlugin : IDataValidationPlugin
internal class ExceptionValidationPlugin : IDataValidationPlugin
{
/// <inheritdoc/>
[RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)]

2
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// <summary>
/// Validates properties on objects that implement <see cref="INotifyDataErrorInfo"/>.
/// </summary>
public class IndeiValidationPlugin : IDataValidationPlugin
internal class IndeiValidationPlugin : IDataValidationPlugin
{
private static readonly WeakEvent<INotifyDataErrorInfo, DataErrorsChangedEventArgs>
ErrorsChangedWeakEvent = WeakEvent.Register<INotifyDataErrorInfo, DataErrorsChangedEventArgs>(

2
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -11,7 +11,7 @@ namespace Avalonia.Data.Core.Plugins
/// Reads a property from a standard C# object that optionally supports the
/// <see cref="INotifyPropertyChanged"/> interface.
/// </summary>
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup =
new Dictionary<(Type, string), PropertyInfo?>();

2
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@ -6,7 +6,7 @@ using System.Reflection;
namespace Avalonia.Data.Core.Plugins
{
public class MethodAccessorPlugin : IPropertyAccessorPlugin
internal class MethodAccessorPlugin : IPropertyAccessorPlugin
{
private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup =
new Dictionary<(Type, string), MethodInfo?>();

4
src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs

@ -10,12 +10,12 @@ namespace Avalonia.Data.Core.Plugins
/// Handles binding to <see cref="IObservable{T}"/>s for the '^' stream binding operator.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
public class ObservableStreamPlugin : IStreamPlugin
internal class ObservableStreamPlugin : IStreamPlugin
{
private static MethodInfo? s_observableGeneric;
private static MethodInfo? s_observableSelect;
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicProperties, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")]
[DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, "Avalonia.Data.Core.Plugins.ObservableStreamPlugin", "Avalonia.Base")]
public ObservableStreamPlugin()
{

2
src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs

@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins
/// Handles binding to <see cref="Task"/>s for the '^' stream binding operator.
/// </summary>
[UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)]
public class TaskStreamPlugin : IStreamPlugin
internal class TaskStreamPlugin : IStreamPlugin
{
/// <summary>
/// Checks whether this plugin handles the specified value.

2
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -5,7 +5,7 @@ using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public class PropertyAccessorNode : SettableNode
internal class PropertyAccessorNode : SettableNode
{
private readonly bool _enableValidation;
private IPropertyAccessorPlugin? _customPlugin;

2
src/Avalonia.Base/Data/Core/SettableNode.cs

@ -2,7 +2,7 @@
namespace Avalonia.Data.Core
{
public abstract class SettableNode : ExpressionNode
internal abstract class SettableNode : ExpressionNode
{
public bool SetTargetValue(object? value, BindingPriority priority)
{

2
src/Avalonia.Base/Data/Core/StreamNode.cs

@ -6,7 +6,7 @@ using Avalonia.Reactive;
namespace Avalonia.Data.Core
{
[RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
public class StreamNode : ExpressionNode
internal class StreamNode : ExpressionNode
{
private IStreamPlugin? _customPlugin = null;
private IDisposable? _subscription;

2
src/Avalonia.Base/Data/Core/TypeCastNode.cs

@ -4,7 +4,7 @@ using System.Text;
namespace Avalonia.Data.Core
{
public class TypeCastNode : ExpressionNode
internal class TypeCastNode : ExpressionNode
{
public override string Description => $"as {TargetType.FullName}";

43
src/Avalonia.Base/Data/InstancedBinding.cs

@ -1,5 +1,6 @@
using System;
using Avalonia.Reactive;
using ObservableEx = Avalonia.Reactive.Observable;
namespace Avalonia.Data
{
@ -14,11 +15,23 @@ namespace Avalonia.Data
/// </remarks>
public class InstancedBinding
{
internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority)
/// <summary>
/// Initializes a new instance of the <see cref="InstancedBinding"/> class.
/// </summary>
/// <param name="source">The binding source.</param>
/// <param name="mode">The binding mode.</param>
/// <param name="priority">The priority of the binding.</param>
/// <remarks>
/// This constructor can be used to create any type of binding and as such requires an
/// <see cref="ISubject{Object}"/> as the binding source because this is the only binding
/// source which can be used for all binding modes. If you wish to create an instance with
/// something other than a subject, use one of the static creation methods on this class.
/// </remarks>
internal InstancedBinding(IObservable<object?> source, BindingMode mode, BindingPriority priority)
{
Mode = mode;
Priority = priority;
Value = value;
Source = source ?? throw new ArgumentNullException(nameof(source));
}
/// <summary>
@ -32,24 +45,12 @@ namespace Avalonia.Data
public BindingPriority Priority { get; }
/// <summary>
/// Gets the value or source of the binding.
/// </summary>
public object? Value { get; }
/// <summary>
/// Gets the <see cref="Value"/> as an observable.
/// Gets the binding source observable.
/// </summary>
public IObservable<object?>? Observable => Value as IObservable<object?>;
public IObservable<object?> Source { get; }
/// <summary>
/// Gets the <see cref="Value"/> as an observer.
/// </summary>
public IObserver<object?>? Observer => Value as IObserver<object?>;
/// <summary>
/// Gets the <see cref="Subject"/> as an subject.
/// </summary>
internal IAvaloniaSubject<object?>? Subject => Value as IAvaloniaSubject<object?>;
[Obsolete("Use Source property")]
public IObservable<object?> Observable => Source;
/// <summary>
/// Creates a new one-time binding with a fixed value.
@ -61,7 +62,7 @@ namespace Avalonia.Data
object value,
BindingPriority priority = BindingPriority.LocalValue)
{
return new InstancedBinding(value, BindingMode.OneTime, priority);
return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority);
}
/// <summary>
@ -106,7 +107,7 @@ namespace Avalonia.Data
{
_ = observer ?? throw new ArgumentNullException(nameof(observer));
return new InstancedBinding(observer, BindingMode.OneWayToSource, priority);
return new InstancedBinding((IObservable<object?>)observer, BindingMode.OneWayToSource, priority);
}
/// <summary>
@ -135,7 +136,7 @@ namespace Avalonia.Data
/// <returns>An <see cref="InstancedBinding"/> instance.</returns>
public InstancedBinding WithPriority(BindingPriority priority)
{
return new InstancedBinding(Value, Mode, priority);
return new InstancedBinding(Source, Mode, priority);
}
}
}

13
src/Avalonia.Base/DirectProperty.cs

@ -33,6 +33,8 @@ namespace Avalonia
{
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter;
IsDirect = true;
IsReadOnly = setter is null;
}
/// <summary>
@ -51,17 +53,10 @@ namespace Avalonia
{
Getter = getter ?? throw new ArgumentNullException(nameof(getter));
Setter = setter;
IsDirect = true;
IsReadOnly = setter is null;
}
/// <inheritdoc/>
public override bool IsDirect => true;
/// <inheritdoc/>
public override bool IsReadOnly => Setter == null;
/// <inheritdoc/>
public override Type Owner => typeof(TOwner);
/// <summary>
/// Gets the getter function.
/// </summary>

6
src/Avalonia.Base/DirectPropertyBase.cs

@ -1,8 +1,6 @@
using System;
using Avalonia.Data;
using Avalonia.PropertyStore;
using Avalonia.Reactive;
using Avalonia.Styling;
namespace Avalonia
{
@ -28,6 +26,7 @@ namespace Avalonia
AvaloniaPropertyMetadata metadata)
: base(name, ownerType, metadata)
{
Owner = ownerType;
}
/// <summary>
@ -42,12 +41,13 @@ namespace Avalonia
AvaloniaPropertyMetadata metadata)
: base(source, ownerType, metadata)
{
Owner = ownerType;
}
/// <summary>
/// Gets the type that registered the property.
/// </summary>
public abstract Type Owner { get; }
public Type Owner { get; }
/// <summary>
/// Gets the value of the property on the instance.

2
src/Avalonia.Base/Logging/TraceLogSink.cs

@ -141,7 +141,7 @@ namespace Avalonia.Logging
result.Append(')');
}
return result.ToString();
return StringBuilderCache.GetStringAndRelease(result);
}
}
}

2
src/Avalonia.Base/Media/DrawingContext.cs

@ -246,7 +246,7 @@ namespace Avalonia.Media
if (foreground != null)
{
PlatformImpl.DrawGlyphRun(foreground, glyphRun);
PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl);
}
}

5
src/Avalonia.Base/Media/DrawingGroup.cs

@ -167,18 +167,17 @@ namespace Avalonia.Media
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
public void DrawGlyphRun(IBrush foreground, IRef<IGlyphRunImpl> glyphRun)
{
if (foreground == null || glyphRun == null)
{
return;
}
// Add a GlyphRunDrawing to the Drawing graph
GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing
{
Foreground = foreground,
GlyphRun = glyphRun,
GlyphRun = new GlyphRun(glyphRun)
};
// Add Drawing to the Drawing graph

2
src/Avalonia.Base/Media/FontManager.cs

@ -132,7 +132,7 @@ namespace Avalonia.Media
{
typeface = new Typeface(fallback.FontFamily, fontStyle, fontWeight, fontStretch);
var glyphTypeface = typeface.GlyphTypeface;
var glyphTypeface = GetOrAddGlyphTypeface(typeface);
if(glyphTypeface.TryGetGlyph((uint)codepoint, out _)){
return true;

74
src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs

@ -1,13 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Avalonia.Utilities;
namespace Avalonia.Media.Fonts
{
public sealed class FamilyNameCollection : IReadOnlyList<string>
{
private readonly string[] _names;
/// <summary>
/// Initializes a new instance of the <see cref="FamilyNameCollection"/> class.
/// </summary>
@ -20,13 +21,20 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames));
}
Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
_names = SplitNames(familyNames);
PrimaryFamilyName = Names[0];
PrimaryFamilyName = _names[0];
HasFallbacks = Names.Count > 1;
HasFallbacks = _names.Length > 1;
}
private static string[] SplitNames(string names)
#if NET6_0_OR_GREATER
=> names.Split(',', StringSplitOptions.TrimEntries);
#else
=> Array.ConvertAll(names.Split(','), p => p.Trim());
#endif
/// <summary>
/// Gets the primary family name.
/// </summary>
@ -43,14 +51,6 @@ namespace Avalonia.Media.Fonts
/// </value>
public bool HasFallbacks { get; }
/// <summary>
/// Gets the internal collection of names.
/// </summary>
/// <value>
/// The names.
/// </value>
internal IReadOnlyList<string> Names { get; }
/// <summary>
/// Returns an enumerator for the name collection.
/// </summary>
@ -76,23 +76,7 @@ namespace Avalonia.Media.Fonts
/// A <see cref="string" /> that represents this instance.
/// </returns>
public override string ToString()
{
var builder = StringBuilderCache.Acquire();
for (var index = 0; index < Names.Count; index++)
{
builder.Append(Names[index]);
if (index == Names.Count - 1)
{
break;
}
builder.Append(", ");
}
return StringBuilderCache.GetStringAndRelease(builder);
}
=> String.Join(", ", _names);
/// <summary>
/// Returns a hash code for this instance.
@ -102,7 +86,7 @@ namespace Avalonia.Media.Fonts
/// </returns>
public override int GetHashCode()
{
if (Count == 0)
if (_names.Length == 0)
{
return 0;
}
@ -111,9 +95,9 @@ namespace Avalonia.Media.Fonts
{
int hash = 17;
for (var i = 0; i < Names.Count; i++)
for (var i = 0; i < _names.Length; i++)
{
string name = Names[i];
string name = _names[i];
hash = hash * 23 + name.GetHashCode();
}
@ -145,30 +129,10 @@ namespace Avalonia.Media.Fonts
/// <c>true</c> if the specified <see cref="object" /> is equal to this instance; otherwise, <c>false</c>.
/// </returns>
public override bool Equals(object? obj)
{
if (!(obj is FamilyNameCollection other))
{
return false;
}
if (other.Count != Count)
{
return false;
}
for (int i = 0; i < Count; i++)
{
if (Names[i] != other.Names[i])
{
return false;
}
}
return true;
}
=> obj is FamilyNameCollection other && _names.AsSpan().SequenceEqual(other._names);
public int Count => Names.Count;
public int Count => _names.Length;
public string this[int index] => Names[index];
public string this[int index] => _names[index];
}
}

559
src/Avalonia.Base/Media/GlyphRun.cs

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -11,64 +13,118 @@ namespace Avalonia.Media
/// </summary>
public sealed class GlyphRun : IDisposable
{
private static readonly IComparer<int> s_ascendingComparer = Comparer<int>.Default;
private static readonly IComparer<int> s_descendingComparer = new ReverseComparer<int>();
private IGlyphRunImpl? _glyphRunImpl;
private IGlyphTypeface _glyphTypeface;
private IRef<IGlyphRunImpl>? _platformImpl;
private double _fontRenderingEmSize;
private int _biDiLevel;
private Point? _baselineOrigin;
private GlyphRunMetrics? _glyphRunMetrics;
private ReadOnlyMemory<char> _characters;
private IReadOnlyList<ushort> _glyphIndices;
private IReadOnlyList<double>? _glyphAdvances;
private IReadOnlyList<Vector>? _glyphOffsets;
private IReadOnlyList<int>? _glyphClusters;
private IReadOnlyList<GlyphInfo> _glyphInfos;
private bool _hasOneCharPerCluster; // if true, character index and cluster are similar
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="glyphAdvances">The glyph advances.</param>
/// <param name="glyphOffsets">The glyph offsets.</param>
/// <param name="characters">The characters.</param>
/// <param name="glyphClusters">The glyph clusters.</param>
/// <param name="glyphIndices">The glyph indices.</param>
/// <param name="biDiLevel">The bidi level.</param>
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlyMemory<char> characters,
IReadOnlyList<ushort> glyphIndices,
IReadOnlyList<double>? glyphAdvances = null,
IReadOnlyList<Vector>? glyphOffsets = null,
IReadOnlyList<int>? glyphClusters = null,
int biDiLevel = 0)
: this(glyphTypeface, fontRenderingEmSize, characters,
CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel)
{
_hasOneCharPerCluster = true;
}
/// <summary>
/// Initializes a new instance of the <see cref="GlyphRun"/> class by specifying properties of the class.
/// </summary>
/// <param name="glyphTypeface">The glyph typeface.</param>
/// <param name="fontRenderingEmSize">The rendering em size.</param>
/// <param name="characters">The characters.</param>
/// <param name="glyphInfos">The list of glyphs used.</param>
/// <param name="biDiLevel">The bidi level.</param>
public GlyphRun(
IGlyphTypeface glyphTypeface,
double fontRenderingEmSize,
ReadOnlyMemory<char> characters,
IReadOnlyList<GlyphInfo> glyphInfos,
int biDiLevel = 0)
{
_glyphTypeface = glyphTypeface;
GlyphTypeface = glyphTypeface;
_fontRenderingEmSize = fontRenderingEmSize;
_characters = characters;
_glyphIndices = glyphIndices;
_glyphInfos = glyphInfos;
_glyphAdvances = glyphAdvances;
_biDiLevel = biDiLevel;
}
_glyphOffsets = glyphOffsets;
internal GlyphRun(IRef<IGlyphRunImpl> platformImpl)
{
_glyphInfos = Array.Empty<GlyphInfo>();
GlyphTypeface = Typeface.Default.GlyphTypeface;
_platformImpl = platformImpl;
}
private static IReadOnlyList<GlyphInfo> CreateGlyphInfos(IReadOnlyList<ushort> glyphIndices,
double fontRenderingEmSize, IGlyphTypeface glyphTypeface)
{
var glyphIndexSpan = ListToSpan(glyphIndices);
var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan);
_glyphClusters = glyphClusters;
var glyphInfos = new GlyphInfo[glyphIndexSpan.Length];
var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight;
_biDiLevel = biDiLevel;
for (var i = 0; i < glyphIndexSpan.Length; ++i)
{
glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale);
}
return glyphInfos;
}
private static ReadOnlySpan<ushort> ListToSpan(IReadOnlyList<ushort> list)
{
var count = list.Count;
if (count == 0)
{
return default;
}
if (list is ushort[] array)
{
return array.AsSpan();
}
#if NET6_0_OR_GREATER
if (list is List<ushort> concreteList)
{
return CollectionsMarshal.AsSpan(concreteList);
}
#endif
array = new ushort[count];
for (var i = 0; i < count; ++i)
{
array[i] = list[i];
}
return array.AsSpan();
}
/// <summary>
/// Gets the <see cref="IGlyphTypeface"/> for the <see cref="GlyphRun"/>.
/// </summary>
public IGlyphTypeface GlyphTypeface => _glyphTypeface;
public IGlyphTypeface GlyphTypeface { get; }
/// <summary>
/// Gets or sets the em size used for rendering the <see cref="GlyphRun"/>.
@ -82,61 +138,18 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the conservative bounding box of the <see cref="GlyphRun"/>.
/// </summary>
public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height);
public Size Size => PlatformImpl.Item.Size;
/// <summary>
///
/// </summary>
public GlyphRunMetrics Metrics
{
get
{
_glyphRunMetrics ??= CreateGlyphRunMetrics();
return _glyphRunMetrics.Value;
}
}
=> _glyphRunMetrics ??= CreateGlyphRunMetrics();
/// <summary>
/// Gets or sets the baseline origin of the<see cref="GlyphRun"/>.
/// Gets the baseline origin of the<see cref="GlyphRun"/>.
/// </summary>
public Point BaselineOrigin
{
get
{
_baselineOrigin ??= CalculateBaselineOrigin();
return _baselineOrigin.Value;
}
set => Set(ref _baselineOrigin, value);
}
/// <summary>
/// Gets or sets an array of <see cref="ushort"/> values that represent the glyph indices in the rendering physical font.
/// </summary>
public IReadOnlyList<ushort> GlyphIndices
{
get => _glyphIndices;
set => Set(ref _glyphIndices, value);
}
/// <summary>
/// Gets or sets an array of <see cref="double"/> values that represent the advances corresponding to the glyph indices.
/// </summary>
public IReadOnlyList<double>? GlyphAdvances
{
get => _glyphAdvances;
set => Set(ref _glyphAdvances, value);
}
/// <summary>
/// Gets or sets an array of <see cref="Vector"/> values representing the offsets of the glyphs in the <see cref="GlyphRun"/>.
/// </summary>
public IReadOnlyList<Vector>? GlyphOffsets
{
get => _glyphOffsets;
set => Set(ref _glyphOffsets, value);
}
public Point BaselineOrigin => PlatformImpl.Item.BaselineOrigin;
/// <summary>
/// Gets or sets the list of UTF16 code points that represent the Unicode content of the <see cref="GlyphRun"/>.
@ -148,12 +161,16 @@ namespace Avalonia.Media
}
/// <summary>
/// Gets or sets a list of <see cref="int"/> values representing a mapping from character index to glyph index.
/// Gets or sets the list of glyphs to use to render this run.
/// </summary>
public IReadOnlyList<int>? GlyphClusters
public IReadOnlyList<GlyphInfo> GlyphInfos
{
get => _glyphClusters;
set => Set(ref _glyphClusters, value);
get => _glyphInfos;
set
{
Set(ref _glyphInfos, value);
_hasOneCharPerCluster = false;
}
}
/// <summary>
@ -178,18 +195,8 @@ namespace Avalonia.Media
/// <summary>
/// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary>
public IGlyphRunImpl GlyphRunImpl
{
get
{
if (_glyphRunImpl == null)
{
Initialize();
}
return _glyphRunImpl!;
}
}
public IRef<IGlyphRunImpl> PlatformImpl
=> _platformImpl ??= CreateGlyphRunImpl();
/// <summary>
/// Obtains geometry for the glyph run.
@ -221,38 +228,32 @@ namespace Avalonia.Media
if (IsLeftToRight)
{
if (GlyphClusters != null)
if (characterIndex < Metrics.FirstCluster)
{
if (characterIndex < Metrics.FirstCluster)
{
return 0;
}
return 0;
}
if (characterIndex > Metrics.LastCluster)
{
return Metrics.WidthIncludingTrailingWhitespace;
}
if (characterIndex > Metrics.LastCluster)
{
return Size.Width;
}
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null)
{
var currentCluster = GlyphClusters[glyphIndex];
var currentCluster = _glyphInfos[glyphIndex].GlyphCluster;
//Move to the end of the glyph cluster
if (characterHit.TrailingLength > 0)
//Move to the end of the glyph cluster
if (characterHit.TrailingLength > 0)
{
while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster)
{
while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster)
{
glyphIndex++;
}
glyphIndex++;
}
}
for (var i = 0; i < glyphIndex; i++)
{
distance += GetGlyphAdvance(i, out _);
distance += _glyphInfos[i].GlyphAdvance;
}
return distance;
@ -262,22 +263,19 @@ namespace Avalonia.Media
//RightToLeft
var glyphIndex = FindGlyphIndex(characterIndex);
if (GlyphClusters != null && GlyphClusters.Count > 0)
if (characterIndex > Metrics.LastCluster)
{
if (characterIndex > Metrics.LastCluster)
{
return 0;
}
return 0;
}
if (characterIndex <= Metrics.FirstCluster)
{
return Size.Width;
}
if (characterIndex <= Metrics.FirstCluster)
{
return Size.Width;
}
for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++)
for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++)
{
distance += GetGlyphAdvance(i, out _);
distance += _glyphInfos[i].GlyphAdvance;
}
return Size.Width - distance;
@ -322,11 +320,12 @@ namespace Avalonia.Media
if (IsLeftToRight)
{
for (var index = 0; index < GlyphIndices.Count; index++)
for (var index = 0; index < _glyphInfos.Count; index++)
{
var advance = GetGlyphAdvance(index, out var cluster);
var glyphInfo = _glyphInfos[index];
var advance = glyphInfo.GlyphAdvance;
characterIndex = cluster;
characterIndex = glyphInfo.GlyphCluster;
if (distance > currentX && distance <= currentX + advance)
{
@ -340,11 +339,12 @@ namespace Avalonia.Media
{
currentX = Size.Width;
for (var index = GlyphIndices.Count - 1; index >= 0; index--)
for (var index = _glyphInfos.Count - 1; index >= 0; index--)
{
var advance = GetGlyphAdvance(index, out var cluster);
var glyphInfo = _glyphInfos[index];
var advance = glyphInfo.GlyphAdvance;
characterIndex = cluster;
characterIndex = glyphInfo.GlyphCluster;
var offsetX = currentX - advance;
@ -424,7 +424,7 @@ namespace Avalonia.Media
/// </returns>
public int FindGlyphIndex(int characterIndex)
{
if (GlyphClusters == null || GlyphClusters.Count == 0)
if (_hasOneCharPerCluster)
{
return characterIndex;
}
@ -433,7 +433,7 @@ namespace Avalonia.Media
{
if (IsLeftToRight)
{
return GlyphIndices.Count - 1;
return _glyphInfos.Count - 1;
}
return 0;
@ -446,15 +446,13 @@ namespace Avalonia.Media
return 0;
}
return GlyphIndices.Count - 1;
return _glyphInfos.Count - 1;
}
var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer;
var clusters = GlyphClusters;
var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer;
// Find the start of the cluster at the character index.
var start = clusters.BinarySearch(characterIndex, comparer);
var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer);
// No cluster found.
if (start < 0)
@ -463,40 +461,38 @@ namespace Avalonia.Media
{
characterIndex--;
start = clusters.BinarySearch(characterIndex, comparer);
start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer);
}
if (start < 0)
{
goto result;
return 0;
}
}
if (IsLeftToRight)
{
while (start > 0 && clusters[start - 1] == clusters[start])
while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
{
start--;
}
}
else
{
while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start])
while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster)
{
start++;
}
}
result:
if (start < 0)
{
return 0;
}
if (start > GlyphIndices.Count - 1)
if (start > _glyphInfos.Count - 1)
{
return GlyphIndices.Count - 1;
return _glyphInfos.Count - 1;
}
return start;
@ -516,14 +512,14 @@ namespace Avalonia.Media
var glyphIndex = FindGlyphIndex(index);
if (GlyphClusters == null)
if (_hasOneCharPerCluster)
{
width = GetGlyphAdvance(index, out _);
width = _glyphInfos[index].GlyphAdvance;
return new CharacterHit(glyphIndex, 1);
}
var cluster = GlyphClusters[glyphIndex];
var cluster = _glyphInfos[glyphIndex].GlyphCluster;
var nextCluster = cluster;
@ -531,13 +527,13 @@ namespace Avalonia.Media
while (nextCluster == cluster)
{
width += GetGlyphAdvance(currentIndex, out _);
width += _glyphInfos[currentIndex].GlyphAdvance;
if (IsLeftToRight)
{
currentIndex++;
if (currentIndex == GlyphClusters.Count)
if (currentIndex == _glyphInfos.Count)
{
break;
}
@ -552,7 +548,7 @@ namespace Avalonia.Media
}
}
nextCluster = GlyphClusters[currentIndex];
nextCluster = _glyphInfos[currentIndex].GlyphCluster;
}
var clusterLength = Math.Max(0, nextCluster - cluster);
@ -565,9 +561,9 @@ namespace Avalonia.Media
if (IsLeftToRight)
{
for (int i = 1; i < GlyphClusters.Count; i++)
for (int i = 1; i < _glyphInfos.Count; i++)
{
nextCluster = GlyphClusters[i];
nextCluster = _glyphInfos[i].GlyphCluster;
if (currentCluster > cluster)
{
@ -583,9 +579,9 @@ namespace Avalonia.Media
}
else
{
for (int i = GlyphClusters.Count - 1; i >= 0; i--)
for (int i = _glyphInfos.Count - 1; i >= 0; i--)
{
nextCluster = GlyphClusters[i];
nextCluster = _glyphInfos[i].GlyphCluster;
if (currentCluster > cluster)
{
@ -613,51 +609,19 @@ namespace Avalonia.Media
return new CharacterHit(cluster, clusterLength);
}
/// <summary>
/// Gets a glyph's width.
/// </summary>
/// <param name="index">The glyph index.</param>
/// <param name="cluster">The current cluster.</param>
/// <returns>The glyph's width.</returns>
private double GetGlyphAdvance(int index, out int cluster)
{
cluster = GlyphClusters != null ? GlyphClusters[index] : index;
if (GlyphAdvances != null)
{
return GlyphAdvances[index];
}
var glyph = GlyphIndices[index];
return GlyphTypeface.GetGlyphAdvance(glyph) * Scale;
}
/// <summary>
/// Calculates the default baseline origin of the <see cref="GlyphRun"/>.
/// </summary>
/// <returns>The baseline origin.</returns>
private Point CalculateBaselineOrigin()
{
return new Point(0, -GlyphTypeface.Metrics.Ascent * Scale);
}
private GlyphRunMetrics CreateGlyphRunMetrics()
{
int firstCluster = 0, lastCluster = 0;
int firstCluster, lastCluster;
if (_glyphClusters != null && _glyphClusters.Count > 0)
if (Characters.IsEmpty)
{
firstCluster = _glyphClusters[0];
lastCluster = _glyphClusters[_glyphClusters.Count - 1];
firstCluster = 0;
lastCluster = 0;
}
else
{
if (!Characters.IsEmpty)
{
firstCluster = 0;
lastCluster = Characters.Length - 1;
}
firstCluster = _glyphInfos[0].GlyphCluster;
lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster;
}
if (!IsLeftToRight)
@ -671,9 +635,9 @@ namespace Avalonia.Media
var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount);
for (var index = 0; index < GlyphIndices.Count; index++)
for (var index = 0; index < _glyphInfos.Count; index++)
{
var advance = GetGlyphAdvance(index, out _);
var advance = _glyphInfos[index].GlyphAdvance;
widthIncludingTrailingWhitespace += advance;
}
@ -684,21 +648,19 @@ namespace Avalonia.Media
{
for (var index = 0; index < glyphCount; index++)
{
width -= GetGlyphAdvance(index, out _);
width -= _glyphInfos[index].GlyphAdvance;
}
}
else
{
for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++)
for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++)
{
width -= GetGlyphAdvance(index, out _);
width -= _glyphInfos[index].GlyphAdvance;
}
}
return new GlyphRunMetrics(
width,
widthIncludingTrailingWhitespace,
height,
trailingWhitespaceLength,
newLineLength,
firstCluster,
@ -710,7 +672,7 @@ namespace Avalonia.Media
{
if (isReversed)
{
return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount);
}
glyphCount = 0;
@ -720,84 +682,59 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
for (var i = charactersSpan.Length - 1; i >= 0;)
{
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
var characterIndex = charactersSpan.Length - 1;
trailingWhitespaceLength++;
i -= count;
glyphCount++;
}
}
else
for (var i = _glyphInfos.Count - 1; i >= 0; i--)
{
var characterIndex = charactersSpan.Length - 1;
var currentCluster = _glyphInfos[i].GlyphCluster;
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
for (var i = GlyphClusters.Count - 1; i >= 0; i--)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex -= characterLength;
characterIndex -= characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
var clusterLength = 1;
while (i - 1 >= 0)
{
var nextCluster = _glyphInfos[i - 1].GlyphCluster;
while (i - 1 >= 0)
if (currentCluster == nextCluster)
{
var nextCluster = GlyphClusters[i - 1];
clusterLength++;
i--;
if (currentCluster == nextCluster)
if(characterIndex >= 0)
{
clusterLength++;
i--;
if(characterIndex >= 0)
{
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength);
characterIndex -= characterLength;
}
continue;
characterIndex -= characterLength;
}
break;
}
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
continue;
}
trailingWhitespaceLength += clusterLength;
break;
}
glyphCount++;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount++;
}
}
return trailingWhitespaceLength;
}
private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount)
{
glyphCount = 0;
newLineLength = 0;
@ -806,71 +743,46 @@ namespace Avalonia.Media
if (!charactersSpan.IsEmpty)
{
if (GlyphClusters == null)
{
for (var i = 0; i < charactersSpan.Length;)
{
var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count);
var characterIndex = 0;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (codepoint.IsBreakChar)
{
newLineLength++;
}
trailingWhitespaceLength++;
i += count;
glyphCount++;
}
}
else
for (var i = 0; i < _glyphInfos.Count; i++)
{
var characterIndex = 0;
var currentCluster = _glyphInfos[i].GlyphCluster;
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
for (var i = 0; i < GlyphClusters.Count; i++)
{
var currentCluster = GlyphClusters[i];
var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength);
characterIndex += characterLength;
characterIndex += characterLength;
if (!codepoint.IsWhiteSpace)
{
break;
}
if (!codepoint.IsWhiteSpace)
{
break;
}
var clusterLength = 1;
var clusterLength = 1;
var j = i;
var j = i;
while (j - 1 >= 0)
{
var nextCluster = _glyphInfos[--j].GlyphCluster;
while (j - 1 >= 0)
if (currentCluster == nextCluster)
{
var nextCluster = GlyphClusters[--j];
if (currentCluster == nextCluster)
{
clusterLength++;
continue;
}
break;
}
clusterLength++;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
continue;
}
trailingWhitespaceLength += clusterLength;
break;
}
glyphCount += clusterLength;
if (codepoint.IsBreakChar)
{
newLineLength += clusterLength;
}
trailingWhitespaceLength += clusterLength;
glyphCount += clusterLength;
}
}
@ -879,55 +791,30 @@ namespace Avalonia.Media
private void Set<T>(ref T field, T value)
{
_glyphRunImpl?.Dispose();
_platformImpl?.Dispose();
_glyphRunImpl = null;
_platformImpl = null;
_glyphRunMetrics = null;
_baselineOrigin = null;
field = value;
}
/// <summary>
/// Initializes the <see cref="GlyphRun"/>.
/// </summary>
private void Initialize()
private IRef<IGlyphRunImpl> CreateGlyphRunImpl()
{
if (GlyphIndices == null)
{
throw new InvalidOperationException();
}
var glyphCount = GlyphIndices.Count;
if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount)
{
throw new InvalidOperationException();
}
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount)
{
throw new InvalidOperationException();
}
var platformImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos);
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
_platformImpl = RefCountable.Create(platformImpl);
_glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets);
return _platformImpl;
}
public void Dispose()
{
_glyphRunImpl?.Dispose();
}
private class ReverseComparer<T> : IComparer<T>
{
public int Compare(T? x, T? y)
{
return Comparer<T>.Default.Compare(y, x);
}
_platformImpl?.Dispose();
_platformImpl = null;
}
}
}

15
src/Avalonia.Base/Media/GlyphRunMetrics.cs

@ -2,27 +2,20 @@
{
public readonly record struct GlyphRunMetrics
{
public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height,
int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
public GlyphRunMetrics(double width, int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster)
{
Width = width;
WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace;
Height = height;
TrailingWhitespaceLength = trailingWhitespaceLength;
NewLineLength= newLineLength;
NewLineLength = newLineLength;
FirstCluster = firstCluster;
LastCluster = lastCluster;
}
public double Width { get; }
public double WidthIncludingTrailingWhitespace { get; }
public double Height { get; }
public int TrailingWhitespaceLength { get; }
public int NewLineLength { get; }
public int NewLineLength { get; }
public int FirstCluster { get; }

2
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -182,7 +182,7 @@ namespace Avalonia.Media
/// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IImmutableBrush foreground, GlyphRun glyphRun)
public void DrawGlyphRun(IImmutableBrush foreground, IRef<IGlyphRunImpl> glyphRun)
{
_ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun));

2
src/Avalonia.Base/Media/TextDecoration.cs

@ -218,7 +218,7 @@ namespace Avalonia.Media
{
var offsetY = glyphRun.BaselineOrigin.Y - origin.Y;
var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY));
var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY));
if (intersections != null && intersections.Count > 0)
{

268
src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs

@ -0,0 +1,268 @@
using System;
using System.Diagnostics;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Reorders text runs according to their bidi level.
/// </summary>
/// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
internal sealed class BidiReorderer
{
[ThreadStatic] private static BidiReorderer? t_instance;
private ArrayBuilder<OrderedBidiRun> _runs;
private ArrayBuilder<BidiRange> _ranges;
public static BidiReorderer Instance
=> t_instance ??= new();
public void BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection)
{
Debug.Assert(_runs.Length == 0);
Debug.Assert(_ranges.Length == 0);
if (textRuns.IsEmpty)
{
return;
}
try
{
_runs.Add(textRuns.Length);
// Build up the collection of ordered runs.
for (var i = 0; i < textRuns.Length; i++)
{
var textRun = textRuns[i];
_runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection));
if (i > 0)
{
_runs[i - 1].NextRunIndex = i;
}
}
// Reorder them into visual order.
var firstIndex = LinearReorder();
// Now perform a recursive reversal of each run.
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels
// not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
// https://unicode.org/reports/tr9/#L2
sbyte max = 0;
var min = sbyte.MaxValue;
for (var i = 0; i < textRuns.Length; i++)
{
var level = GetRunBidiLevel(textRuns[i], flowDirection);
if (level > max)
{
max = level;
}
if ((level & 1) != 0 && level < min)
{
min = level;
}
}
if (min > max)
{
min = max;
}
if (max == 0 || (min == max && (max & 1) == 0))
{
// Nothing to reverse.
return;
}
// Now apply the reversal and replace the original contents.
var minLevelToReverse = max;
int currentIndex;
while (minLevelToReverse >= min)
{
currentIndex = firstIndex;
while (currentIndex >= 0)
{
ref var current = ref _runs[currentIndex];
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
{
shapedTextCharacters.Reverse();
}
}
currentIndex = current.NextRunIndex;
}
minLevelToReverse--;
}
var index = 0;
currentIndex = firstIndex;
while (currentIndex >= 0)
{
ref var current = ref _runs[currentIndex];
textRuns[index++] = current.Run;
currentIndex = current.NextRunIndex;
}
}
finally
{
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges);
}
}
private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextRun shapedTextRun)
{
return shapedTextRun.BidiLevel;
}
var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
return (sbyte)defaultLevel;
}
/// <summary>
/// Reorders the runs from logical to visual order.
/// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
/// </summary>
/// <returns>The first run index in visual order.</returns>
private int LinearReorder()
{
var runIndex = 0;
var rangeIndex = -1;
while (runIndex >= 0)
{
ref var run = ref _runs[runIndex];
var nextRunIndex = run.NextRunIndex;
while (rangeIndex >= 0
&& _ranges[rangeIndex].Level > run.Level
&& _ranges[rangeIndex].PreviousRangeIndex >= 0
&& _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level)
{
rangeIndex = MergeRangeWithPrevious(rangeIndex);
}
if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level)
{
// Attach run to the range.
if ((run.Level & 1) != 0)
{
// Odd, range goes to the right of run.
run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex;
_ranges[rangeIndex].LeftRunIndex = runIndex;
}
else
{
// Even, range goes to the left of run.
_runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex;
_ranges[rangeIndex].RightRunIndex = runIndex;
}
_ranges[rangeIndex].Level = run.Level;
}
else
{
var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex);
_ranges.AddItem(r);
rangeIndex = _ranges.Length - 1;
}
runIndex = nextRunIndex;
}
while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0)
{
rangeIndex = MergeRangeWithPrevious(rangeIndex);
}
// Terminate.
_runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1;
return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex;
}
private int MergeRangeWithPrevious(int index)
{
var previousIndex = _ranges[index].PreviousRangeIndex;
ref var previous = ref _ranges[previousIndex];
int leftIndex;
int rightIndex;
if ((previous.Level & 1) != 0)
{
// Odd, previous goes to the right of range.
leftIndex = index;
rightIndex = previousIndex;
}
else
{
// Even, previous goes to the left of range.
leftIndex = previousIndex;
rightIndex = index;
}
// Stitch them
ref var left = ref _ranges[leftIndex];
ref var right = ref _ranges[rightIndex];
_runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex;
previous.LeftRunIndex = left.LeftRunIndex;
previous.RightRunIndex = right.RightRunIndex;
return previousIndex;
}
private struct OrderedBidiRun
{
public OrderedBidiRun(int runIndex, TextRun run, sbyte level)
{
RunIndex = runIndex;
Run = run;
Level = level;
NextRunIndex = -1;
}
public int RunIndex { get; }
public sbyte Level { get; }
public TextRun Run { get; }
public int NextRunIndex { get; set; } // -1 if none
}
private struct BidiRange
{
public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex)
{
Level = level;
LeftRunIndex = leftRunIndex;
RightRunIndex = rightRunIndex;
PreviousRangeIndex = previousRangeIndex;
}
public sbyte Level { get; set; }
public int LeftRunIndex { get; set; }
public int RightRunIndex { get; set; }
public int PreviousRangeIndex { get; } // -1 if none
}
}
}

6
src/Avalonia.Base/Media/TextFormatting/FormattedTextSource.cs

@ -128,11 +128,9 @@ namespace Avalonia.Media.TextFormatting
var graphemeEnumerator = new GraphemeEnumerator(text);
while (graphemeEnumerator.MoveNext())
while (graphemeEnumerator.MoveNext(out var grapheme))
{
var grapheme = graphemeEnumerator.Current;
finalLength += grapheme.Text.Length;
finalLength += grapheme.Length;
if (finalLength >= length)
{

84
src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs

@ -0,0 +1,84 @@
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal static class FormattingBufferHelper
{
// 1MB, arbitrary, that's 512K characters or 128K object references on x64
private const long MaxKeptBufferSizeInBytes = 1024 * 1024;
public static void ClearThenResetIfTooLarge<T>(ref ArrayBuilder<T> arrayBuilder)
{
arrayBuilder.Clear();
if (IsBufferTooLarge<T>((uint) arrayBuilder.Capacity))
{
arrayBuilder = default;
}
}
public static void ClearThenResetIfTooLarge<T>(List<T> list)
{
list.Clear();
if (IsBufferTooLarge<T>((uint) list.Capacity))
{
list.TrimExcess();
}
}
public static void ClearThenResetIfTooLarge<T>(Stack<T> stack)
{
var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count);
stack.Clear();
if (IsBufferTooLarge<T>(approximateCapacity))
{
stack.TrimExcess();
}
}
public static void ClearThenResetIfTooLarge<TKey, TValue>(ref Dictionary<TKey, TValue> dictionary)
where TKey : notnull
{
var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count);
dictionary.Clear();
// dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here
if (IsBufferTooLarge<KeyValuePair<TKey, TValue>>(approximateCapacity))
{
#if NET6_0_OR_GREATER
dictionary.TrimExcess();
#else
dictionary = new Dictionary<TKey, TValue>();
#endif
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsBufferTooLarge<T>(uint capacity)
=> (long) (uint) Unsafe.SizeOf<T>() * capacity > MaxKeptBufferSizeInBytes;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RoundUpToPowerOf2(uint value)
{
#if NET6_0_OR_GREATER
return BitOperations.RoundUpToPowerOf2(value);
#else
// Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
--value;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return value + 1;
#endif
}
}
}

138
src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// <para>Contains various list pools that are commonly used during text layout.</para>
/// <para>
/// This class provides an instance per thread.
/// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout.
/// </para>
/// </summary>
/// <seealso cref="RentedList{T}"/>
internal sealed class FormattingObjectPool
{
[ThreadStatic] private static FormattingObjectPool? t_instance;
/// <summary>
/// Gets an instance of this class for the current thread.
/// </summary>
/// <remarks>
/// Since this is backed by a thread static field which is slower than a normal static field,
/// prefer passing the instance around when possible instead of calling this property each time.
/// </remarks>
public static FormattingObjectPool Instance
=> t_instance ??= new();
public ListPool<TextRun> TextRunLists { get; } = new();
public ListPool<UnshapedTextRun> UnshapedTextRunLists { get; } = new();
public ListPool<TextLine> TextLines { get; } = new();
[Conditional("DEBUG")]
public void VerifyAllReturned()
{
TextRunLists.VerifyAllReturned();
UnshapedTextRunLists.VerifyAllReturned();
TextLines.VerifyAllReturned();
}
internal sealed class ListPool<T>
{
// we don't need a big number here, these are for temporary usages only which should quickly be returned
private const int MaxSize = 16;
private readonly RentedList<T>[] _lists = new RentedList<T>[MaxSize];
private int _size;
private int _pendingReturnCount;
/// <summary>
/// Rents a list.
/// See <see cref="RentedList{T}"/> for the intended usages.
/// </summary>
/// <returns>A rented list instance that must be returned to the pool.</returns>
/// <seealso cref="RentedList{T}"/>
public RentedList<T> Rent()
{
var list = _size > 0 ? _lists[--_size] : new RentedList<T>();
Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!");
++_pendingReturnCount;
return list;
}
/// <summary>
/// Returns a rented list to the pool.
/// </summary>
/// <param name="rentedList">
/// On input, the list to return.
/// On output, the reference is set to null to avoid misuse.
/// </param>
public void Return(ref RentedList<T>? rentedList)
{
if (rentedList is null)
{
return;
}
--_pendingReturnCount;
FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList);
if (_size < MaxSize)
{
_lists[_size++] = rentedList;
}
rentedList = null;
}
[Conditional("DEBUG")]
public void VerifyAllReturned()
{
var pendingReturnCount = _pendingReturnCount;
_pendingReturnCount = 0;
if (pendingReturnCount > 0)
{
throw new InvalidOperationException(
$"{pendingReturnCount} RentedList<{typeof(T).Name}> haven't been returned to the pool!");
}
if (pendingReturnCount < 0)
{
throw new InvalidOperationException(
$"{-pendingReturnCount} RentedList<{typeof(T).Name}> extra lists have been returned to the pool!");
}
}
}
/// <summary>
/// <para>Represents a list that has been rented through <see cref="FormattingObjectPool"/>.</para>
/// <para>
/// This class can be used when a temporary list is needed to store some items during text layout.
/// It can also be used as a reusable array builder by calling <see cref="List{T}.ToArray"/> when done.
/// </para>
/// <list type="bullet">
/// <item>NEVER use an instance of this type after it's been returned to the pool.</item>
/// <item>AVOID storing an instance of this type into a field or property.</item>
/// <item>AVOID casting an instance of this type to another type.</item>
/// <item>
/// AVOID passing an instance of this type as an argument to a method expecting a standard list,
/// unless you're absolutely sure it won't store it.
/// </item>
/// <item>
/// If you call a method returning an instance of this type,
/// you're now responsible for returning it to the pool.
/// </item>
/// </list>
/// </summary>
/// <typeparam name="T">The type of elements in the list.</typeparam>
internal sealed class RentedList<T> : List<T>
{
}
}
}

36
src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs

@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Represents a single glyph.
/// </summary>
public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default)
{
internal static Comparer<GlyphInfo> ClusterAscendingComparer { get; } =
Comparer<GlyphInfo>.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster));
internal static Comparer<GlyphInfo> ClusterDescendingComparer { get; } =
Comparer<GlyphInfo>.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster));
/// <summary>
/// Get the glyph index.
/// </summary>
public ushort GlyphIndex { get; } = GlyphIndex;
/// <summary>
/// Get the glyph cluster.
/// </summary>
public int GlyphCluster { get; } = GlyphCluster;
/// <summary>
/// Get the glyph advance.
/// </summary>
public double GlyphAdvance { get; } = GlyphAdvance;
/// <summary>
/// Get the glyph offset.
/// </summary>
public Vector GlyphOffset { get; } = GlyphOffset;
}
}

9
src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs

@ -48,8 +48,9 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = textLine.FirstTextSourceIndex;
foreach (var textRun in lineImpl.TextRuns)
for (var i = 0; i < lineImpl.TextRuns.Count; ++i)
{
var textRun = lineImpl.TextRuns[i];
var text = textRun.Text;
if (text.IsEmpty)
@ -59,10 +60,8 @@ namespace Avalonia.Media.TextFormatting
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
while (lineBreakEnumerator.MoveNext(out var currentBreak))
{
var currentBreak = lineBreakEnumerator.Current;
if (!currentBreak.Required && currentBreak.PositionWrap != textRun.Length)
{
breakOportunities.Enqueue(currentPosition + currentBreak.PositionMeasure);
@ -111,7 +110,7 @@ namespace Avalonia.Media.TextFormatting
shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing);
}
glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances;
glyphRun.GlyphInfos = shapedBuffer.GlyphInfos;
}
currentPosition += textRun.Length;

230
src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs

@ -1,24 +1,23 @@
using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
public sealed class ShapedBuffer : IList<GlyphInfo>, IDisposable
public sealed class ShapedBuffer : IReadOnlyList<GlyphInfo>, IDisposable
{
private static readonly IComparer<GlyphInfo> s_clusterComparer = new CompareClusters();
private bool _bufferRented;
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) :
this(text,
new ArraySlice<GlyphInfo>(ArrayPool<GlyphInfo>.Shared.Rent(bufferLength), 0, bufferLength),
glyphTypeface,
fontRenderingEmSize,
bidiLevel)
private GlyphInfo[]? _rentedBuffer;
public ShapedBuffer(ReadOnlyMemory<char> text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
{
_bufferRented = true;
Length = bufferLength;
_rentedBuffer = ArrayPool<GlyphInfo>.Shared.Rent(bufferLength);
Text = text;
GlyphInfos = new ArraySlice<GlyphInfo>(_rentedBuffer, 0, bufferLength);
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
}
internal ShapedBuffer(ReadOnlyMemory<char> text, ArraySlice<GlyphInfo> glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel)
@ -28,12 +27,12 @@ namespace Avalonia.Media.TextFormatting
GlyphTypeface = glyphTypeface;
FontRenderingEmSize = fontRenderingEmSize;
BidiLevel = bidiLevel;
Length = GlyphInfos.Length;
}
internal ArraySlice<GlyphInfo> GlyphInfos { get; }
public int Length { get; }
internal ArraySlice<GlyphInfo> GlyphInfos { get; private set; }
public int Length
=> GlyphInfos.Length;
public IGlyphTypeface GlyphTypeface { get; }
@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting
public sbyte BidiLevel { get; }
public bool IsLeftToRight => (BidiLevel & 1) == 0;
public IReadOnlyList<ushort> GlyphIndices => new GlyphIndexList(GlyphInfos);
public IReadOnlyList<int> GlyphClusters => new GlyphClusterList(GlyphInfos);
public IReadOnlyList<double> GlyphAdvances => new GlyphAdvanceList(GlyphInfos);
public IReadOnlyList<Vector> GlyphOffsets => new GlyphOffsetList(GlyphInfos);
public ReadOnlyMemory<char> Text { get; }
@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting
}
var comparer = s_clusterComparer;
var comparer = GlyphInfo.ClusterAscendingComparer;
var clusters = GlyphInfos.Span;
var glyphInfos = GlyphInfos.Span;
var searchValue = new GlyphInfo(0, characterIndex);
var searchValue = new GlyphInfo(default, characterIndex, default);
var start = clusters.BinarySearch(searchValue, comparer);
var start = glyphInfos.BinarySearch(searchValue, comparer);
if (start < 0)
{
@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting
{
characterIndex--;
searchValue = new GlyphInfo(0, characterIndex);
searchValue = new GlyphInfo(default, characterIndex, default);
start = clusters.BinarySearch(searchValue, comparer);
start = glyphInfos.BinarySearch(searchValue, comparer);
}
if (start < 0)
@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting
}
}
while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster)
while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster)
{
start--;
}
@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(this, null);
}
var firstCluster = GlyphClusters[0];
var lastCluster = GlyphClusters[GlyphClusters.Count - 1];
var firstCluster = GlyphInfos[0].GlyphCluster;
var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster;
var start = firstCluster < lastCluster ? firstCluster : lastCluster;
@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting
return new SplitResult<ShapedBuffer>(first, second);
}
int ICollection<GlyphInfo>.Count => throw new NotImplementedException();
bool ICollection<GlyphInfo>.IsReadOnly => true;
int IReadOnlyCollection<GlyphInfo>.Count => GlyphInfos.Length;
public GlyphInfo this[int index]
{
@ -144,177 +133,18 @@ namespace Avalonia.Media.TextFormatting
set => GlyphInfos[index] = value;
}
int IList<GlyphInfo>.IndexOf(GlyphInfo item)
{
throw new NotImplementedException();
}
void IList<GlyphInfo>.Insert(int index, GlyphInfo item)
{
throw new NotImplementedException();
}
void IList<GlyphInfo>.RemoveAt(int index)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.Add(GlyphInfo item)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.Clear()
{
throw new NotImplementedException();
}
bool ICollection<GlyphInfo>.Contains(GlyphInfo item)
{
throw new NotImplementedException();
}
void ICollection<GlyphInfo>.CopyTo(GlyphInfo[] array, int arrayIndex)
{
throw new NotImplementedException();
}
bool ICollection<GlyphInfo>.Remove(GlyphInfo item)
{
throw new NotImplementedException();
}
public IEnumerator<GlyphInfo> GetEnumerator() => GlyphInfos.GetEnumerator();
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
private class CompareClusters : IComparer<GlyphInfo>
{
private static readonly Comparer<int> s_intClusterComparer = Comparer<int>.Default;
public int Compare(GlyphInfo x, GlyphInfo y)
{
return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster);
}
}
private readonly struct GlyphAdvanceList : IReadOnlyList<double>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphAdvanceList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public double this[int index] => _glyphInfos[index].GlyphAdvance;
public int Count => _glyphInfos.Length;
public IEnumerator<double> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<double>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphIndexList : IReadOnlyList<ushort>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphIndexList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public ushort this[int index] => _glyphInfos[index].GlyphIndex;
public int Count => _glyphInfos.Length;
public IEnumerator<ushort> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<ushort>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphClusterList : IReadOnlyList<int>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphClusterList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public int this[int index] => _glyphInfos[index].GlyphCluster;
public int Count => _glyphInfos.Length;
public IEnumerator<int> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<int>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
private readonly struct GlyphOffsetList : IReadOnlyList<Vector>
{
private readonly ArraySlice<GlyphInfo> _glyphInfos;
public GlyphOffsetList(ArraySlice<GlyphInfo> glyphInfos)
{
_glyphInfos = glyphInfos;
}
public Vector this[int index] => _glyphInfos[index].GlyphOffset;
public int Count => _glyphInfos.Length;
public IEnumerator<Vector> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<Vector>(this);
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Dispose()
{
GC.SuppressFinalize(this);
if (_bufferRented)
if (_rentedBuffer is not null)
{
GlyphInfos.ReturnRent();
ArrayPool<GlyphInfo>.Shared.Return(_rentedBuffer);
_rentedBuffer = null;
GlyphInfos = ArraySlice<GlyphInfo>.Empty; // ensure we don't misuse the returned array
}
}
~ShapedBuffer()
{
if (_bufferRented)
{
GlyphInfos.ReturnRent();
}
}
}
public readonly record struct GlyphInfo
{
public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default)
{
GlyphIndex = glyphIndex;
GlyphAdvance = glyphAdvance;
GlyphCluster = glyphCluster;
GlyphOffset = glyphOffset;
}
/// <summary>
/// Get the glyph index.
/// </summary>
public ushort GlyphIndex { get; }
/// <summary>
/// Get the glyph cluster.
/// </summary>
public int GlyphCluster { get; }
/// <summary>
/// Get the glyph advance.
/// </summary>
public double GlyphAdvance { get; }
/// <summary>
/// Get the glyph offset.
/// </summary>
public Vector GlyphOffset { get; }
}
}

26
src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs

@ -14,7 +14,7 @@ namespace Avalonia.Media.TextFormatting
{
ShapedBuffer = shapedBuffer;
Properties = properties;
TextMetrics = new TextMetrics(properties.Typeface.GlyphTypeface, properties.FontRenderingEmSize);
TextMetrics = new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
}
public bool IsReversed { get; private set; }
@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting
public override Size Size => GlyphRun.Size;
public GlyphRun GlyphRun
{
get
{
if(_glyphRun is null)
{
_glyphRun = CreateGlyphRun();
}
return _glyphRun;
}
}
public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun();
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin)
{
using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin)))
{
if (GlyphRun.GlyphIndices.Count == 0)
if (GlyphRun.GlyphInfos.Count == 0)
{
return;
}
@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = 0; i < ShapedBuffer.Length; i++)
{
var advance = ShapedBuffer.GlyphAdvances[i];
var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
if (currentWidth + advance > availableWidth)
{
@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting
for (var i = ShapedBuffer.Length - 1; i >= 0; i--)
{
var advance = ShapedBuffer.GlyphAdvances[i];
var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance;
if (width + advance > availableWidth)
{
@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting
ShapedBuffer.GlyphTypeface,
ShapedBuffer.FontRenderingEmSize,
Text,
ShapedBuffer.GlyphIndices,
ShapedBuffer.GlyphAdvances,
ShapedBuffer.GlyphOffsets,
ShapedBuffer.GlyphClusters,
ShapedBuffer,
BidiLevel);
}

11
src/Avalonia.Base/Media/TextFormatting/SplitResult.cs

@ -26,5 +26,16 @@
/// The second part.
/// </value>
public T? Second { get; }
/// <summary>
/// Deconstructs the split results into its components.
/// </summary>
/// <param name="first">On return, contains the first part.</param>
/// <param name="second">On return, contains the second part.</param>
public void Deconstruct(out T first, out T? second)
{
first = First;
second = Second;
}
}
}

90
src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
using static Avalonia.Media.TextFormatting.FormattingObjectPool;
namespace Avalonia.Media.TextFormatting
{
@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting
/// Gets a list of <see cref="UnshapedTextRun"/>.
/// </summary>
/// <returns>The shapeable text characters.</returns>
internal IReadOnlyList<UnshapedTextRun> GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
ref TextRunProperties? previousProperties)
internal void GetShapeableCharacters(ReadOnlyMemory<char> text, sbyte biDiLevel,
FontManager fontManager, ref TextRunProperties? previousProperties, RentedList<TextRun> results)
{
var shapeableCharacters = new List<UnshapedTextRun>(2);
var properties = Properties;
while (!text.IsEmpty)
{
var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties);
var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, fontManager, ref previousProperties);
shapeableCharacters.Add(shapeableRun);
results.Add(shapeableRun);
text = text.Slice(shapeableRun.Length);
previousProperties = shapeableRun.Properties;
}
return shapeableCharacters;
}
/// <summary>
@ -72,37 +69,40 @@ namespace Avalonia.Media.TextFormatting
/// <param name="text">The characters to create text runs from.</param>
/// <param name="defaultProperties">The default text run properties.</param>
/// <param name="biDiLevel">The bidi level of the run.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="previousProperties"></param>
/// <returns>A list of shapeable text runs.</returns>
private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory<char> text,
TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties? previousProperties)
TextRunProperties defaultProperties, sbyte biDiLevel, FontManager fontManager,
ref TextRunProperties? previousProperties)
{
var defaultTypeface = defaultProperties.Typeface;
var currentTypeface = defaultTypeface;
var defaultGlyphTypeface = defaultProperties.CachedGlyphTypeface;
var previousTypeface = previousProperties?.Typeface;
var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface;
var textSpan = text.Span;
if (TryGetShapeableLength(textSpan, currentTypeface, null, out var count, out var script))
if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script))
{
if (script == Script.Common && previousTypeface is not null)
if (script == Script.Common && previousGlyphTypeface is not null)
{
if (TryGetShapeableLength(textSpan, previousTypeface.Value, null, out var fallbackCount, out _))
if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _))
{
return new UnshapedTextRun(text.Slice(0, fallbackCount),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
}
}
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface),
biDiLevel);
}
if (previousTypeface is not null)
if (previousGlyphTypeface is not null)
{
if (TryGetShapeableLength(textSpan, previousTypeface.Value, defaultTypeface, out count, out _))
if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _))
{
return new UnshapedTextRun(text.Slice(0, count),
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel);
}
}
@ -110,48 +110,44 @@ namespace Avalonia.Media.TextFormatting
var codepointEnumerator = new CodepointEnumerator(text.Slice(count).Span);
while (codepointEnumerator.MoveNext())
while (codepointEnumerator.MoveNext(out var cp))
{
if (codepointEnumerator.Current.IsWhiteSpace)
if (cp.IsWhiteSpace)
{
continue;
}
codepoint = codepointEnumerator.Current;
codepoint = cp;
break;
}
//ToDo: Fix FontFamily fallback
var matchFound =
FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight,
defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo,
out currentTypeface);
out var fallbackTypeface);
if (matchFound && TryGetShapeableLength(textSpan, currentTypeface, defaultTypeface, out count, out _))
var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface);
if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _))
{
//Fallback found
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(currentTypeface),
return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface),
biDiLevel);
}
// no fallback found
currentTypeface = defaultTypeface;
var glyphTypeface = currentTypeface.GlyphTypeface;
var enumerator = new GraphemeEnumerator(textSpan);
while (enumerator.MoveNext())
while (enumerator.MoveNext(out var grapheme))
{
var grapheme = enumerator.Current;
if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
if (!grapheme.FirstCodepoint.IsWhiteSpace && defaultGlyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _))
{
break;
}
count += grapheme.Text.Length;
count += grapheme.Length;
}
return new UnshapedTextRun(text.Slice(0, count), defaultProperties, biDiLevel);
@ -161,15 +157,15 @@ namespace Avalonia.Media.TextFormatting
/// Tries to get a shapeable length that is supported by the specified typeface.
/// </summary>
/// <param name="text">The characters to shape.</param>
/// <param name="typeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultTypeface"></param>
/// <param name="glyphTypeface">The typeface that is used to find matching characters.</param>
/// <param name="defaultGlyphTypeface">The default typeface.</param>
/// <param name="length">The shapeable length.</param>
/// <param name="script"></param>
/// <returns></returns>
internal static bool TryGetShapeableLength(
ReadOnlySpan<char> text,
Typeface typeface,
Typeface? defaultTypeface,
IGlyphTypeface glyphTypeface,
IGlyphTypeface? defaultGlyphTypeface,
out int length,
out Script script)
{
@ -181,24 +177,22 @@ namespace Avalonia.Media.TextFormatting
return false;
}
var font = typeface.GlyphTypeface;
var defaultFont = defaultTypeface?.GlyphTypeface;
var enumerator = new GraphemeEnumerator(text);
while (enumerator.MoveNext())
while (enumerator.MoveNext(out var currentGrapheme))
{
var currentGrapheme = enumerator.Current;
var currentScript = currentGrapheme.FirstCodepoint.Script;
var currentCodepoint = currentGrapheme.FirstCodepoint;
var currentScript = currentCodepoint.Script;
if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
if (!currentCodepoint.IsWhiteSpace
&& defaultGlyphTypeface != null
&& defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _))
{
break;
}
//Stop at the first missing glyph
if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _))
if (!currentCodepoint.IsBreakChar && !glyphTypeface.TryGetGlyph(currentCodepoint, out _))
{
break;
}
@ -219,7 +213,7 @@ namespace Avalonia.Media.TextFormatting
}
}
length += currentGrapheme.Text.Length;
length += currentGrapheme.Length;
}
return length > 0;

6
src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs

@ -1,6 +1,4 @@
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing.
@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting
/// Collapses given text line.
/// </summary>
/// <param name="textLine">Text line to collapse.</param>
public abstract List<TextRun>? Collapse(TextLine textLine);
public abstract TextRun[]? Collapse(TextLine textLine);
}
}

65
src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs

@ -1,15 +1,16 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
{
internal static class TextEllipsisHelper
{
public static List<TextRun>? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis)
{
var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0)
if (textRuns.Count == 0)
{
return null;
}
@ -22,7 +23,7 @@ namespace Avalonia.Media.TextFormatting
if (properties.Width < shapedSymbol.GlyphRun.Size.Width)
{
//Not enough space to fit in the symbol
return new List<TextRun>(0);
return Array.Empty<TextRun>();
}
var availableWidth = properties.Width - shapedSymbol.Size.Width;
@ -47,9 +48,9 @@ namespace Avalonia.Media.TextFormatting
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext())
while (currentBreakPosition < measuredLength && lineBreaker.MoveNext(out var lineBreak))
{
var nextBreakPosition = lineBreaker.Current.PositionMeasure;
var nextBreakPosition = lineBreak.PositionMeasure;
if (nextBreakPosition == 0)
{
@ -70,18 +71,7 @@ namespace Avalonia.Media.TextFormatting
collapsedLength += measuredLength;
var collapsedRuns = new List<TextRun>(textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
}
availableWidth -= shapedRun.Size.Width;
@ -94,18 +84,7 @@ namespace Avalonia.Media.TextFormatting
//The whole run needs to fit into available space
if (currentWidth + drawableRun.Size.Width > availableWidth)
{
var collapsedRuns = new List<TextRun>(textRuns.Count);
if (collapsedLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength);
collapsedRuns.AddRange(splitResult.First);
}
collapsedRuns.Add(shapedSymbol);
return collapsedRuns;
return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol);
}
availableWidth -= drawableRun.Size.Width;
@ -121,5 +100,31 @@ namespace Avalonia.Media.TextFormatting
return null;
}
private static TextRun[] CreateCollapsedRuns(IReadOnlyList<TextRun> textRuns, int collapsedLength,
TextRun shapedSymbol)
{
if (collapsedLength <= 0)
{
return new[] { shapedSymbol };
}
var objectPool = FormattingObjectPool.Instance;
var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool);
try
{
var collapsedRuns = new TextRun[preSplitRuns.Count + 1];
preSplitRuns.CopyTo(collapsedRuns);
collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol;
return collapsedRuns;
}
finally
{
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
}
}
}
}

339
src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs

@ -1,15 +1,22 @@
using System;
// ReSharper disable ForCanBeConvertedToForeach
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Utilities;
using static Avalonia.Media.TextFormatting.FormattingObjectPool;
namespace Avalonia.Media.TextFormatting
{
internal class TextFormatterImpl : TextFormatter
internal sealed class TextFormatterImpl : TextFormatter
{
private static readonly char[] s_empty = { ' ' };
private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength];
[ThreadStatic] private static BidiData? t_bidiData;
[ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm;
/// <inheritdoc cref="TextFormatter.FormatLine"/>
public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth,
@ -18,52 +25,71 @@ namespace Avalonia.Media.TextFormatting
var textWrapping = paragraphProperties.TextWrapping;
FlowDirection resolvedFlowDirection;
TextLineBreak? nextLineBreak = null;
IReadOnlyList<TextRun> textRuns;
IReadOnlyList<TextRun>? textRuns;
var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex,
var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool,
out var textEndOfLine, out var textSourceLength);
if (previousLineBreak?.RemainingRuns != null)
{
resolvedFlowDirection = previousLineBreak.FlowDirection;
textRuns = previousLineBreak.RemainingRuns;
nextLineBreak = previousLineBreak;
}
else
{
textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection);
RentedList<TextRun>? shapedTextRuns = null;
if (nextLineBreak == null && textEndOfLine != null)
try
{
if (previousLineBreak?.RemainingRuns is { } remainingRuns)
{
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
resolvedFlowDirection = previousLineBreak.FlowDirection;
textRuns = remainingRuns;
nextLineBreak = previousLineBreak;
shapedTextRuns = null;
}
}
else
{
shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager,
out resolvedFlowDirection);
textRuns = shapedTextRuns;
TextLineImpl textLine;
if (nextLineBreak == null && textEndOfLine != null)
{
nextLineBreak = new TextLineBreak(textEndOfLine, resolvedFlowDirection);
}
}
switch (textWrapping)
{
case TextWrapping.NoWrap:
TextLineImpl textLine;
switch (textWrapping)
{
case TextWrapping.NoWrap:
{
textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength,
// perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class
// which already uses an array: ToArray() won't ever be called in this case
var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray();
textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak);
textLine.FinalizeLine();
break;
}
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
case TextWrapping.WrapWithOverflow:
case TextWrapping.Wrap:
{
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties,
resolvedFlowDirection, nextLineBreak);
textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth,
paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool, fontManager);
break;
}
default:
throw new ArgumentOutOfRangeException(nameof(textWrapping));
}
default:
throw new ArgumentOutOfRangeException(nameof(textWrapping));
}
return textLine;
return textLine;
}
finally
{
objectPool.TextRunLists.Return(ref shapedTextRuns);
objectPool.TextRunLists.Return(ref fetchedRuns);
}
}
/// <summary>
@ -71,9 +97,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
/// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <returns>The split text runs.</returns>
internal static SplitResult<IReadOnlyList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length)
internal static SplitResult<RentedList<TextRun>> SplitTextRuns(IReadOnlyList<TextRun> textRuns, int length,
FormattingObjectPool objectPool)
{
var first = objectPool.TextRunLists.Rent();
var currentLength = 0;
for (var i = 0; i < textRuns.Count; i++)
@ -89,8 +118,6 @@ namespace Avalonia.Media.TextFormatting
var firstCount = currentRun.Length >= 1 ? i + 1 : i;
var first = new List<TextRun>(firstCount);
if (firstCount > 1)
{
for (var j = 0; j < i; j++)
@ -103,7 +130,7 @@ namespace Avalonia.Media.TextFormatting
if (currentLength + currentRun.Length == length)
{
var second = secondCount > 0 ? new List<TextRun>(secondCount) : null;
var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null;
if (second != null)
{
@ -117,13 +144,13 @@ namespace Avalonia.Media.TextFormatting
first.Add(currentRun);
return new SplitResult<IReadOnlyList<TextRun>>(first, second);
return new SplitResult<RentedList<TextRun>>(first, second);
}
else
{
secondCount++;
var second = new List<TextRun>(secondCount);
var second = objectPool.TextRunLists.Rent();
if (currentRun is ShapedTextRun shapedTextCharacters)
{
@ -139,11 +166,16 @@ namespace Avalonia.Media.TextFormatting
second.Add(textRuns[i + j]);
}
return new SplitResult<IReadOnlyList<TextRun>>(first, second);
return new SplitResult<RentedList<TextRun>>(first, second);
}
}
return new SplitResult<IReadOnlyList<TextRun>>(textRuns, null);
for (var i = 0; i < textRuns.Count; i++)
{
first.Add(textRuns[i]);
}
return new SplitResult<RentedList<TextRun>>(first, null);
}
/// <summary>
@ -152,51 +184,77 @@ namespace Avalonia.Media.TextFormatting
/// <param name="textRuns">The text runs to shape.</param>
/// <param name="paragraphProperties">The default paragraph properties.</param>
/// <param name="resolvedFlowDirection">The resolved flow direction.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns>
/// A list of shaped text characters.
/// </returns>
private static List<TextRun> ShapeTextRuns(List<TextRun> textRuns, TextParagraphProperties paragraphProperties,
out FlowDirection resolvedFlowDirection)
private static RentedList<TextRun> ShapeTextRuns(IReadOnlyList<TextRun> textRuns,
TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool,
FontManager fontManager, out FlowDirection resolvedFlowDirection)
{
var flowDirection = paragraphProperties.FlowDirection;
var shapedRuns = new List<TextRun>();
using var biDiData = new BidiData((sbyte)flowDirection);
var shapedRuns = objectPool.TextRunLists.Rent();
foreach (var textRun in textRuns)
if (textRuns.Count == 0)
{
if (textRun.Text.IsEmpty)
{
biDiData.Append(new char[textRun.Length]);
}
else
{
biDiData.Append(textRun.Text.Span);
}
resolvedFlowDirection = flowDirection;
return shapedRuns;
}
using var biDi = new BidiAlgorithm();
var bidiData = t_bidiData ??= new();
bidiData.Reset();
bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection;
biDi.Process(biDiData);
for (var i = 0; i < textRuns.Count; ++i)
{
var textRun = textRuns[i];
var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes);
ReadOnlySpan<char> text;
if (!textRun.Text.IsEmpty)
text = textRun.Text.Span;
else if (textRun.Length == TextRun.DefaultTextSourceLength)
text = s_defaultText;
else
text = new char[textRun.Length];
bidiData.Append(text);
}
var bidiAlgorithm = t_bidiAlgorithm ??= new();
bidiAlgorithm.Process(bidiData);
var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes);
resolvedFlowDirection =
(resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
var processedRuns = new List<TextRun>(textRuns.Count);
CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns);
var processedRuns = objectPool.TextRunLists.Rent();
var groupedRuns = objectPool.UnshapedTextRunLists.Rent();
for (var index = 0; index < processedRuns.Count; index++)
try
{
var currentRun = processedRuns[index];
CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, fontManager, processedRuns);
switch (currentRun)
bidiData.Reset();
bidiAlgorithm.Reset();
var textShaper = TextShaper.Current;
for (var index = 0; index < processedRuns.Count; index++)
{
case UnshapedTextRun shapeableRun:
var currentRun = processedRuns[index];
switch (currentRun)
{
case UnshapedTextRun shapeableRun:
{
var groupedRuns = new List<UnshapedTextRun>(2) { shapeableRun };
groupedRuns.Clear();
groupedRuns.Add(shapeableRun);
var text = shapeableRun.Text;
var properties = shapeableRun.Properties;
while (index + 1 < processedRuns.Count)
{
@ -207,7 +265,7 @@ namespace Avalonia.Media.TextFormatting
if (shapeableRun.BidiLevel == nextRun.BidiLevel
&& TryJoinContiguousMemories(text, nextRun.Text, out var joinedText)
&& CanShapeTogether(shapeableRun.Properties, nextRun.Properties))
&& CanShapeTogether(properties, nextRun.Properties))
{
groupedRuns.Add(nextRun);
index++;
@ -219,23 +277,29 @@ namespace Avalonia.Media.TextFormatting
break;
}
var shaperOptions = new TextShaperOptions(currentRun.Properties!.Typeface.GlyphTypeface,
currentRun.Properties.FontRenderingEmSize,
shapeableRun.BidiLevel, currentRun.Properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
var shaperOptions = new TextShaperOptions(
properties.CachedGlyphTypeface,
properties.FontRenderingEmSize, shapeableRun.BidiLevel, properties.CultureInfo,
paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing);
shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions));
ShapeTogether(groupedRuns, text, shaperOptions, textShaper, shapedRuns);
break;
}
default:
default:
{
shapedRuns.Add(currentRun);
break;
}
}
}
}
finally
{
objectPool.TextRunLists.Return(ref processedRuns);
objectPool.UnshapedTextRunLists.Return(ref groupedRuns);
}
return shapedRuns;
}
@ -303,18 +367,15 @@ namespace Avalonia.Media.TextFormatting
}
}
private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y)
=> MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize)
&& x.Typeface == y.Typeface
&& x.BaselineAlignment == y.BaselineAlignment;
private static IReadOnlyList<ShapedTextRun> ShapeTogether(
IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text, TextShaperOptions options)
private static void ShapeTogether(IReadOnlyList<UnshapedTextRun> textRuns, ReadOnlyMemory<char> text,
TextShaperOptions options, TextShaper textShaper, RentedList<TextRun> results)
{
var shapedRuns = new List<ShapedTextRun>(textRuns.Count);
var shapedBuffer = TextShaper.Current.ShapeText(text, options);
var shapedBuffer = textShaper.ShapeText(text, options);
for (var i = 0; i < textRuns.Count; i++)
{
@ -322,12 +383,10 @@ namespace Avalonia.Media.TextFormatting
var splitResult = shapedBuffer.Split(currentRun.Length);
shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties));
shapedBuffer = splitResult.Second!;
}
return shapedRuns;
}
/// <summary>
@ -335,10 +394,11 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
/// <param name="textCharacters">The text characters to form <see cref="UnshapedTextRun"/> from.</param>
/// <param name="levels">The bidi levels.</param>
/// <param name="processedRuns"></param>
/// <param name="fontManager">The font manager to use.</param>
/// <param name="processedRuns">A list that will be filled with the processed runs.</param>
/// <returns></returns>
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ArraySlice<sbyte> levels,
List<TextRun> processedRuns)
private static void CoalesceLevels(IReadOnlyList<TextRun> textCharacters, ReadOnlySpan<sbyte> levels,
FontManager fontManager, RentedList<TextRun> processedRuns)
{
if (levels.Length == 0)
{
@ -385,7 +445,8 @@ namespace Avalonia.Media.TextFormatting
if (j == runTextSpan.Length)
{
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager,
ref previousProperties, processedRuns);
runLevel = levels[levelIndex];
@ -398,7 +459,8 @@ namespace Avalonia.Media.TextFormatting
}
// End of this run
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties));
currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, fontManager,
ref previousProperties, processedRuns);
runText = runText.Slice(j);
runTextSpan = runText.Span;
@ -415,7 +477,7 @@ namespace Avalonia.Media.TextFormatting
return;
}
processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties));
currentRun.GetShapeableCharacters(runText, runLevel, fontManager, ref previousProperties, processedRuns);
}
/// <summary>
@ -423,19 +485,20 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="firstTextSourceIndex">The first text source index.</param>
/// <param name="endOfLine"></param>
/// <param name="textSourceLength"></param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="endOfLine">On return, the end of line, if any.</param>
/// <param name="textSourceLength">On return, the processed text source length.</param>
/// <returns>
/// The formatted text runs.
/// </returns>
private static List<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
out TextEndOfLine? endOfLine, out int textSourceLength)
private static RentedList<TextRun> FetchTextRuns(ITextSource textSource, int firstTextSourceIndex,
FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength)
{
textSourceLength = 0;
endOfLine = null;
var textRuns = new List<TextRun>();
var textRuns = objectPool.TextRunLists.Rent();
var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex);
@ -509,15 +572,13 @@ namespace Avalonia.Media.TextFormatting
var lineBreakEnumerator = new LineBreakEnumerator(text.Span);
while (lineBreakEnumerator.MoveNext())
while (lineBreakEnumerator.MoveNext(out lineBreak))
{
if (!lineBreakEnumerator.Current.Required)
if (!lineBreak.Required)
{
continue;
}
lineBreak = lineBreakEnumerator.Current;
return lineBreak.PositionWrap >= textRun.Length || true;
}
@ -529,8 +590,10 @@ namespace Avalonia.Media.TextFormatting
measuredLength = 0;
var currentWidth = 0.0;
foreach (var currentRun in textRuns)
for (var i = 0; i < textRuns.Count; ++i)
{
var currentRun = textRuns[i];
switch (currentRun)
{
case ShapedTextRun shapedTextCharacters:
@ -540,15 +603,15 @@ namespace Avalonia.Media.TextFormatting
var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster;
var lastCluster = firstCluster;
for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++)
for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++)
{
var glyphInfo = shapedTextCharacters.ShapedBuffer[i];
var glyphInfo = shapedTextCharacters.ShapedBuffer[j];
if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth)
{
measuredLength += Math.Max(0, lastCluster - firstCluster);
goto found;
return measuredLength != 0;
}
lastCluster = glyphInfo.GlyphCluster;
@ -565,7 +628,7 @@ namespace Avalonia.Media.TextFormatting
{
if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth)
{
goto found;
return measuredLength != 0;
}
measuredLength += currentRun.Length;
@ -582,8 +645,6 @@ namespace Avalonia.Media.TextFormatting
}
}
found:
return measuredLength != 0;
}
@ -591,20 +652,23 @@ namespace Avalonia.Media.TextFormatting
/// Creates an empty text line.
/// </summary>
/// <returns>The empty text line.</returns>
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties)
public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth,
TextParagraphProperties paragraphProperties, FontManager fontManager)
{
var flowDirection = paragraphProperties.FlowDirection;
var properties = paragraphProperties.DefaultTextRunProperties;
var glyphTypeface = properties.Typeface.GlyphTypeface;
var glyphTypeface = properties.CachedGlyphTypeface;
var glyph = glyphTypeface.GetGlyph(s_empty[0]);
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) };
var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) };
var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize,
(sbyte)flowDirection);
var textRuns = new List<DrawableTextRun> { new ShapedTextRun(shapedBuffer, properties) };
var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) };
return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine();
var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection);
line.FinalizeLine();
return line;
}
/// <summary>
@ -616,14 +680,16 @@ namespace Avalonia.Media.TextFormatting
/// <param name="paragraphProperties">The text paragraph properties.</param>
/// <param name="resolvedFlowDirection"></param>
/// <param name="currentLineBreak">The current line break if the line was explicitly broken.</param>
/// <param name="objectPool">A pool used to get reusable formatting objects.</param>
/// <param name="fontManager">The font manager to use.</param>
/// <returns>The wrapped text line.</returns>
private static TextLineImpl PerformTextWrapping(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex,
double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection,
TextLineBreak? currentLineBreak)
TextLineBreak? currentLineBreak, FormattingObjectPool objectPool, FontManager fontManager)
{
if (textRuns.Count == 0)
{
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties);
return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, fontManager);
}
if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength))
@ -649,20 +715,20 @@ namespace Avalonia.Media.TextFormatting
{
var lineBreaker = new LineBreakEnumerator(currentRun.Text.Span);
while (lineBreaker.MoveNext())
while (lineBreaker.MoveNext(out var lineBreak))
{
if (lineBreaker.Current.Required &&
currentLength + lineBreaker.Current.PositionMeasure <= measuredLength)
if (lineBreak.Required &&
currentLength + lineBreak.PositionMeasure <= measuredLength)
{
//Explicit break found
breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
currentPosition = currentLength + lineBreak.PositionWrap;
break;
}
if (currentLength + lineBreaker.Current.PositionMeasure > measuredLength)
if (currentLength + lineBreak.PositionMeasure > measuredLength)
{
if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow)
{
@ -678,21 +744,21 @@ namespace Avalonia.Media.TextFormatting
//Find next possible wrap position (overflow)
if (index < textRuns.Count - 1)
{
if (lineBreaker.Current.PositionWrap != currentRun.Length)
if (lineBreak.PositionWrap != currentRun.Length)
{
//We already found the next possible wrap position.
breakFound = true;
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
currentPosition = currentLength + lineBreak.PositionWrap;
break;
}
while (lineBreaker.MoveNext() && index < textRuns.Count)
while (lineBreaker.MoveNext(out lineBreak) && index < textRuns.Count)
{
currentPosition += lineBreaker.Current.PositionWrap;
currentPosition += lineBreak.PositionWrap;
if (lineBreaker.Current.PositionWrap != currentRun.Length)
if (lineBreak.PositionWrap != currentRun.Length)
{
break;
}
@ -711,7 +777,7 @@ namespace Avalonia.Media.TextFormatting
}
else
{
currentPosition = currentLength + lineBreaker.Current.PositionWrap;
currentPosition = currentLength + lineBreak.PositionWrap;
}
breakFound = true;
@ -727,9 +793,9 @@ namespace Avalonia.Media.TextFormatting
break;
}
if (lineBreaker.Current.PositionMeasure != lineBreaker.Current.PositionWrap)
if (lineBreak.PositionMeasure != lineBreak.PositionWrap)
{
lastWrapPosition = currentLength + lineBreaker.Current.PositionWrap;
lastWrapPosition = currentLength + lineBreak.PositionWrap;
}
}
@ -749,24 +815,31 @@ namespace Avalonia.Media.TextFormatting
break;
}
var splitResult = SplitTextRuns(textRuns, measuredLength);
var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool);
try
{
var textLineBreak = postSplitRuns?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) :
null;
var remainingCharacters = splitResult.Second;
if (textLineBreak is null && currentLineBreak?.TextEndOfLine != null)
{
textLineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
}
var lineBreak = remainingCharacters?.Count > 0 ?
new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) :
null;
var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
textLineBreak);
if (lineBreak is null && currentLineBreak?.TextEndOfLine != null)
textLine.FinalizeLine();
return textLine;
}
finally
{
lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection);
objectPool.TextRunLists.Return(ref preSplitRuns);
objectPool.TextRunLists.Return(ref postSplitRuns);
}
var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength,
paragraphWidth, paragraphProperties, resolvedFlowDirection,
lineBreak);
return textLine.FinalizeLine();
}
private struct TextRunEnumerator
@ -816,7 +889,7 @@ namespace Avalonia.Media.TextFormatting
{
var textShaper = TextShaper.Current;
var glyphTypeface = textRun.Properties!.Typeface.GlyphTypeface;
var glyphTypeface = textRun.Properties!.CachedGlyphTypeface;
var fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;

206
src/Avalonia.Base/Media/TextFormatting/TextLayout.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting
private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties;
private readonly TextTrimming _textTrimming;
private readonly TextLine[] _textLines;
private int _textSourceLength;
@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
MaxLines = maxLines;
TextLines = CreateTextLines();
_textLines = CreateTextLines();
}
/// <summary>
@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting
MaxLines = maxLines;
TextLines = CreateTextLines();
_textLines = CreateTextLines();
}
/// <summary>
@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting
/// <value>
/// The text lines.
/// </value>
public IReadOnlyList<TextLine> TextLines { get; private set; }
public IReadOnlyList<TextLine> TextLines
=> _textLines;
/// <summary>
/// Gets the bounds of the layout.
@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting
/// <param name="origin">The origin.</param>
public void Draw(DrawingContext context, Point origin)
{
if (!TextLines.Any())
if (_textLines.Length == 0)
{
return;
}
var (currentX, currentY) = origin;
foreach (var textLine in TextLines)
foreach (var textLine in _textLines)
{
textLine.Draw(context, new Point(currentX + textLine.Start, currentY));
@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting
/// <returns></returns>
public Rect HitTestTextPosition(int textPosition)
{
if (TextLines.Count == 0)
if (_textLines.Length == 0)
{
return new Rect();
}
@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting
var currentY = 0.0;
foreach (var textLine in TextLines)
foreach (var textLine in _textLines)
{
var end = textLine.FirstTextSourceIndex + textLine.Length;
@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting
return Array.Empty<Rect>();
}
var result = new List<Rect>(TextLines.Count);
var result = new List<Rect>(_textLines.Length);
var currentY = 0d;
foreach (var textLine in TextLines)
foreach (var textLine in _textLines)
{
//Current line isn't covered.
if (textLine.FirstTextSourceIndex + textLine.Length < start)
@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting
{
var currentY = 0d;
var lineIndex = 0;
TextLine? currentLine = null;
CharacterHit characterHit;
for (; lineIndex < TextLines.Count; lineIndex++)
for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++)
{
currentLine = TextLines[lineIndex];
currentLine = _textLines[lineIndex];
if (currentY + currentLine.Height > point.Y)
{
@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting
if (charIndex > _textSourceLength)
{
return TextLines.Count - 1;
return _textLines.Length - 1;
}
for (var index = 0; index < TextLines.Count; index++)
for (var index = 0; index < _textLines.Length; index++)
{
var textLine = TextLines[index];
var textLine = _textLines[index];
if (textLine.FirstTextSourceIndex + textLine.Length < charIndex)
{
@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting
}
}
return TextLines.Count - 1;
return _textLines.Length - 1;
}
private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point)
@ -424,132 +424,150 @@ namespace Avalonia.Media.TextFormatting
height += textLine.Height;
}
private IReadOnlyList<TextLine> CreateTextLines()
private TextLine[] CreateTextLines()
{
var objectPool = FormattingObjectPool.Instance;
var fontManager = FontManager.Current;
if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight))
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties);
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties,
fontManager);
Bounds = new Rect(0, 0, 0, textLine.Height);
return new List<TextLine> { textLine };
return new TextLine[] { textLine };
}
var textLines = new List<TextLine>();
var textLines = objectPool.TextLines.Rent();
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
try
{
double left = double.PositiveInfinity, width = 0.0, height = 0.0;
_textSourceLength = 0;
_textSourceLength = 0;
TextLine? previousLine = null;
TextLine? previousLine = null;
while (true)
{
var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
var textFormatter = TextFormatter.Current;
if(textLine == null || textLine.Length == 0)
while (true)
{
if (previousLine != null && previousLine.NewLineLength > 0)
var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth,
_paragraphProperties, previousLine?.TextLineBreak);
if (textLine.Length == 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties);
if (previousLine != null && previousLine.NewLineLength > 0)
{
var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth,
_paragraphProperties, fontManager);
textLines.Add(emptyTextLine);
textLines.Add(emptyTextLine);
UpdateBounds(emptyTextLine, ref left, ref width, ref height);
}
UpdateBounds(emptyTextLine, ref left, ref width, ref height);
}
break;
}
break;
}
_textSourceLength += textLine.Length;
_textSourceLength += textLine.Length;
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight)
{
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
//Fulfill max height constraint
if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight)
&& height + textLine.Height > MaxHeight)
{
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
if (previousLine?.TextLineBreak != null && _textTrimming != TextTrimming.None)
{
var collapsedLine =
previousLine.Collapse(GetCollapsingProperties(MaxWidth));
textLines[textLines.Count - 1] = collapsedLine;
}
textLines[textLines.Count - 1] = collapsedLine;
}
break;
}
break;
}
var hasOverflowed = textLine.HasOverflowed;
var hasOverflowed = textLine.HasOverflowed;
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
}
if (hasOverflowed && _textTrimming != TextTrimming.None)
{
textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth));
}
textLines.Add(textLine);
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
UpdateBounds(textLine, ref left, ref width, ref height);
previousLine = textLine;
previousLine = textLine;
//Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines)
{
if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null)
//Fulfill max lines constraint
if (MaxLines > 0 && textLines.Count >= MaxLines)
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
if (textLine.TextLineBreak?.RemainingRuns is not null)
{
textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width));
}
break;
}
break;
if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
{
break;
}
}
if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfParagraph)
//Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{
break;
}
}
//Make sure the TextLayout always contains at least on empty line
if (textLines.Count == 0)
{
var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties);
var textLine =
TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, fontManager);
textLines.Add(textLine);
textLines.Add(textLine);
UpdateBounds(textLine, ref left, ref width, ref height);
}
Bounds = new Rect(left, 0, width, height);
UpdateBounds(textLine, ref left, ref width, ref height);
}
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var whitespaceWidth = 0d;
Bounds = new Rect(left, 0, width, height);
foreach (var line in textLines)
if (_paragraphProperties.TextAlignment == TextAlignment.Justify)
{
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
var whitespaceWidth = 0d;
if (lineWhitespaceWidth > whitespaceWidth)
for (var i = 0; i < textLines.Count; i++)
{
whitespaceWidth = lineWhitespaceWidth;
}
}
var line = textLines[i];
var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace;
var justificationWidth = width - whitespaceWidth;
if (lineWhitespaceWidth > whitespaceWidth)
{
whitespaceWidth = lineWhitespaceWidth;
}
}
if (justificationWidth > 0)
{
var justificationProperties = new InterWordJustification(justificationWidth);
var justificationWidth = width - whitespaceWidth;
for (var i = 0; i < textLines.Count - 1; i++)
if (justificationWidth > 0)
{
var line = textLines[i];
var justificationProperties = new InterWordJustification(justificationWidth);
line.Justify(justificationProperties);
for (var i = 0; i < textLines.Count - 1; i++)
{
var line = textLines[i];
line.Justify(justificationProperties);
}
}
}
}
return textLines;
return textLines.ToArray();
}
finally
{
objectPool.TextLines.Return(ref textLines);
objectPool.VerifyAllReturned();
}
}
/// <summary>
@ -569,7 +587,7 @@ namespace Avalonia.Media.TextFormatting
public void Dispose()
{
foreach (var line in TextLines)
foreach (var line in _textLines)
{
line.Dispose();
}

100
src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs

@ -1,5 +1,7 @@
using System;
// ReSharper disable ForCanBeConvertedToForeach
using System;
using System.Collections.Generic;
using static Avalonia.Media.TextFormatting.FormattingObjectPool;
namespace Avalonia.Media.TextFormatting
{
@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine)
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{
var textRuns = textLine.TextRuns;
if (textRuns == null || textRuns.Count == 0)
if (textRuns.Count == 0)
{
return null;
}
@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting
if (Width < shapedSymbol.GlyphRun.Size.Width)
{
return new List<TextRun>(0);
return Array.Empty<TextRun>();
}
// Overview of ellipsis structure
@ -75,55 +78,63 @@ namespace Avalonia.Media.TextFormatting
{
shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength);
var collapsedRuns = new List<TextRun>(textRuns.Count);
if (measuredLength > 0)
{
IReadOnlyList<TextRun>? preSplitRuns = null;
IReadOnlyList<TextRun>? postSplitRuns;
var objectPool = FormattingObjectPool.Instance;
if (_prefixLength > 0)
{
var splitResult = TextFormatterImpl.SplitTextRuns(textRuns,
Math.Min(_prefixLength, measuredLength));
var collapsedRuns = objectPool.TextRunLists.Rent();
collapsedRuns.AddRange(splitResult.First);
RentedList<TextRun>? rentedPreSplitRuns = null;
RentedList<TextRun>? rentedPostSplitRuns = null;
preSplitRuns = splitResult.First;
postSplitRuns = splitResult.Second;
}
else
try
{
postSplitRuns = textRuns;
}
IReadOnlyList<TextRun>? effectivePostSplitRuns;
collapsedRuns.Add(shapedSymbol);
if (_prefixLength > 0)
{
(rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns(
textRuns, Math.Min(_prefixLength, measuredLength), objectPool);
if (measuredLength <= _prefixLength || postSplitRuns is null)
{
return collapsedRuns;
}
effectivePostSplitRuns = rentedPostSplitRuns;
foreach (var preSplitRun in rentedPreSplitRuns)
{
collapsedRuns.Add(preSplitRun);
}
}
else
{
effectivePostSplitRuns = textRuns;
}
var availableSuffixWidth = availableWidth;
collapsedRuns.Add(shapedSymbol);
if (preSplitRuns is not null)
{
foreach (var run in preSplitRuns)
if (measuredLength <= _prefixLength || effectivePostSplitRuns is null)
{
if (run is DrawableTextRun drawableTextRun)
return collapsedRuns.ToArray();
}
var availableSuffixWidth = availableWidth;
if (rentedPreSplitRuns is not null)
{
foreach (var run in rentedPreSplitRuns)
{
availableSuffixWidth -= drawableTextRun.Size.Width;
if (run is DrawableTextRun drawableTextRun)
{
availableSuffixWidth -= drawableTextRun.Size.Width;
}
}
}
}
for (var i = postSplitRuns.Count - 1; i >= 0; i--)
{
var run = postSplitRuns[i];
switch (run)
for (var i = effectivePostSplitRuns.Count - 1; i >= 0; i--)
{
case ShapedTextRun endShapedRun:
var run = effectivePostSplitRuns[i];
switch (run)
{
case ShapedTextRun endShapedRun:
{
if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth,
out var suffixCount, out var suffixWidth))
@ -141,15 +152,20 @@ namespace Avalonia.Media.TextFormatting
break;
}
}
}
return collapsedRuns.ToArray();
}
finally
{
objectPool.TextRunLists.Return(ref rentedPreSplitRuns);
objectPool.TextRunLists.Return(ref rentedPostSplitRuns);
objectPool.TextRunLists.Return(ref collapsedRuns);
}
}
else
{
collapsedRuns.Add(shapedSymbol);
}
return collapsedRuns;
return new TextRun[] { shapedSymbol };
}
availableWidth -= shapedRun.Size.Width;

305
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -1,19 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
internal class TextLineImpl : TextLine
internal sealed class TextLineImpl : TextLine
{
private IReadOnlyList<TextRun> _textRuns;
private readonly TextRun[] _textRuns;
private readonly double _paragraphWidth;
private readonly TextParagraphProperties _paragraphProperties;
private TextLineMetrics _textLineMetrics;
private readonly FlowDirection _resolvedFlowDirection;
public TextLineImpl(IReadOnlyList<TextRun> textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth,
TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight,
TextLineBreak? lineBreak = null, bool hasCollapsed = false)
{
@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting
var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties,
_resolvedFlowDirection, TextLineBreak, true);
if (collapsedRuns.Count > 0)
if (collapsedRuns.Length > 0)
{
collapsedLine.FinalizeLine();
}
@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override CharacterHit GetCharacterHitFromDistance(double distance)
{
if (_textRuns.Count == 0)
if (_textRuns.Length == 0)
{
return new CharacterHit();
}
@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting
if (distance >= WidthIncludingTrailingWhitespace)
{
var lastRun = _textRuns[_textRuns.Count - 1];
var lastRun = _textRuns[_textRuns.Length - 1];
var size = 0.0;
@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting
var currentPosition = FirstTextSourceIndex;
var currentDistance = 0.0;
for (var i = 0; i < _textRuns.Count; i++)
for (var i = 0; i < _textRuns.Length; i++)
{
var currentRun = _textRuns[i];
@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = i;
currentPosition += currentRun.Length;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1)
while (rightToLeftIndex + 1 <= _textRuns.Length - 1)
{
var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun;
@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting
for (var j = i; i <= rightToLeftIndex; j++)
{
if (j > _textRuns.Count - 1)
if (j > _textRuns.Length - 1)
{
break;
}
@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting
if (currentRun is DrawableTextRun drawableTextRun)
{
if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance)
if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance)
{
currentDistance += drawableTextRun.Size.Width;
@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting
if (flowDirection == FlowDirection.LeftToRight)
{
for (var index = 0; index < _textRuns.Count; index++)
for (var index = 0; index < _textRuns.Length; index++)
{
var currentRun = _textRuns[index];
@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftWidth = shapedRun.Size.Width;
while (i + 1 <= _textRuns.Count - 1)
while (i + 1 <= _textRuns.Length - 1)
{
var nextRun = _textRuns[i + 1];
@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting
{
currentDistance += WidthIncludingTrailingWhitespace;
for (var index = _textRuns.Count - 1; index >= 0; index--)
for (var index = _textRuns.Length - 1; index >= 0; index--)
{
var currentRun = _textRuns[index];
@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit)
{
if (_textRuns.Count == 0)
if (_textRuns.Length == 0)
{
return new CharacterHit();
}
@ -568,7 +567,7 @@ namespace Avalonia.Media.TextFormatting
{
var characterIndex = firstTextSourceIndex + textLength;
var result = new List<TextBounds>(TextRuns.Count);
var result = new List<TextBounds>(_textRuns.Length);
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
@ -581,9 +580,9 @@ namespace Avalonia.Media.TextFormatting
TextRunBounds lastRunBounds = default;
for (var index = 0; index < TextRuns.Count; index++)
for (var index = 0; index < _textRuns.Length; index++)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
if (_textRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting
var rightToLeftIndex = index;
var rightToLeftWidth = currentShapedRun.Size.Width;
while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun)
{
if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight)
{
@ -669,12 +668,12 @@ namespace Avalonia.Media.TextFormatting
for (int i = rightToLeftIndex - 1; i >= index; i--)
{
if (TextRuns[i] is not ShapedTextRun)
if (_textRuns[i] is not ShapedTextRun shapedRun)
{
continue;
}
currentShapedRun = (ShapedTextRun)TextRuns[i];
currentShapedRun = shapedRun;
currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength);
@ -784,7 +783,7 @@ namespace Avalonia.Media.TextFormatting
{
var characterIndex = firstTextSourceIndex + textLength;
var result = new List<TextBounds>(TextRuns.Count);
var result = new List<TextBounds>(_textRuns.Length);
var lastDirection = FlowDirection.LeftToRight;
var currentDirection = lastDirection;
@ -795,9 +794,9 @@ namespace Avalonia.Media.TextFormatting
double currentWidth = 0;
var currentRect = default(Rect);
for (var index = TextRuns.Count - 1; index >= 0; index--)
for (var index = _textRuns.Length - 1; index >= 0; index--)
{
if (TextRuns[index] is not DrawableTextRun currentRun)
if (_textRuns[index] is not DrawableTextRun currentRun)
{
continue;
}
@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting
public override void Dispose()
{
for (int i = 0; i < _textRuns.Count; i++)
for (int i = 0; i < _textRuns.Length; i++)
{
if (_textRuns[i] is ShapedTextRun shapedTextRun)
{
@ -990,186 +989,11 @@ namespace Avalonia.Media.TextFormatting
}
}
public TextLineImpl FinalizeLine()
public void FinalizeLine()
{
_textLineMetrics = CreateLineMetrics();
BidiReorder();
return this;
}
private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection)
{
if (run is ShapedTextRun shapedTextCharacters)
{
return shapedTextCharacters.BidiLevel;
}
var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1;
return (sbyte)defaultLevel;
}
private void BidiReorder()
{
if (_textRuns.Count == 0)
{
return;
}
// Build up the collection of ordered runs.
var run = _textRuns[0];
OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection));
var current = orderedRun;
for (var i = 1; i < _textRuns.Count; i++)
{
run = _textRuns[i];
current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection));
current = current.Next;
}
// Reorder them into visual order.
orderedRun = LinearReOrder(orderedRun);
// Now perform a recursive reversal of each run.
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels
// not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
// https://unicode.org/reports/tr9/#L2
sbyte max = 0;
var min = sbyte.MaxValue;
for (var i = 0; i < _textRuns.Count; i++)
{
var currentRun = _textRuns[i];
var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection);
if (level > max)
{
max = level;
}
if ((level & 1) != 0 && level < min)
{
min = level;
}
}
if (min > max)
{
min = max;
}
if (max == 0 || (min == max && (max & 1) == 0))
{
// Nothing to reverse.
return;
}
// Now apply the reversal and replace the original contents.
var minLevelToReverse = max;
while (minLevelToReverse >= min)
{
current = orderedRun;
while (current != null)
{
if (current.Level >= minLevelToReverse && current.Level % 2 != 0)
{
if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters)
{
shapedTextCharacters.Reverse();
}
}
current = current.Next;
}
minLevelToReverse--;
}
var textRuns = new List<TextRun>(_textRuns.Count);
current = orderedRun;
while (current != null)
{
textRuns.Add(current.Run);
current = current.Next;
}
_textRuns = textRuns;
}
/// <summary>
/// Reorders a series of runs from logical to visual order, returning the left most run.
/// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
/// </summary>
/// <param name="run">The ordered bidi run.</param>
/// <returns>The <see cref="OrderedBidiRun"/>.</returns>
private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run)
{
BidiRange? range = null;
while (run != null)
{
var next = run.Next;
while (range != null && range.Level > run.Level
&& range.Previous != null && range.Previous.Level >= run.Level)
{
range = BidiRange.MergeWithPrevious(range);
}
if (range != null && range.Level >= run.Level)
{
// Attach run to the range.
if ((run.Level & 1) != 0)
{
// Odd, range goes to the right of run.
run.Next = range.Left;
range.Left = run;
}
else
{
// Even, range goes to the left of run.
range.Right!.Next = run;
range.Right = run;
}
range.Level = run.Level;
}
else
{
var r = new BidiRange();
r.Left = r.Right = run;
r.Level = run.Level;
r.Previous = range;
range = r;
}
run = next;
}
while (range?.Previous != null)
{
range = BidiRange.MergeWithPrevious(range);
}
// Terminate.
range!.Right!.Next = null;
return range.Left!;
BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection);
}
/// <summary>
@ -1197,7 +1021,7 @@ namespace Avalonia.Media.TextFormatting
var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition);
while (runIndex < _textRuns.Count)
while (runIndex < _textRuns.Length)
{
var currentRun = _textRuns[runIndex];
@ -1346,7 +1170,7 @@ namespace Avalonia.Media.TextFormatting
textPosition = FirstTextSourceIndex;
TextRun? previousRun = null;
while (runIndex < _textRuns.Count)
while (runIndex < _textRuns.Length)
{
var currentRun = _textRuns[runIndex];
@ -1395,7 +1219,7 @@ namespace Avalonia.Media.TextFormatting
}
}
if (runIndex + 1 >= _textRuns.Count)
if (runIndex + 1 >= _textRuns.Length)
{
return runIndex;
}
@ -1411,7 +1235,7 @@ namespace Avalonia.Media.TextFormatting
return runIndex;
}
if (runIndex + 1 >= _textRuns.Count)
if (runIndex + 1 >= _textRuns.Length)
{
return runIndex;
}
@ -1432,7 +1256,7 @@ namespace Avalonia.Media.TextFormatting
private TextLineMetrics CreateLineMetrics()
{
var fontMetrics = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface.Metrics;
var fontMetrics = _paragraphProperties.DefaultTextRunProperties.CachedGlyphTypeface.Metrics;
var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize;
var scale = fontRenderingEmSize / fontMetrics.DesignEmHeight;
@ -1448,25 +1272,26 @@ namespace Avalonia.Media.TextFormatting
var lineHeight = _paragraphProperties.LineHeight;
var lastRunIndex = _textRuns.Count - 1;
var lastRunIndex = _textRuns.Length - 1;
if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine)
{
lastRunIndex--;
}
for (var index = 0; index < _textRuns.Count; index++)
for (var index = 0; index < _textRuns.Length; index++)
{
switch (_textRuns[index])
{
case ShapedTextRun textRun:
{
var properties = textRun.Properties;
var textMetrics =
new TextMetrics(textRun.Properties.Typeface.GlyphTypeface, textRun.Properties.FontRenderingEmSize);
new TextMetrics(properties.CachedGlyphTypeface, properties.FontRenderingEmSize);
if (fontRenderingEmSize < textRun.Properties.FontRenderingEmSize)
if (fontRenderingEmSize < properties.FontRenderingEmSize)
{
fontRenderingEmSize = textRun.Properties.FontRenderingEmSize;
fontRenderingEmSize = properties.FontRenderingEmSize;
if (ascent > textMetrics.Ascent)
{
@ -1496,7 +1321,7 @@ namespace Avalonia.Media.TextFormatting
newLineLength = textRun.GlyphRun.Metrics.NewLineLength;
}
widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace;
widthIncludingWhitespace += textRun.Size.Width;
break;
}
@ -1620,59 +1445,5 @@ namespace Avalonia.Media.TextFormatting
return 0;
}
}
private sealed class OrderedBidiRun
{
public OrderedBidiRun(TextRun run, sbyte level)
{
Run = run;
Level = level;
}
public sbyte Level { get; }
public TextRun Run { get; }
public OrderedBidiRun? Next { get; set; }
}
private sealed class BidiRange
{
public int Level { get; set; }
public OrderedBidiRun? Left { get; set; }
public OrderedBidiRun? Right { get; set; }
public BidiRange? Previous { get; set; }
public static BidiRange MergeWithPrevious(BidiRange range)
{
var previous = range.Previous;
BidiRange left;
BidiRange right;
if ((previous!.Level & 1) != 0)
{
// Odd, previous goes to the right of range.
left = range;
right = previous;
}
else
{
// Even, previous goes to the left of range.
left = previous;
right = range;
}
// Stitch them
left.Right!.Next = right.Left;
previous.Left = left.Left;
previous.Right = right.Right;
return previous;
}
}
}
}

8
src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs

@ -12,6 +12,8 @@ namespace Avalonia.Media.TextFormatting
/// </remarks>
public abstract class TextRunProperties : IEquatable<TextRunProperties>
{
private IGlyphTypeface? _cachedGlyphTypeFace;
/// <summary>
/// Run typeface
/// </summary>
@ -47,6 +49,9 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public virtual BaselineAlignment BaselineAlignment => BaselineAlignment.Baseline;
internal IGlyphTypeface CachedGlyphTypeface
=> _cachedGlyphTypeFace ??= Typeface.GlyphTypeface;
public bool Equals(TextRunProperties? other)
{
if (ReferenceEquals(null, other))
@ -93,6 +98,9 @@ namespace Avalonia.Media.TextFormatting
internal TextRunProperties WithTypeface(Typeface typeface)
{
if (this is GenericTextRunProperties other && other.Typeface == typeface)
return this;
return new GenericTextRunProperties(typeface, FontRenderingEmSize,
TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment);
}

7
src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -1,6 +1,4 @@
using System.Collections.Generic;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// A collapsing properties to collapse whole line toward the end
@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine)
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, false);
}

8
src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -1,7 +1,4 @@
using System.Collections.Generic;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// a collapsing properties to collapse whole line toward the end
@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextRun Symbol { get; }
public override List<TextRun>? Collapse(TextLine textLine)
/// <inheritdoc />
public override TextRun[]? Collapse(TextLine textLine)
{
return TextEllipsisHelper.Collapse(textLine, this, true);
}

309
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -5,8 +5,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting.Unicode
@ -28,8 +26,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// as much as possible.
/// </para>
/// </remarks>
internal struct BidiAlgorithm : IDisposable
internal sealed class BidiAlgorithm
{
/// <summary>
/// Whether the state is clean and can be reused without a reset.
/// </summary>
private bool _hasCleanState = true;
/// <summary>
/// The original BiDiClass classes as provided by the caller
/// </summary>
@ -67,7 +70,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// The forward mapping maps the start index to the end index.
/// The reverse mapping maps the end index to the start index.
/// </remarks>
private BidiDictionary<int, int>? _isolatePairs;
private readonly BidiDictionary<int, int> _isolatePairs = new();
/// <summary>
/// The working BiDi classes
@ -98,7 +101,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// The status stack used during resolution of explicit
/// embedding and isolating runs
/// </summary>
private readonly Stack<Status> _statusStack = new Stack<Status>();
private readonly Stack<Status> _statusStack = new();
/// <summary>
/// Mapping used to virtually remove characters for rule X9
@ -108,7 +111,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// Re-usable list of level runs
/// </summary>
private readonly List<LevelRun> _levelRuns = new List<LevelRun>();
private readonly List<LevelRun> _levelRuns = new();
/// <summary>
/// Mapping for the current isolating sequence, built
@ -119,7 +122,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// A stack of pending isolate openings used by FindIsolatePairs()
/// </summary>
private Stack<int>? _pendingIsolateOpenings;
private readonly Stack<int> _pendingIsolateOpenings = new();
/// <summary>
/// The level of the isolating run currently being processed
@ -175,12 +178,12 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Reusable list of pending opening brackets used by the
/// LocatePairedBrackets method
/// </summary>
private readonly List<int> _pendingOpeningBrackets = new List<int>();
private readonly List<int> _pendingOpeningBrackets = new();
/// <summary>
/// Resolved list of paired brackets
/// </summary>
private readonly List<BracketPair> _pairedBrackets = new List<BracketPair>();
private readonly List<BracketPair> _pairedBrackets = new();
/// <summary>
/// Initializes a new instance of the <see cref="BidiAlgorithm"/> class.
@ -228,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
ArraySlice<sbyte>? outLevels)
{
// Reset state
_isolatePairs?.Clear();
_workingClassesBuffer.Clear();
_levelRuns.Clear();
_resolvedLevelsBuffer.Clear();
Reset();
if (types.IsEmpty)
{
return;
}
_hasCleanState = false;
// Setup original types and working types
_originalClasses = types;
_workingClasses = _workingClassesBuffer.Add(types);
@ -324,7 +326,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Skip isolate pairs
// (Because we're working with a slice, we need to adjust the indices
// we're using for the isolatePairs map)
if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true)
if (_isolatePairs.TryGetValue(data.Start + i, out i))
{
i -= data.Start;
}
@ -341,6 +343,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
return 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsIsolateStart(BidiClass type)
{
const uint mask =
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate);
return ((1U << (int)type) & mask) != 0U;
}
/// <summary>
/// Build a list of matching isolates for a directionality slice
/// Implements BD9
@ -359,7 +372,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
_hasIsolates = false;
// BD9...
_pendingIsolateOpenings?.Clear();
_pendingIsolateOpenings.Clear();
for (var i = 0; i < _originalClasses.Length; i++)
{
@ -371,16 +384,14 @@ namespace Avalonia.Media.TextFormatting.Unicode
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
{
_pendingIsolateOpenings ??= new Stack<int>();
_pendingIsolateOpenings.Push(i);
_hasIsolates = true;
break;
}
case BidiClass.PopDirectionalIsolate:
{
if (_pendingIsolateOpenings?.Count > 0)
if (_pendingIsolateOpenings.Count > 0)
{
_isolatePairs ??= new BidiDictionary<int, int>();
_isolatePairs.Add(_pendingIsolateOpenings.Pop(), i);
}
@ -501,7 +512,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (resolvedIsolate == BidiClass.FirstStrongIsolate)
{
if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate))
if (!_isolatePairs.TryGetValue(i, out var endOfIsolate))
{
endOfIsolate = _originalClasses.Length;
}
@ -701,28 +712,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
var lastType = _workingClasses[lastCharIndex];
int nextLevel;
switch (lastType)
if (IsIsolateStart(lastType))
{
nextLevel = _paragraphEmbeddingLevel;
}
else
{
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
i = lastCharIndex + 1;
while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
{
nextLevel = _paragraphEmbeddingLevel;
break;
i++;
}
default:
{
i = lastCharIndex + 1;
while (i < _originalClasses.Length && IsRemovedByX9(_originalClasses[i]))
{
i++;
}
nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
break;
}
nextLevel = i >= _originalClasses.Length ? _paragraphEmbeddingLevel : _resolvedLevels[i];
}
var eos = DirectionFromLevel(Math.Max(nextLevel, level));
@ -831,8 +833,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
// PDI and concatenate that run to this one
var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1];
var lastType = _originalClasses[lastCharacterIndex];
if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) &&
_isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true)
if (IsIsolateStart(lastType) && _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex))
{
// Find the continuing run index
runIndex = FindRunForIndex(nextRunIndex);
@ -855,87 +856,73 @@ namespace Avalonia.Media.TextFormatting.Unicode
private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel)
{
// Create mappings onto the underlying data
_runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, _isolatedRunMapping.AsSlice());
_runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, _isolatedRunMapping.AsSlice());
_runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, _isolatedRunMapping.AsSlice());
var isolatedRunMapping = _isolatedRunMapping.AsSlice();
_runResolvedClasses = new MappedArraySlice<BidiClass>(_workingClasses, isolatedRunMapping);
_runOriginalClasses = new MappedArraySlice<BidiClass>(_originalClasses, isolatedRunMapping);
_runLevels = new MappedArraySlice<sbyte>(_resolvedLevels, isolatedRunMapping);
if (_hasBrackets)
{
_runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, _isolatedRunMapping.AsSlice());
_runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, _isolatedRunMapping.AsSlice());
_runBiDiPairedBracketTypes = new MappedArraySlice<BidiPairedBracketType>(_pairedBracketTypes, isolatedRunMapping);
_runPairedBracketValues = new MappedArraySlice<int>(_pairedBracketValues, isolatedRunMapping);
}
_runLevel = runLevel;
_runDirection = DirectionFromLevel(runLevel);
_runLength = _runResolvedClasses.Length;
// By tracking the types of characters known to be in the current run, we can
// skip some of the rules that we know won't apply. The flags will be
// initialized while we're processing rule W1 below.
var hasEN = false;
var hasAL = false;
var hasES = false;
var hasCS = false;
var hasAN = false;
var hasET = false;
// Rule W1
// Also, set hasXX flags
int i;
var previousClass = sos;
const uint isolateMask =
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate);
const uint wRulesMask =
(1U << (int)BidiClass.EuropeanNumber) |
(1U << (int)BidiClass.ArabicLetter) |
(1U << (int)BidiClass.EuropeanSeparator) |
(1U << (int)BidiClass.CommonSeparator) |
(1U << (int)BidiClass.ArabicNumber) |
(1U << (int)BidiClass.EuropeanTerminator);
uint wRules = 0;
for (i = 0; i < _runLength; i++)
{
var resolvedClass = _runResolvedClasses[i];
switch (resolvedClass)
{
case BidiClass.NonspacingMark:
_runResolvedClasses[i] = previousClass;
break;
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
if (resolvedClass == BidiClass.NonspacingMark)
{
_runResolvedClasses[i] = previousClass;
}
else
{
var classBit = 1U << (int)resolvedClass;
if ((classBit & isolateMask) != 0U)
{
previousClass = BidiClass.OtherNeutral;
break;
case BidiClass.EuropeanNumber:
hasEN = true;
previousClass = resolvedClass;
break;
case BidiClass.ArabicLetter:
hasAL = true;
previousClass = resolvedClass;
break;
case BidiClass.EuropeanSeparator:
hasES = true;
previousClass = resolvedClass;
break;
case BidiClass.CommonSeparator:
hasCS = true;
previousClass = resolvedClass;
break;
case BidiClass.ArabicNumber:
hasAN = true;
previousClass = resolvedClass;
break;
case BidiClass.EuropeanTerminator:
hasET = true;
previousClass = resolvedClass;
break;
default:
}
else
{
wRules |= classBit & wRulesMask;
previousClass = resolvedClass;
break;
}
}
}
// By tracking the types of characters known to be in the current run, we can
// skip some of the rules that we know won't apply.
var hasEN = (wRules & (1U << (int)BidiClass.EuropeanNumber)) != 0U;
var hasAL = (wRules & (1U << (int)BidiClass.ArabicLetter)) != 0U;
var hasES = (wRules & (1U << (int)BidiClass.EuropeanSeparator)) != 0U;
var hasCS = (wRules & (1U << (int)BidiClass.CommonSeparator)) != 0U;
var hasAN = (wRules & (1U << (int)BidiClass.ArabicNumber)) != 0U;
var hasET = (wRules & (1U << (int)BidiClass.EuropeanTerminator)) != 0U;
// Rule W2
if (hasEN)
{
@ -1547,23 +1534,20 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsWhitespace(BidiClass biDiClass)
{
switch (biDiClass)
{
case BidiClass.LeftToRightEmbedding:
case BidiClass.RightToLeftEmbedding:
case BidiClass.LeftToRightOverride:
case BidiClass.RightToLeftOverride:
case BidiClass.PopDirectionalFormat:
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
case BidiClass.BoundaryNeutral:
case BidiClass.WhiteSpace:
return true;
default:
return false;
}
const uint mask =
(1U << (int)BidiClass.LeftToRightEmbedding) |
(1U << (int)BidiClass.RightToLeftEmbedding) |
(1U << (int)BidiClass.LeftToRightOverride) |
(1U << (int)BidiClass.RightToLeftOverride) |
(1U << (int)BidiClass.PopDirectionalFormat) |
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate) |
(1U << (int)BidiClass.BoundaryNeutral) |
(1U << (int)BidiClass.WhiteSpace);
return ((1U << (int)biDiClass) & mask) != 0U;
}
/// <summary>
@ -1584,18 +1568,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsRemovedByX9(BidiClass biDiClass)
{
switch (biDiClass)
{
case BidiClass.LeftToRightEmbedding:
case BidiClass.RightToLeftEmbedding:
case BidiClass.LeftToRightOverride:
case BidiClass.RightToLeftOverride:
case BidiClass.PopDirectionalFormat:
case BidiClass.BoundaryNeutral:
return true;
default:
return false;
}
const uint mask =
(1U << (int)BidiClass.LeftToRightEmbedding) |
(1U << (int)BidiClass.RightToLeftEmbedding) |
(1U << (int)BidiClass.LeftToRightOverride) |
(1U << (int)BidiClass.RightToLeftOverride) |
(1U << (int)BidiClass.PopDirectionalFormat) |
(1U << (int)BidiClass.BoundaryNeutral);
return ((1U << (int)biDiClass) & mask) != 0U;
}
/// <summary>
@ -1604,20 +1585,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsNeutralClass(BidiClass direction)
{
switch (direction)
{
case BidiClass.ParagraphSeparator:
case BidiClass.SegmentSeparator:
case BidiClass.WhiteSpace:
case BidiClass.OtherNeutral:
case BidiClass.RightToLeftIsolate:
case BidiClass.LeftToRightIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
return true;
default:
return false;
}
const uint mask =
(1U << (int)BidiClass.ParagraphSeparator) |
(1U << (int)BidiClass.SegmentSeparator) |
(1U << (int)BidiClass.WhiteSpace) |
(1U << (int)BidiClass.OtherNeutral) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate);
return ((1U << (int)direction) & mask) != 0U;
}
/// <summary>
@ -1642,6 +1620,47 @@ namespace Avalonia.Media.TextFormatting.Unicode
}
}
/// <summary>
/// Resets the bidi algorithm to a clean state.
/// </summary>
public void Reset()
{
if (_hasCleanState)
{
return;
}
_originalClasses = default;
_pairedBracketTypes = default;
_pairedBracketValues = default;
_hasBrackets = default;
_hasEmbeddings = default;
_hasIsolates = default;
_isolatePairs.ClearThenResetIfTooLarge();
_workingClasses = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer);
_resolvedLevels = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer);
_paragraphEmbeddingLevel = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map);
FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping);
FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings);
_runLevel = default;
_runDirection = default;
_runLength = default;
_runResolvedClasses = default;
_runOriginalClasses = default;
_runLevels = default;
_runBiDiPairedBracketTypes = default;
_runPairedBracketValues = default;
FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets);
FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets);
_hasCleanState = true;
}
/// <summary>
/// Hold the start and end index of a pair of brackets
/// </summary>
@ -1717,13 +1736,5 @@ namespace Avalonia.Media.TextFormatting.Unicode
public BidiClass Eos { get; }
}
public void Dispose()
{
_workingClassesBuffer.Dispose();
_resolvedLevelsBuffer.Dispose();
_x9Map.Dispose();
_isolatedRunMapping.Dispose();
}
}
}

94
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -11,8 +11,10 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// Represents a unicode string and all associated attributes
/// for each character required for the bidirectional Unicode algorithm
/// </summary>
internal struct BidiData : IDisposable
/// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
internal sealed class BidiData
{
private bool _hasCleanState = true;
private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
private ArrayBuilder<int> _pairedBracketValues;
@ -20,12 +22,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
private ArrayBuilder<BidiPairedBracketType> _savedPairedBracketTypes;
private ArrayBuilder<sbyte> _tempLevelBuffer;
public BidiData(sbyte paragraphEmbeddingLevel)
{
ParagraphEmbeddingLevel = paragraphEmbeddingLevel;
}
public sbyte ParagraphEmbeddingLevel { get; private set; }
public sbyte ParagraphEmbeddingLevel { get; set; }
public bool HasBrackets { get; private set; }
@ -36,7 +33,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// Gets the length of the data held by the BidiData
/// </summary>
public int Length{get; private set; }
public int Length { get; private set; }
/// <summary>
/// Gets the bidi character type of each code point
@ -66,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="text">The text to process.</param>
public void Append(ReadOnlySpan<char> text)
{
_hasCleanState = false;
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);
_pairedBracketValues.Add(text.Length);
@ -74,39 +73,32 @@ namespace Avalonia.Media.TextFormatting.Unicode
// bracket values for all code points
int i = Length;
const uint embeddingMask =
(1U << (int)BidiClass.LeftToRightEmbedding) |
(1U << (int)BidiClass.LeftToRightOverride) |
(1U << (int)BidiClass.RightToLeftEmbedding) |
(1U << (int)BidiClass.RightToLeftOverride) |
(1U << (int)BidiClass.PopDirectionalFormat);
const uint isolateMask =
(1U << (int)BidiClass.LeftToRightIsolate) |
(1U << (int)BidiClass.RightToLeftIsolate) |
(1U << (int)BidiClass.FirstStrongIsolate) |
(1U << (int)BidiClass.PopDirectionalIsolate);
var codePointEnumerator = new CodepointEnumerator(text);
while (codePointEnumerator.MoveNext())
while (codePointEnumerator.MoveNext(out var codepoint))
{
var codepoint = codePointEnumerator.Current;
// Look up BiDiClass
var dir = codepoint.BiDiClass;
_classes[i] = dir;
switch (dir)
{
case BidiClass.LeftToRightEmbedding:
case BidiClass.LeftToRightOverride:
case BidiClass.RightToLeftEmbedding:
case BidiClass.RightToLeftOverride:
case BidiClass.PopDirectionalFormat:
{
HasEmbeddings = true;
break;
}
case BidiClass.LeftToRightIsolate:
case BidiClass.RightToLeftIsolate:
case BidiClass.FirstStrongIsolate:
case BidiClass.PopDirectionalIsolate:
{
HasIsolates = true;
break;
}
}
var dirBit = 1U << (int)dir;
HasEmbeddings = (dirBit & embeddingMask) != 0U;
HasIsolates = (dirBit & isolateMask) != 0U;
// Lookup paired bracket types
var pbt = codepoint.PairedBracketType;
@ -151,6 +143,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </remarks>
public void SaveTypes()
{
_hasCleanState = false;
// Capture the types data
_savedClasses.Clear();
_savedClasses.Add(_classes.AsSlice());
@ -163,6 +157,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
public void RestoreTypes()
{
_hasCleanState = false;
_classes.Clear();
_classes.Add(_savedClasses.AsSlice());
_pairedBracketTypes.Clear();
@ -182,14 +178,34 @@ namespace Avalonia.Media.TextFormatting.Unicode
return _tempLevelBuffer.Add(length, false);
}
public void Dispose()
/// <summary>
/// Resets the bidi data to a clean state.
/// </summary>
public void Reset()
{
_classes.Dispose();
_pairedBracketTypes.Dispose();
_pairedBracketValues.Dispose();
_savedClasses.Dispose();
_savedPairedBracketTypes.Dispose();
_tempLevelBuffer.Dispose();
if (_hasCleanState)
{
return;
}
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes);
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer);
ParagraphEmbeddingLevel = 0;
HasBrackets = false;
HasEmbeddings = false;
HasIsolates = false;
Length = 0;
Classes = default;
PairedBracketTypes = default;
PairedBracketValues = default;
_hasCleanState = true;
}
}
}

117
src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode
@ -11,13 +10,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <summary>
/// The replacement codepoint that is used for non supported values.
/// </summary>
public static readonly Codepoint ReplacementCodepoint = new Codepoint('\uFFFD');
public Codepoint(uint value)
public static Codepoint ReplacementCodepoint
{
_value = value;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new('\uFFFD');
}
/// <summary>
/// Creates a new instance of <see cref="Codepoint"/> with the specified value.
/// </summary>
/// <param name="value">The codepoint value.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Codepoint(uint value) => _value = value;
/// <summary>
/// Get the codepoint's value.
/// </summary>
@ -87,19 +92,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </returns>
public bool IsWhiteSpace
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
switch (GeneralCategory)
{
case GeneralCategory.Control:
case GeneralCategory.NonspacingMark:
case GeneralCategory.Format:
case GeneralCategory.SpaceSeparator:
case GeneralCategory.SpacingMark:
return true;
}
return false;
const ulong whiteSpaceMask =
(1UL << (int)GeneralCategory.Control) |
(1UL << (int)GeneralCategory.NonspacingMark) |
(1UL << (int)GeneralCategory.Format) |
(1UL << (int)GeneralCategory.SpaceSeparator) |
(1UL << (int)GeneralCategory.SpacingMark);
return ((1UL << (int)GeneralCategory) & whiteSpaceMask) != 0UL;
}
}
@ -166,56 +169,62 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="index">The index to read at.</param>
/// <param name="count">The count of character that were read.</param>
/// <returns></returns>
#if NET6_0_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
#else
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
public static Codepoint ReadAt(ReadOnlySpan<char> text, int index, out int count)
{
// Perf note: this method is performance critical for text layout, modify with care!
count = 1;
if (index >= text.Length)
// Perf note: uint check allows the JIT to ellide the next bound check
if ((uint)index >= (uint)text.Length)
{
return ReplacementCodepoint;
}
var code = text[index];
ushort hi, low;
uint code = text[index];
//# High surrogate
if (0xD800 <= code && code <= 0xDBFF)
//# Surrogate
if (IsInRangeInclusive(code, 0xD800U, 0xDFFFU))
{
hi = code;
if (index + 1 == text.Length)
{
return ReplacementCodepoint;
}
low = text[index + 1];
if (0xDC00 <= low && low <= 0xDFFF)
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
}
return ReplacementCodepoint;
}
uint hi, low;
//# Low surrogate
if (0xDC00 <= code && code <= 0xDFFF)
{
if (index == 0)
//# High surrogate
if (code <= 0xDBFF)
{
return ReplacementCodepoint;
if ((uint)(index + 1) < (uint)text.Length)
{
hi = code;
low = text[index + 1];
if (IsInRangeInclusive(low, 0xDC00U, 0xDFFFU))
{
count = 2;
// Perf note: the code is written as below to become just two instructions: shl, lea.
// See https://github.com/dotnet/runtime/blob/7ec3634ee579d89b6024f72b595bfd7118093fc5/src/libraries/System.Private.CoreLib/src/System/Text/UnicodeUtility.cs#L38
return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16)));
}
}
}
hi = text[index - 1];
low = code;
if (0xD800 <= hi && hi <= 0xDBFF)
//# Low surrogate
else
{
count = 2;
return new Codepoint((uint)((hi - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000));
if (index > 0)
{
low = code;
hi = text[index - 1];
if (IsInRangeInclusive(hi, 0xD800U, 0xDBFFU))
{
count = 2;
return new Codepoint((hi << 10) + low - ((0xD800U << 10) + 0xDC00U - (1 << 16)));
}
}
}
return ReplacementCodepoint;
@ -224,12 +233,16 @@ namespace Avalonia.Media.TextFormatting.Unicode
return new Codepoint(code);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsInRangeInclusive(uint value, uint lowerBound, uint upperBound)
=> value - lowerBound <= upperBound - lowerBound;
/// <summary>
/// Returns <see langword="true"/> if <paramref name="cp"/> is between
/// <paramref name="lowerBound"/> and <paramref name="upperBound"/>, inclusive.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsInRangeInclusive(Codepoint cp, uint lowerBound, uint upperBound)
=> (cp._value - lowerBound) <= (upperBound - lowerBound);
=> IsInRangeInclusive(cp._value, lowerBound, upperBound);
}
}

24
src/Avalonia.Base/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -4,35 +4,27 @@ namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct CodepointEnumerator
{
private ReadOnlySpan<char> _text;
private readonly ReadOnlySpan<char> _text;
private int _offset;
public CodepointEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = Codepoint.ReplacementCodepoint;
}
/// <summary>
/// Gets the current <see cref="Codepoint"/>.
/// </summary>
public Codepoint Current { get; private set; }
=> _text = text;
/// <summary>
/// Moves to the next <see cref="Codepoint"/>.
/// </summary>
/// <returns></returns>
public bool MoveNext()
public bool MoveNext(out Codepoint codepoint)
{
if (_text.IsEmpty)
if ((uint)_offset >= (uint)_text.Length)
{
Current = Codepoint.ReplacementCodepoint;
codepoint = Codepoint.ReplacementCodepoint;
return false;
}
Current = Codepoint.ReadAt(_text, 0, out var count);
codepoint = Codepoint.ReadAt(_text, _offset, out var count);
_text = _text.Slice(count);
_offset += count;
return true;
}

18
src/Avalonia.Base/Media/TextFormatting/Unicode/Grapheme.cs

@ -1,16 +1,15 @@
using System;
namespace Avalonia.Media.TextFormatting.Unicode
namespace Avalonia.Media.TextFormatting.Unicode
{
/// <summary>
/// Represents the smallest unit of a writing system of any given language.
/// </summary>
public readonly ref struct Grapheme
{
public Grapheme(Codepoint firstCodepoint, ReadOnlySpan<char> text)
public Grapheme(Codepoint firstCodepoint, int offset, int length)
{
FirstCodepoint = firstCodepoint;
Text = text;
Offset = offset;
Length = length;
}
/// <summary>
@ -19,8 +18,13 @@ namespace Avalonia.Media.TextFormatting.Unicode
public Codepoint FirstCodepoint { get; }
/// <summary>
/// The text of the grapheme cluster
/// Gets the starting code unit offset of this grapheme inside its containing text.
/// </summary>
public int Offset { get; }
/// <summary>
/// Gets the length of this grapheme, in code units.
/// </summary>
public ReadOnlySpan<char> Text { get; }
public int Length { get; }
}
}

220
src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeEnumerator.cs

@ -4,57 +4,79 @@
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
public ref struct GraphemeEnumerator
{
private ReadOnlySpan<char> _text;
private readonly ReadOnlySpan<char> _text;
private int _currentCodeUnitOffset;
private int _codeUnitLengthOfCurrentCodepoint;
private Codepoint _currentCodepoint;
/// <summary>
/// Will be <see cref="GraphemeBreakClass.Other"/> if invalid data or EOF reached.
/// Caller shouldn't need to special-case this since the normal rules will halt on this condition.
/// </summary>
private GraphemeBreakClass _currentType;
public GraphemeEnumerator(ReadOnlySpan<char> text)
{
_text = text;
Current = default;
_currentCodeUnitOffset = 0;
_codeUnitLengthOfCurrentCodepoint = 0;
_currentCodepoint = Codepoint.ReplacementCodepoint;
_currentType = GraphemeBreakClass.Other;
}
/// <summary>
/// Gets the current <see cref="Grapheme"/>.
/// </summary>
public Grapheme Current { get; private set; }
/// <summary>
/// Moves to the next <see cref="Grapheme"/>.
/// </summary>
/// <returns></returns>
public bool MoveNext()
public bool MoveNext(out Grapheme grapheme)
{
if (_text.IsEmpty)
var startOffset = _currentCodeUnitOffset;
if ((uint)startOffset >= (uint)_text.Length)
{
grapheme = default;
return false;
}
// Algorithm given at https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules.
var processor = new Processor(_text);
processor.MoveNext();
if (startOffset == 0)
{
ReadNextCodepoint();
}
var firstCodepoint = processor.CurrentCodepoint;
var firstCodepoint = _currentCodepoint;
// First, consume as many Prepend scalars as we can (rule GB9b).
while (processor.CurrentType == GraphemeBreakClass.Prepend)
if (_currentType == GraphemeBreakClass.Prepend)
{
processor.MoveNext();
do
{
ReadNextCodepoint();
} while (_currentType == GraphemeBreakClass.Prepend);
// There were only Prepend scalars in the text
if ((uint)_currentCodeUnitOffset >= (uint)_text.Length)
{
goto Return;
}
}
// Next, make sure we're not about to violate control character restrictions.
// Essentially, if we saw Prepend data, we can't have Control | CR | LF data afterward (rule GB5).
if (processor.CurrentCodeUnitOffset > 0)
if (_currentCodeUnitOffset > startOffset)
{
if (processor.CurrentType == GraphemeBreakClass.Control
|| processor.CurrentType == GraphemeBreakClass.CR
|| processor.CurrentType == GraphemeBreakClass.LF)
const uint controlCrLfMask =
(1U << (int)GraphemeBreakClass.Control) |
(1U << (int)GraphemeBreakClass.CR) |
(1U << (int)GraphemeBreakClass.LF);
if (((1U << (int)_currentType) & controlCrLfMask) != 0U)
{
goto Return;
}
@ -62,19 +84,19 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Now begin the main state machine.
var previousClusterBreakType = processor.CurrentType;
var previousClusterBreakType = _currentType;
processor.MoveNext();
ReadNextCodepoint();
switch (previousClusterBreakType)
{
case GraphemeBreakClass.CR:
if (processor.CurrentType != GraphemeBreakClass.LF)
if (_currentType != GraphemeBreakClass.LF)
{
goto Return; // rules GB3 & GB4 (only <LF> can follow <CR>)
}
processor.MoveNext();
ReadNextCodepoint();
goto case GraphemeBreakClass.LF;
case GraphemeBreakClass.Control:
@ -82,53 +104,57 @@ namespace Avalonia.Media.TextFormatting.Unicode
goto Return; // rule GB4 (no data after Control | LF)
case GraphemeBreakClass.L:
if (processor.CurrentType == GraphemeBreakClass.L)
{
if (_currentType == GraphemeBreakClass.L)
{
processor.MoveNext(); // rule GB6 (L x L)
ReadNextCodepoint(); // rule GB6 (L x L)
goto case GraphemeBreakClass.L;
}
else if (processor.CurrentType == GraphemeBreakClass.V)
else if (_currentType == GraphemeBreakClass.V)
{
processor.MoveNext(); // rule GB6 (L x V)
ReadNextCodepoint(); // rule GB6 (L x V)
goto case GraphemeBreakClass.V;
}
else if (processor.CurrentType == GraphemeBreakClass.LV)
else if (_currentType == GraphemeBreakClass.LV)
{
processor.MoveNext(); // rule GB6 (L x LV)
ReadNextCodepoint(); // rule GB6 (L x LV)
goto case GraphemeBreakClass.LV;
}
else if (processor.CurrentType == GraphemeBreakClass.LVT)
else if (_currentType == GraphemeBreakClass.LVT)
{
processor.MoveNext(); // rule GB6 (L x LVT)
ReadNextCodepoint(); // rule GB6 (L x LVT)
goto case GraphemeBreakClass.LVT;
}
else
{
break;
}
}
case GraphemeBreakClass.LV:
case GraphemeBreakClass.V:
if (processor.CurrentType == GraphemeBreakClass.V)
{
if (_currentType == GraphemeBreakClass.V)
{
processor.MoveNext(); // rule GB7 (LV | V x V)
ReadNextCodepoint(); // rule GB7 (LV | V x V)
goto case GraphemeBreakClass.V;
}
else if (processor.CurrentType == GraphemeBreakClass.T)
else if (_currentType == GraphemeBreakClass.T)
{
processor.MoveNext(); // rule GB7 (LV | V x T)
ReadNextCodepoint(); // rule GB7 (LV | V x T)
goto case GraphemeBreakClass.T;
}
else
{
break;
}
}
case GraphemeBreakClass.LVT:
case GraphemeBreakClass.T:
if (processor.CurrentType == GraphemeBreakClass.T)
if (_currentType == GraphemeBreakClass.T)
{
processor.MoveNext(); // rule GB8 (LVT | T x T)
ReadNextCodepoint(); // rule GB8 (LVT | T x T)
goto case GraphemeBreakClass.T;
}
else
@ -139,123 +165,79 @@ namespace Avalonia.Media.TextFormatting.Unicode
case GraphemeBreakClass.ExtendedPictographic:
// Attempt processing extended pictographic (rules GB11, GB9).
// First, drain any Extend scalars that might exist
while (processor.CurrentType == GraphemeBreakClass.Extend)
while (_currentType == GraphemeBreakClass.Extend)
{
processor.MoveNext();
ReadNextCodepoint();
}
// Now see if there's a ZWJ + extended pictograph again.
if (processor.CurrentType != GraphemeBreakClass.ZWJ)
if (_currentType != GraphemeBreakClass.ZWJ)
{
break;
}
processor.MoveNext();
if (processor.CurrentType != GraphemeBreakClass.ExtendedPictographic)
ReadNextCodepoint();
if (_currentType != GraphemeBreakClass.ExtendedPictographic)
{
break;
}
processor.MoveNext();
ReadNextCodepoint();
goto case GraphemeBreakClass.ExtendedPictographic;
case GraphemeBreakClass.RegionalIndicator:
// We've consumed a single RI scalar. Try to consume another (to make it a pair).
if (processor.CurrentType == GraphemeBreakClass.RegionalIndicator)
if (_currentType == GraphemeBreakClass.RegionalIndicator)
{
processor.MoveNext();
ReadNextCodepoint();
}
// Standlone RI scalars (or a single pair of RI scalars) can only be followed by trailers.
break; // nothing but trailers after the final RI
default:
break;
}
const uint gb9Mask =
(1U << (int)GraphemeBreakClass.Extend) |
(1U << (int)GraphemeBreakClass.ZWJ) |
(1U << (int)GraphemeBreakClass.SpacingMark);
// rules GB9, GB9a
while (processor.CurrentType == GraphemeBreakClass.Extend
|| processor.CurrentType == GraphemeBreakClass.ZWJ
|| processor.CurrentType == GraphemeBreakClass.SpacingMark)
while (((1U << (int)_currentType) & gb9Mask) != 0U)
{
processor.MoveNext();
ReadNextCodepoint();
}
Return:
Current = new Grapheme(firstCodepoint, _text.Slice(0, processor.CurrentCodeUnitOffset));
_text = _text.Slice(processor.CurrentCodeUnitOffset);
var graphemeLength = _currentCodeUnitOffset - startOffset;
grapheme = new Grapheme(firstCodepoint, startOffset, graphemeLength);
return true; // rules GB2, GB999
}
[StructLayout(LayoutKind.Auto)]
private ref struct Processor
private void ReadNextCodepoint()
{
private readonly ReadOnlySpan<char> _buffer;
private int _codeUnitLengthOfCurrentScalar;
internal Processor(ReadOnlySpan<char> buffer)
{
_buffer = buffer;
_codeUnitLengthOfCurrentScalar = 0;
CurrentCodepoint = Codepoint.ReplacementCodepoint;
CurrentType = GraphemeBreakClass.Other;
CurrentCodeUnitOffset = 0;
}
public int CurrentCodeUnitOffset { get; private set; }
/// <summary>
/// Will be <see cref="GraphemeBreakClass.Other"/> if invalid data or EOF reached.
/// Caller shouldn't need to special-case this since the normal rules will halt on this condition.
/// </summary>
public GraphemeBreakClass CurrentType { get; private set; }
/// <summary>
/// Get the currently processed <see cref="Codepoint"/>.
/// </summary>
public Codepoint CurrentCodepoint { get; private set; }
public void MoveNext()
{
// For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on
// the decoder's default behavior of interpreting these ill-formed subsequences as
// equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property
// of Other (XX), which matches the modifications made to UAX#29, Rev. 35.
// See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications
// This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file
// https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
// has the line "D800..DFFF ; Control # Cs [2048] <surrogate-D800>..<surrogate-DFFF>",
// but starting with Unicode 12.0 that line has been removed.
//
// If a later version of the Unicode Standard further modifies this guidance we should reflect
// that here.
if (CurrentCodeUnitOffset == _buffer.Length)
{
CurrentCodepoint = Codepoint.ReplacementCodepoint;
}
else
{
CurrentCodeUnitOffset += _codeUnitLengthOfCurrentScalar;
if (CurrentCodeUnitOffset < _buffer.Length)
{
CurrentCodepoint = Codepoint.ReadAt(_buffer, CurrentCodeUnitOffset,
out _codeUnitLengthOfCurrentScalar);
}
else
{
CurrentCodepoint = Codepoint.ReplacementCodepoint;
}
}
CurrentType = CurrentCodepoint.GraphemeBreakClass;
}
// For ill-formed subsequences (like unpaired UTF-16 surrogate code points), we rely on
// the decoder's default behavior of interpreting these ill-formed subsequences as
// equivalent to U+FFFD REPLACEMENT CHARACTER. This code point has a boundary property
// of Other (XX), which matches the modifications made to UAX#29, Rev. 35.
// See: https://www.unicode.org/reports/tr29/tr29-35.html#Modifications
// This change is also reflected in the UCD files. For example, Unicode 11.0's UCD file
// https://www.unicode.org/Public/11.0.0/ucd/auxiliary/GraphemeBreakProperty.txt
// has the line "D800..DFFF ; Control # Cs [2048] <surrogate-D800>..<surrogate-DFFF>",
// but starting with Unicode 12.0 that line has been removed.
//
// If a later version of the Unicode Standard further modifies this guidance we should reflect
// that here.
_currentCodeUnitOffset += _codeUnitLengthOfCurrentCodepoint;
_currentCodepoint = Codepoint.ReadAt(_text, _currentCodeUnitOffset,
out _codeUnitLengthOfCurrentCodepoint);
_currentType = _currentCodepoint.GraphemeBreakClass;
}
}
}

244
src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs

@ -3,6 +3,7 @@
// Ported from: https://github.com/SixLabors/Fonts/
using System;
using System.Runtime.CompilerServices;
namespace Avalonia.Media.TextFormatting.Unicode
{
@ -46,10 +47,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
_lb30 = false;
_lb30a = 0;
}
public LineBreak Current { get; private set; }
public bool MoveNext()
public bool MoveNext(out LineBreak lineBreak)
{
// Get the first char if we're at the beginning of the string.
if (_first)
@ -75,7 +74,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
case LineBreakClass.CarriageReturn when _nextClass != LineBreakClass.LineFeed:
{
_currentClass = MapFirst(_nextClass);
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true);
lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, true);
return true;
}
}
@ -87,7 +86,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
if (shouldBreak)
{
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition);
return true;
}
}
@ -108,23 +107,23 @@ namespace Avalonia.Media.TextFormatting.Unicode
break;
}
Current = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required);
lineBreak = new LineBreak(FindPriorNonWhitespace(_lastPosition), _lastPosition, required);
return true;
}
}
Current = default;
lineBreak = default;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LineBreakClass MapClass(Codepoint cp)
{
if (cp.Value == 327685)
{
return LineBreakClass.Alphabetic;
}
// LB 1
// ==========================================
// Resolved Original General_Category
@ -133,26 +132,38 @@ namespace Avalonia.Media.TextFormatting.Unicode
// CM SA Only Mn or Mc
// AL SA Any except Mn and Mc
// NS CJ Any
switch (cp.LineBreakClass)
{
case LineBreakClass.Ambiguous:
case LineBreakClass.Surrogate:
case LineBreakClass.Unknown:
return LineBreakClass.Alphabetic;
case LineBreakClass.ComplexContext:
return cp.GeneralCategory == GeneralCategory.NonspacingMark || cp.GeneralCategory == GeneralCategory.SpacingMark
? LineBreakClass.CombiningMark
: LineBreakClass.Alphabetic;
var cls = cp.LineBreakClass;
case LineBreakClass.ConditionalJapaneseStarter:
return LineBreakClass.Nonstarter;
const ulong specialMask =
(1UL << (int)LineBreakClass.Ambiguous) |
(1UL << (int)LineBreakClass.Surrogate) |
(1UL << (int)LineBreakClass.Unknown) |
(1UL << (int)LineBreakClass.ComplexContext) |
(1UL << (int)LineBreakClass.ConditionalJapaneseStarter);
default:
return cp.LineBreakClass;
if (((1UL << (int)cls) & specialMask) != 0UL)
{
switch (cls)
{
case LineBreakClass.Ambiguous:
case LineBreakClass.Surrogate:
case LineBreakClass.Unknown:
return LineBreakClass.Alphabetic;
case LineBreakClass.ComplexContext:
return cp.GeneralCategory is GeneralCategory.NonspacingMark or GeneralCategory.SpacingMark
? LineBreakClass.CombiningMark
: LineBreakClass.Alphabetic;
case LineBreakClass.ConditionalJapaneseStarter:
return LineBreakClass.Nonstarter;
}
}
return cls;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static LineBreakClass MapFirst(LineBreakClass c)
{
switch (c)
@ -169,10 +180,80 @@ namespace Avalonia.Media.TextFormatting.Unicode
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsAlphaNumeric(LineBreakClass cls)
=> cls == LineBreakClass.Alphabetic
|| cls == LineBreakClass.HebrewLetter
|| cls == LineBreakClass.Numeric;
{
const ulong mask =
(1UL << (int)LineBreakClass.Alphabetic) |
(1UL << (int)LineBreakClass.HebrewLetter) |
(1UL << (int)LineBreakClass.Numeric);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPrefixPostfixNumericOrSpace(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.PostfixNumeric) |
(1UL << (int)LineBreakClass.PrefixNumeric) |
(1UL << (int)LineBreakClass.Space);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsPrefixPostfixNumeric(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.PostfixNumeric) |
(1UL << (int)LineBreakClass.PrefixNumeric);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsClosePunctuationOrParenthesis(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.ClosePunctuation) |
(1UL << (int)LineBreakClass.CloseParenthesis);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsClosePunctuationOrInfixNumericOrBreakSymbols(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.ClosePunctuation) |
(1UL << (int)LineBreakClass.InfixNumeric) |
(1UL << (int)LineBreakClass.BreakSymbols);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsSpaceOrWordJoinerOrAlphabetic(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.Space) |
(1UL << (int)LineBreakClass.WordJoiner) |
(1UL << (int)LineBreakClass.Alphabetic);
return ((1UL << (int)cls) & mask) != 0UL;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsMandatoryBreakOrLineFeedOrCarriageReturn(LineBreakClass cls)
{
const ulong mask =
(1UL << (int)LineBreakClass.MandatoryBreak) |
(1UL << (int)LineBreakClass.LineFeed) |
(1UL << (int)LineBreakClass.CarriageReturn);
return ((1UL << (int)cls) & mask) != 0UL;
}
private LineBreakClass PeekNextCharClass()
{
@ -198,83 +279,77 @@ namespace Avalonia.Media.TextFormatting.Unicode
// Track combining mark exceptions. LB22
if (cls == LineBreakClass.CombiningMark)
{
switch (_currentClass)
const ulong lb22ExMask =
(1UL << (int)LineBreakClass.MandatoryBreak) |
(1UL << (int)LineBreakClass.ContingentBreak) |
(1UL << (int)LineBreakClass.Exclamation) |
(1UL << (int)LineBreakClass.LineFeed) |
(1UL << (int)LineBreakClass.NextLine) |
(1UL << (int)LineBreakClass.Space) |
(1UL << (int)LineBreakClass.ZWSpace) |
(1UL << (int)LineBreakClass.CarriageReturn);
if (((1UL << (int)_currentClass) & lb22ExMask) != 0UL)
{
case LineBreakClass.MandatoryBreak:
case LineBreakClass.ContingentBreak:
case LineBreakClass.Exclamation:
case LineBreakClass.LineFeed:
case LineBreakClass.NextLine:
case LineBreakClass.Space:
case LineBreakClass.ZWSpace:
case LineBreakClass.CarriageReturn:
_lb22ex = true;
break;
_lb22ex = true;
}
}
// Track combining mark exceptions. LB31
if (_first && cls == LineBreakClass.CombiningMark)
{
_lb31 = true;
const ulong lb31Mask =
(1UL << (int)LineBreakClass.MandatoryBreak) |
(1UL << (int)LineBreakClass.ContingentBreak) |
(1UL << (int)LineBreakClass.Exclamation) |
(1UL << (int)LineBreakClass.LineFeed) |
(1UL << (int)LineBreakClass.NextLine) |
(1UL << (int)LineBreakClass.Space) |
(1UL << (int)LineBreakClass.ZWSpace) |
(1UL << (int)LineBreakClass.CarriageReturn) |
(1UL << (int)LineBreakClass.ZWJ);
// Track combining mark exceptions. LB31
if (_first || ((1UL << (int)_currentClass) & lb31Mask) != 0UL)
{
_lb31 = true;
}
}
if (cls == LineBreakClass.CombiningMark)
if (_first)
{
switch (_currentClass)
// Rule LB24
if (IsClosePunctuationOrParenthesis(cls))
{
case LineBreakClass.MandatoryBreak:
case LineBreakClass.ContingentBreak:
case LineBreakClass.Exclamation:
case LineBreakClass.LineFeed:
case LineBreakClass.NextLine:
case LineBreakClass.Space:
case LineBreakClass.ZWSpace:
case LineBreakClass.CarriageReturn:
case LineBreakClass.ZWJ:
_lb31 = true;
break;
_lb24ex = true;
}
}
if (_first
&& (cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space))
{
_lb31 = true;
// Rule LB25
if (IsClosePunctuationOrInfixNumericOrBreakSymbols(cls))
{
_lb25ex = true;
}
if (IsPrefixPostfixNumericOrSpace(cls))
{
_lb31 = true;
}
}
if (_currentClass == LineBreakClass.Alphabetic &&
(cls == LineBreakClass.PostfixNumeric || cls == LineBreakClass.PrefixNumeric || cls == LineBreakClass.Space))
if (_currentClass == LineBreakClass.Alphabetic && IsPrefixPostfixNumericOrSpace(cls))
{
_lb31 = true;
}
// Reset LB31 if next is U+0028 (Left Opening Parenthesis)
if (_lb31
&& _currentClass != LineBreakClass.PostfixNumeric
&& _currentClass != LineBreakClass.PrefixNumeric
&& cls == LineBreakClass.OpenPunctuation && cp.Value == 0x0028)
&& !IsPrefixPostfixNumeric(_currentClass)
&& cls == LineBreakClass.OpenPunctuation
&& cp.Value == 0x0028)
{
_lb31 = false;
}
// Rule LB24
if (_first && (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.CloseParenthesis))
{
_lb24ex = true;
}
// Rule LB25
if (_first
&& (cls == LineBreakClass.ClosePunctuation || cls == LineBreakClass.InfixNumeric || cls == LineBreakClass.BreakSymbols))
{
_lb25ex = true;
}
if (cls == LineBreakClass.Space || cls == LineBreakClass.WordJoiner || cls == LineBreakClass.Alphabetic)
if (IsSpaceOrWordJoinerOrAlphabetic(cls))
{
var next = PeekNextCharClass();
if (next == LineBreakClass.ClosePunctuation || next == LineBreakClass.InfixNumeric || next == LineBreakClass.BreakSymbols)
if (IsClosePunctuationOrInfixNumericOrBreakSymbols(next))
{
_lb25ex = true;
}
@ -295,6 +370,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
return cls;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool? GetSimpleBreak()
{
// handle classes not handled by the pair table
@ -317,6 +393,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] // quite long but only one usage
private bool GetPairTableBreak(LineBreakClass lastClass)
{
// If not handled already, use the pair table
@ -477,8 +554,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
var cls = cp.LineBreakClass;
if (cls == LineBreakClass.MandatoryBreak || cls == LineBreakClass.LineFeed ||
cls == LineBreakClass.CarriageReturn)
if (IsMandatoryBreakOrLineFeedOrCarriageReturn(cls))
{
from -= count;
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save