Browse Source

Merge branch 'master' into feature/3744-onapplytemplate

pull/3858/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
8bbe9a40dc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      build/SkiaSharp.props
  2. 14
      nukebuild/Build.cs
  3. 2
      samples/ControlCatalog.NetCore/Program.cs
  4. 4
      samples/ControlCatalog/ControlCatalog.csproj
  5. 1
      samples/ControlCatalog/MainView.xaml
  6. 29
      samples/ControlCatalog/Pages/OpenGlPage.xaml
  7. 401
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  8. BIN
      samples/ControlCatalog/Pages/teapot.bin
  9. 28
      samples/RenderDemo/Pages/AnimationsPage.xaml
  10. 10
      scripts/ReplaceNugetCache.ps1
  11. 10
      scripts/ReplaceNugetCacheRelease.ps1
  12. 50
      src/Avalonia.Base/Utilities/DisposableLock.cs
  13. 25
      src/Avalonia.Controls/Border.cs
  14. 3
      src/Avalonia.Controls/Image.cs
  15. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  16. 4
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  17. 2
      src/Avalonia.Controls/TreeView.cs
  18. 46
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  19. 3
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  20. 4
      src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs
  21. 90
      src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs
  22. 266
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  23. 472
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs
  24. 2
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/.gitignore
  25. 8878
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  26. 41
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json
  27. 57
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/FramePresenter.tsx
  28. 78
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/PreviewerServerConnection.ts
  29. 14
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html
  30. 15
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.tsx
  31. 35
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json
  32. 117
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js
  33. 53
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  34. 86
      src/Avalonia.Native/GlPlatformFeature.cs
  35. 3
      src/Avalonia.Native/PopupImpl.cs
  36. 5
      src/Avalonia.Native/WindowImpl.cs
  37. 6
      src/Avalonia.Native/WindowImplBase.cs
  38. 2
      src/Avalonia.OpenGL/Avalonia.OpenGL.csproj
  39. 55
      src/Avalonia.OpenGL/EglContext.cs
  40. 56
      src/Avalonia.OpenGL/EglDisplay.cs
  41. 17
      src/Avalonia.OpenGL/EglGlPlatformFeature.cs
  42. 24
      src/Avalonia.OpenGL/EglGlPlatformSurface.cs
  43. 37
      src/Avalonia.OpenGL/EglInterface.cs
  44. 4
      src/Avalonia.OpenGL/EglSurface.cs
  45. 72
      src/Avalonia.OpenGL/GlBasicInfoInterface.cs
  46. 4674
      src/Avalonia.OpenGL/GlConsts.cs
  47. 8
      src/Avalonia.OpenGL/GlDisplayType.cs
  48. 113
      src/Avalonia.OpenGL/GlEntryPointAttribute.cs
  49. 284
      src/Avalonia.OpenGL/GlInterface.cs
  50. 68
      src/Avalonia.OpenGL/GlInterfaceBase.cs
  51. 22
      src/Avalonia.OpenGL/GlVersion.cs
  52. 11
      src/Avalonia.OpenGL/IGlContext.cs
  53. 11
      src/Avalonia.OpenGL/IGlDisplay.cs
  54. 2
      src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs
  55. 9
      src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs
  56. 3
      src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs
  57. 13
      src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs
  58. 46
      src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs
  59. 215
      src/Avalonia.OpenGL/OpenGlControlBase.cs
  60. 4
      src/Avalonia.Remote.Protocol/BsonStreamTransport.cs
  61. 1
      src/Avalonia.Remote.Protocol/ITransport.cs
  62. 2
      src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs
  63. 9
      src/Avalonia.Remote.Protocol/TransportMessages.cs
  64. 2
      src/Avalonia.Remote.Protocol/ViewportMessages.cs
  65. 23
      src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs
  66. 40
      src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs
  67. 133
      src/Avalonia.Visuals/Media/BoxShadow.cs
  68. 137
      src/Avalonia.Visuals/Media/BoxShadows.cs
  69. 44
      src/Avalonia.Visuals/Media/Color.cs
  70. 6
      src/Avalonia.Visuals/Media/DrawingContext.cs
  71. 10
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  72. 2
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  73. 10
      src/Avalonia.Visuals/Rect.cs
  74. 45
      src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs
  75. 7
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  76. 10
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  77. 44
      src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs
  78. 136
      src/Avalonia.Visuals/RoundedRect.cs
  79. 41
      src/Avalonia.X11/Glx/Glx.cs
  80. 2
      src/Avalonia.X11/Glx/GlxConsts.cs
  81. 56
      src/Avalonia.X11/Glx/GlxContext.cs
  82. 134
      src/Avalonia.X11/Glx/GlxDisplay.cs
  83. 14
      src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs
  84. 13
      src/Avalonia.X11/Glx/GlxPlatformFeature.cs
  85. 12
      src/Avalonia.X11/X11Platform.cs
  86. 2
      src/Avalonia.X11/X11Window.cs
  87. 44
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
  88. 164
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  89. 15
      src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs
  90. 2
      src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs
  91. 7
      src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs
  92. 73
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs
  93. 46
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs
  94. 81
      src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs
  95. 12
      src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs
  96. 70
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  97. 8
      src/Skia/Avalonia.Skia/SkiaOptions.cs
  98. 5
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  99. 2
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  100. 9
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.2" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" />
<PackageReference Include="SkiaSharp" Version="1.68.2.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2.1" />
</ItemGroup>
</Project>

14
nukebuild/Build.cs

@ -12,6 +12,7 @@ using Nuke.Common.ProjectModel;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.EnvironmentInfo;
@ -121,8 +122,21 @@ partial class Build : NukeBuild
EnsureCleanDirectory(Parameters.TestResultsRoot);
});
Target CompileHtmlPreviewer => _ => _
.DependsOn(Clean)
.Executes(() =>
{
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir));
NpmTasks.NpmRun(c => c
.SetWorkingDirectory(webappDir)
.SetCommand("dist"));
});
Target Compile => _ => _
.DependsOn(Clean)
.DependsOn(CompileHtmlPreviewer)
.Executes(() =>
{
if (Parameters.IsRunningOnWindows)

2
samples/ControlCatalog.NetCore/Program.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
@ -6,6 +7,7 @@ using System.Threading;
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Dialogs;
using Avalonia.OpenGL;
namespace ControlCatalog.NetCore
{

4
samples/ControlCatalog/ControlCatalog.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
@ -17,6 +18,7 @@
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-BoldItalic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Italic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Regular.ttf" />
<EmbeddedResource Include="Pages\teapot.bin" />
</ItemGroup>
<ItemGroup>

1
samples/ControlCatalog/MainView.xaml

@ -47,6 +47,7 @@
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="OpenGL"><pages:OpenGlPage/></TabItem>
<TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>

29
samples/ControlCatalog/Pages/OpenGlPage.xaml

@ -0,0 +1,29 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.OpenGlPage"
xmlns:pages="clr-namespace:ControlCatalog.Pages">
<Grid>
<pages:OpenGlPageControl x:Name="GL"/>
<StackPanel>
<TextBlock Text="{Binding #GL.Info}"/>
</StackPanel>
<Grid ColumnDefinitions="*,Auto" Margin="20">
<StackPanel Grid.Column="1" MinWidth="300">
<TextBlock>Yaw</TextBlock>
<Slider Value="{Binding Yaw, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<TextBlock>Pitch</TextBlock>
<Slider Value="{Binding Pitch, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<TextBlock>Roll</TextBlock>
<Slider Value="{Binding Roll, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<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 Disco, Mode=TwoWay, ElementName=GL}" Maximum="1"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

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

@ -0,0 +1,401 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.OpenGL;
using Avalonia.Platform.Interop;
using Avalonia.Threading;
using static Avalonia.OpenGL.GlConsts;
// ReSharper disable StringLiteralTypo
namespace ControlCatalog.Pages
{
public class OpenGlPage : UserControl
{
}
public class OpenGlPageControl : OpenGlControlBase
{
private float _yaw;
public static readonly DirectProperty<OpenGlPageControl, float> YawProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Yaw", o => o.Yaw, (o, v) => o.Yaw = v);
public float Yaw
{
get => _yaw;
set => SetAndRaise(YawProperty, ref _yaw, value);
}
private float _pitch;
public static readonly DirectProperty<OpenGlPageControl, float> PitchProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Pitch", o => o.Pitch, (o, v) => o.Pitch = v);
public float Pitch
{
get => _pitch;
set => SetAndRaise(PitchProperty, ref _pitch, value);
}
private float _roll;
public static readonly DirectProperty<OpenGlPageControl, float> RollProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, 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<OpenGlPageControl, float> DiscoProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Disco", o => o.Disco, (o, v) => o.Disco = v);
public float Disco
{
get => _disco;
set => SetAndRaise(DiscoProperty, ref _disco, value);
}
private string _info;
public static readonly DirectProperty<OpenGlPageControl, string> InfoProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, string>("Info", o => o.Info, (o, v) => o.Info = v);
public string Info
{
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;
private int _vertexBufferObject;
private int _indexBufferObject;
private int _vertexArrayObject;
private GlExtrasInterface _glExt;
private string GetShader(bool fragment, string shader)
{
var version = (GlVersion.Type == GlProfileType.OpenGL ?
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 120 :
100);
var data = "#version " + version + "\n";
if (GlVersion.Type == GlProfileType.OpenGLES)
data += "precision mediump float;\n";
if (version >= 150)
{
shader = shader.Replace("attribute", "in");
if (fragment)
shader = shader
.Replace("varying", "in")
.Replace("//DECLAREGLFRAG", "out vec4 outFragColor;")
.Replace("gl_FragColor", "outFragColor");
else
shader = shader.Replace("varying", "out");
}
data += shader;
return data;
}
private string VertexShaderSource => GetShader(false, @"
attribute vec3 aPos;
attribute vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uProjection;
uniform mat4 uView;
varying vec3 FragPos;
varying vec3 VecPos;
varying vec3 Normal;
uniform float uTime;
uniform float uDisco;
void main()
{
float discoScale = sin(uTime * 10.0) / 10.0;
float distortionX = 1.0 + uDisco * cos(uTime * 20.0) / 10.0;
float scale = 1.0 + uDisco * discoScale;
vec3 scaledPos = aPos;
scaledPos.x = scaledPos.x * distortionX;
scaledPos *= scale;
gl_Position = uProjection * uView * uModel * vec4(scaledPos, 1.0);
FragPos = vec3(uModel * vec4(aPos, 1.0));
VecPos = aPos;
Normal = normalize(vec3(uModel * vec4(aNormal, 1.0)));
}
");
private string FragmentShaderSource => GetShader(true, @"
varying vec3 FragPos;
varying vec3 VecPos;
varying vec3 Normal;
uniform float uMaxY;
uniform float uMinY;
uniform float uTime;
uniform float uDisco;
//DECLAREGLFRAG
void main()
{
float y = (VecPos.y - uMinY) / (uMaxY - uMinY);
float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + uTime * 40.0 + y * 50.0);
float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - uTime * 20.0 - y * 30.0);
vec3 discoColor = vec3(
0.5 + abs(0.5 - y) * cos(uTime * 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 - uDisco) + discoColor * uDisco;
float ambientStrength = 0.3;
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 lightPos = vec3(uMaxY * 2.0, uMaxY * 2.0, uMaxY * 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;
gl_FragColor = vec4(result, 1.0);
}
");
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
}
private readonly Vertex[] _points;
private readonly ushort[] _indices;
private readonly float _minY;
private readonly float _maxY;
public OpenGlPageControl()
{
var name = typeof(OpenGlPage).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin"));
using (var sr = new BinaryReader(typeof(OpenGlPage).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);
}
}
}
private void CheckError(GlInterface gl)
{
int err;
while ((err = gl.GetError()) != GL_NO_ERROR)
Console.WriteLine(err);
}
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb)
{
CheckError(GL);
_glExt = new GlExtrasInterface(GL);
Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}";
// Load the source of the vertex shader and compile it.
_vertexShader = GL.CreateShader(GL_VERTEX_SHADER);
Console.WriteLine(GL.CompileShaderAndGetError(_vertexShader, VertexShaderSource));
// Load the source of the fragment shader and compile it.
_fragmentShader = GL.CreateShader(GL_FRAGMENT_SHADER);
Console.WriteLine(GL.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource));
// Create the shader program, attach the vertex and fragment shaders and link the program.
_shaderProgram = GL.CreateProgram();
GL.AttachShader(_shaderProgram, _vertexShader);
GL.AttachShader(_shaderProgram, _fragmentShader);
const int positionLocation = 0;
const int normalLocation = 1;
GL.BindAttribLocationString(_shaderProgram, positionLocation, "aPos");
GL.BindAttribLocationString(_shaderProgram, normalLocation, "aNormal");
Console.WriteLine(GL.LinkProgramAndGetError(_shaderProgram));
CheckError(GL);
// Create the vertex buffer object (VBO) for the vertex data.
_vertexBufferObject = GL.GenBuffer();
// Bind the VBO and copy the vertex data into it.
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
CheckError(GL);
var vertexSize = Marshal.SizeOf<Vertex>();
fixed (void* pdata = _points)
GL.BufferData(GL_ARRAY_BUFFER, new IntPtr(_points.Length * vertexSize),
new IntPtr(pdata), GL_STATIC_DRAW);
_indexBufferObject = GL.GenBuffer();
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
CheckError(GL);
fixed (void* pdata = _indices)
GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata),
GL_STATIC_DRAW);
CheckError(GL);
_vertexArrayObject = _glExt.GenVertexArray();
_glExt.BindVertexArray(_vertexArrayObject);
CheckError(GL);
GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT,
0, vertexSize, IntPtr.Zero);
GL.VertexAttribPointer(normalLocation, 3, GL_FLOAT,
0, vertexSize, new IntPtr(12));
GL.EnableVertexAttribArray(positionLocation);
GL.EnableVertexAttribArray(normalLocation);
CheckError(GL);
}
protected override void OnOpenGlDeinit(GlInterface GL, int fb)
{
// Unbind everything
GL.BindBuffer(GL_ARRAY_BUFFER, 0);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
_glExt.BindVertexArray(0);
GL.UseProgram(0);
// Delete all resources.
GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject });
_glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject });
GL.DeleteProgram(_shaderProgram);
GL.DeleteShader(_fragmentShader);
GL.DeleteShader(_vertexShader);
}
static Stopwatch St = Stopwatch.StartNew();
protected override unsafe void OnOpenGlRender(GlInterface gl, int fb)
{
gl.ClearColor(0, 0, 0, 0);
gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
gl.Enable(GL_DEPTH_TEST);
gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height);
var GL = gl;
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
_glExt.BindVertexArray(_vertexArrayObject);
GL.UseProgram(_shaderProgram);
CheckError(GL);
var projection =
Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)(Bounds.Width / Bounds.Height),
0.01f, 1000);
var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0));
var model = Matrix4x4.CreateFromYawPitchRoll(_yaw, _pitch, _roll);
var modelLoc = GL.GetUniformLocationString(_shaderProgram, "uModel");
var viewLoc = GL.GetUniformLocationString(_shaderProgram, "uView");
var projectionLoc = GL.GetUniformLocationString(_shaderProgram, "uProjection");
var maxYLoc = GL.GetUniformLocationString(_shaderProgram, "uMaxY");
var minYLoc = GL.GetUniformLocationString(_shaderProgram, "uMinY");
var timeLoc = GL.GetUniformLocationString(_shaderProgram, "uTime");
var discoLoc = GL.GetUniformLocationString(_shaderProgram, "uDisco");
GL.UniformMatrix4fv(modelLoc, 1, false, &model);
GL.UniformMatrix4fv(viewLoc, 1, false, &view);
GL.UniformMatrix4fv(projectionLoc, 1, false, &projection);
GL.Uniform1f(maxYLoc, _maxY);
GL.Uniform1f(minYLoc, _minY);
GL.Uniform1f(timeLoc, (float)St.Elapsed.TotalSeconds);
GL.Uniform1f(discoLoc, _disco);
CheckError(GL);
GL.DrawElements(GL_TRIANGLES, _indices.Length, GL_UNSIGNED_SHORT, IntPtr.Zero);
CheckError(GL);
if (_disco > 0.01)
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
class GlExtrasInterface : GlInterfaceBase<GlInterface.GlContextInfo>
{
public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo)
{
}
public delegate void GlDeleteVertexArrays(int count, int[] buffers);
[GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)]
[GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")]
public GlDeleteVertexArrays DeleteVertexArrays { get; }
public delegate void GlBindVertexArray(int array);
[GlMinVersionEntryPoint("glBindVertexArray", 3,0)]
[GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")]
public GlBindVertexArray BindVertexArray { get; }
public delegate void GlGenVertexArrays(int n, int[] rv);
[GlMinVersionEntryPoint("glGenVertexArrays",3,0)]
[GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")]
public GlGenVertexArrays GenVertexArrays { get; }
public int GenVertexArray()
{
var rv = new int[1];
GenVertexArrays(1, rv);
return rv[0];
}
}
}
}

BIN
samples/ControlCatalog/Pages/teapot.bin

Binary file not shown.

28
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -134,6 +134,32 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Shadow">
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red, -15 -15 Green"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue, -15 20 0 0 Blue"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="BoxShadow" Value="inset 0 0 20 30 Green, 20 20 20 0 Red"/>
</KeyFrame>
<KeyFrame Cue="85%">
<Setter Property="BoxShadow" Value="inset 30 0 20 30 Green, 20 20 20 10 Red"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -152,6 +178,8 @@
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Red"/>
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
</WrapPanel>
</StackPanel>
</Grid>

10
scripts/ReplaceNugetCache.ps1

@ -1,5 +1,5 @@
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\

10
scripts/ReplaceNugetCacheRelease.ps1

@ -1,5 +1,5 @@
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\

50
src/Avalonia.Base/Utilities/DisposableLock.cs

@ -0,0 +1,50 @@
using System;
using System.Threading;
namespace Avalonia.Utilities
{
public class DisposableLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to take a lock
/// </summary>
/// <returns>IDisposable if succeeded to obtain the lock</returns>
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return new UnlockDisposable(_lock);
return null;
}
/// <summary>
/// Enters a waiting lock
/// </summary>
public IDisposable Lock()
{
Monitor.Enter(_lock);
return new UnlockDisposable(_lock);
}
private sealed class UnlockDisposable : IDisposable
{
private object _lock;
public UnlockDisposable(object @lock)
{
_lock = @lock;
}
public void Dispose()
{
object @lock = Interlocked.Exchange(ref _lock, null);
if (@lock != null)
{
Monitor.Exit(@lock);
}
}
}
}
}

25
src/Avalonia.Controls/Border.cs

@ -33,6 +33,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary>
@ -44,7 +50,8 @@ namespace Avalonia.Controls
BackgroundProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty);
CornerRadiusProperty,
BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
@ -83,14 +90,24 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Renders the control.
/// </summary>
/// <param name="context">The drawing context.</param>
public override void Render(DrawingContext context)
{
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
}
/// <summary>
@ -110,8 +127,6 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
}
}

3
src/Avalonia.Controls/Image.cs

@ -69,10 +69,11 @@ namespace Avalonia.Controls
{
var source = Source;
if (source != null)
if (source != null && Bounds.Width > 0 && Bounds.Height > 0)
{
Rect viewPort = new Rect(Bounds.Size);
Size sourceSize = source.Size;
Vector scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection);
Size scaledSize = sourceSize * scale;
Rect destRect = viewPort

21
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -40,7 +40,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
Border.BoxShadowProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
@ -132,6 +137,15 @@ namespace Avalonia.Controls.Presenters
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Gets the control displayed by the presenter.
/// </summary>
@ -274,7 +288,8 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
public override void Render(DrawingContext context)
{
_borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
_borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
}
/// <summary>
@ -321,8 +336,6 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
return ArrangeOverrideImpl(finalSize, new Vector());
}

4
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -115,7 +115,7 @@ namespace Avalonia.Controls.Remote.Server
{
lock (_lock)
{
_lastReceivedFrame = lastFrame.SequenceId;
_lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame);
}
Dispatcher.UIThread.Post(RenderIfNeeded);
}
@ -298,6 +298,8 @@ namespace Avalonia.Controls.Remote.Server
Width = width,
Height = height,
Stride = width * bpp,
DpiX = _dpi.X,
DpiY = _dpi.Y
};
}

2
src/Avalonia.Controls/TreeView.cs

@ -395,7 +395,7 @@ namespace Avalonia.Controls
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl;
e.Children = container.GetObservable(ItemsProperty);
e.Children = container?.GetObservable(ItemsProperty);
}
private TreeViewItem GetContainerInDirection(

46
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -1,17 +1,30 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Controls.Utils
{
internal class BorderRenderHelper
{
private bool _useComplexRendering;
private bool? _backendSupportsIndividualCorners;
private StreamGeometry _backgroundGeometryCache;
private StreamGeometry _borderGeometryCache;
private Size _size;
private Thickness _borderThickness;
private CornerRadius _cornerRadius;
private bool _initialized;
public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
{
if (borderThickness.IsUniform && cornerRadius.IsUniform)
_backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
.SupportsIndividualRoundRects;
_size = finalSize;
_borderThickness = borderThickness;
_cornerRadius = cornerRadius;
_initialized = true;
if (borderThickness.IsUniform && (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true))
{
_backgroundGeometryCache = null;
_borderGeometryCache = null;
@ -67,7 +80,19 @@ namespace Avalonia.Controls.Utils
}
}
public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
public void Render(DrawingContext context,
Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
IBrush background, IBrush borderBrush, BoxShadows boxShadows)
{
if (_size != finalSize
|| _borderThickness != borderThickness
|| _cornerRadius != cornerRadius
|| !_initialized)
Update(finalSize, borderThickness, cornerRadius);
RenderCore(context, background, borderBrush, boxShadows);
}
void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows)
{
if (_useComplexRendering)
{
@ -85,9 +110,7 @@ namespace Avalonia.Controls.Utils
}
else
{
var borderThickness = borders.Top;
var top = borderThickness * 0.5;
var borderThickness = _borderThickness.Top;
IPen pen = null;
if (borderThickness > 0)
@ -95,11 +118,16 @@ namespace Avalonia.Controls.Utils
pen = new Pen(borderBrush, borderThickness);
}
var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness);
var rrect = new RoundedRect(new Rect(_size), _cornerRadius.TopLeft, _cornerRadius.TopRight,
_cornerRadius.BottomRight, _cornerRadius.BottomLeft);
if (Math.Abs(borderThickness) > double.Epsilon)
{
rrect = rrect.Deflate(borderThickness * 0.5, borderThickness * 0.5);
}
context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft);
context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows);
}
}
}
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderGeometryKeypoints keypoints)
{

3
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -8,8 +8,11 @@
-->
<Version>0.7.0</Version>
<NoWarn>CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Remote\HtmlTransport\webapp\build\**\*.gz" />
<EmbeddedResource Condition="'$(Configuration)' == 'Debug'" Remove="Remote\HtmlTransport\webapp\build\**\*.map.gz"/>
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

4
src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs

@ -35,5 +35,7 @@ namespace Avalonia.DesignerSupport.Remote
add {}
remove {}
}
public void Start() => _inner?.Start();
}
}
}

90
src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs

@ -0,0 +1,90 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
class FileWatcherTransport : IAvaloniaRemoteTransportConnection, ITransportWithEnforcedMethod
{
private string _path;
private string _lastContents;
private bool _disposed;
public FileWatcherTransport(Uri file)
{
_path = file.LocalPath;
}
public void Dispose()
{
_disposed = true;
}
void Dump(object o, string pad)
{
foreach (var p in o.GetType().GetProperties())
{
Console.Write($"{pad}{p.Name}: ");
var v = p.GetValue(o);
if (v == null || v.GetType().IsPrimitive || v is string || v is Guid)
Console.WriteLine(v);
else
{
Console.WriteLine();
Dump(v, pad + " ");
}
}
}
public Task Send(object data)
{
Console.WriteLine(data.GetType().Name);
Dump(data, " ");
return Task.CompletedTask;
}
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
{
add
{
_onMessage+=value;
}
remove { _onMessage -= value; }
}
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
public void Start()
{
UpdaterThread();
}
// I couldn't get FileSystemWatcher working on Linux, so I came up with this abomination
async void UpdaterThread()
{
while (!_disposed)
{
var data = File.ReadAllText(_path);
if (data != _lastContents)
{
Console.WriteLine("Triggering XAML update");
_lastContents = data;
_onMessage?.Invoke(this, new UpdateXamlMessage { Xaml = data });
}
await Task.Delay(100);
}
}
public string PreviewerMethod => RemoteDesignerEntryPoint.Methods.Html;
}
interface ITransportWithEnforcedMethod
{
string PreviewerMethod { get; }
}
}

266
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Viewport;
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
{
public class HtmlWebSocketTransport : IAvaloniaRemoteTransportConnection
{
private readonly IAvaloniaRemoteTransportConnection _signalTransport;
private readonly SimpleWebSocketHttpServer _simpleServer;
private readonly Dictionary<string, byte[]> _resources;
private SimpleWebSocket _pendingSocket;
private bool _disposed;
private object _lock = new object();
private AutoResetEvent _wakeup = new AutoResetEvent(false);
private FrameMessage _lastFrameMessage = null;
private FrameMessage _lastSentFrameMessage = null;
private RequestViewportResizeMessage _lastViewportRequest;
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
private Action<IAvaloniaRemoteTransportConnection, Exception> _onException;
private static readonly Dictionary<string, string> Mime = new Dictionary<string, string>
{
["html"] = "text/html", ["htm"] = "text/html", ["js"] = "text/javascript", ["css"] = "text/css"
};
private static readonly byte[] NotFound = Encoding.UTF8.GetBytes("404 - Not Found");
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri)
{
if (listenUri.Scheme != "http")
throw new ArgumentException("listenUri");
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build.";
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary(
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3),
r =>
{
using (var s =
new GZipStream(typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceStream(r),
CompressionMode.Decompress))
{
var ms = new MemoryStream();
s.CopyTo(ms);
return ms.ToArray();
}
});
_signalTransport = signalTransport;
var address = IPAddress.Parse(listenUri.Host);
_simpleServer = new SimpleWebSocketHttpServer(address, listenUri.Port);
_simpleServer.Listen();
Task.Run(AcceptWorker);
Task.Run(SocketWorker);
_signalTransport.Send(new HtmlTransportStartedMessage { Uri = "http://" + address + ":" + listenUri.Port + "/" });
}
async void AcceptWorker()
{
while (true)
{
using (var req = await _simpleServer.AcceptAsync())
{
if (!req.IsWebsocketRequest)
{
var key = req.Path == "/" ? "index.html" : req.Path.TrimStart('/').Replace('/', '.');
if (_resources.TryGetValue(key, out var data))
{
var ext = Path.GetExtension(key).Substring(1);
string mime = null;
if (ext == null || !Mime.TryGetValue(ext, out mime))
mime = "application/octet-stream";
await req.RespondAsync(200, data, mime);
}
else
{
await req.RespondAsync(404, NotFound, "text/plain");
}
}
else
{
var socket = await req.AcceptWebSocket();
SocketReceiveWorker(socket);
lock (_lock)
{
_pendingSocket?.Dispose();
_pendingSocket = socket;
}
}
}
}
}
async void SocketReceiveWorker(SimpleWebSocket socket)
{
try
{
while (true)
{
var msg = await socket.ReceiveMessage().ConfigureAwait(false);
if(msg == null)
return;
if (msg.IsText)
{
var s = Encoding.UTF8.GetString(msg.Data);
var parts = s.Split(':');
if (parts[0] == "frame-received")
_onMessage?.Invoke(this, new FrameReceivedMessage { SequenceId = long.Parse(parts[1]) });
}
}
}
catch(Exception e)
{
Console.Error.WriteLine(e.ToString());
}
}
async void SocketWorker()
{
try
{
SimpleWebSocket socket = null;
while (true)
{
if (_disposed)
{
socket?.Dispose();
return;
}
FrameMessage sendNow = null;
lock (_lock)
{
if (_pendingSocket != null)
{
socket?.Dispose();
socket = _pendingSocket;
_pendingSocket = null;
_lastSentFrameMessage = null;
}
if (_lastFrameMessage != _lastSentFrameMessage)
_lastSentFrameMessage = sendNow = _lastFrameMessage;
}
if (sendNow != null && socket != null)
{
await socket.SendMessage(
$"frame:{sendNow.SequenceId}:{sendNow.Width}:{sendNow.Height}:{sendNow.Stride}:{sendNow.DpiX}:{sendNow.DpiY}");
await socket.SendMessage(false, sendNow.Data);
}
_wakeup.WaitOne(TimeSpan.FromSeconds(1));
}
}
catch(Exception e)
{
Console.Error.WriteLine(e.ToString());
}
}
public void Dispose()
{
_pendingSocket?.Dispose();
_simpleServer.Dispose();
}
public Task Send(object data)
{
if (data is FrameMessage frame)
{
_lastFrameMessage = frame;
_wakeup.Set();
return Task.CompletedTask;
}
if (data is RequestViewportResizeMessage req)
{
return Task.CompletedTask;
}
return _signalTransport.Send(data);
}
public void Start()
{
_onMessage?.Invoke(this, new Avalonia.Remote.Protocol.Viewport.ClientSupportedPixelFormatsMessage
{
Formats = new []{PixelFormat.Rgba8888}
});
_signalTransport.Start();
}
#region Forward
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
{
add
{
bool subscribeToInner;
lock (_lock)
{
subscribeToInner = _onMessage == null;
_onMessage += value;
}
if (subscribeToInner)
_signalTransport.OnMessage += OnSignalTransportMessage;
}
remove
{
lock (_lock)
{
_onMessage -= value;
if (_onMessage == null)
_signalTransport.OnMessage -= OnSignalTransportMessage;
}
}
}
private void OnSignalTransportMessage(IAvaloniaRemoteTransportConnection signal, object message) => _onMessage?.Invoke(this, message);
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException
{
add
{
lock (_lock)
{
var subscribeToInner = _onException == null;
_onException += value;
if (subscribeToInner)
_signalTransport.OnException += OnSignalTransportException;
}
}
remove
{
lock (_lock)
{
_onException -= value;
if(_onException==null)
_signalTransport.OnException -= OnSignalTransportException;
}
}
}
private void OnSignalTransportException(IAvaloniaRemoteTransportConnection arg1, Exception ex)
{
_onException?.Invoke(this, ex);
}
#endregion
}
}

472
src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs

@ -0,0 +1,472 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
{
public class SimpleWebSocketHttpServer : IDisposable
{
private readonly IPAddress _address;
private readonly int _port;
private TcpListener _listener;
public async Task<SimpleWebSocketHttpRequest> AcceptAsync()
{
while (true)
{
var res = await AcceptAsyncImpl();
if (res != null)
return res;
}
}
async Task<SimpleWebSocketHttpRequest> AcceptAsyncImpl()
{
if (_listener == null)
throw new InvalidOperationException("Currently not listening");
var socket = await _listener.AcceptSocketAsync();
var stream = new NetworkStream(socket);
bool error = true;
async Task<string> ReadLineAsync()
{
var readBuffer = new byte[1];
var lineBuffer = new byte[1024];
for (var c = 0; c < 1024; c++)
{
if (await stream.ReadAsync(readBuffer, 0, 1) == 0)
throw new EndOfStreamException();
if (readBuffer[0] == 10)
{
if (c == 0)
return "";
if (lineBuffer[c - 1] == 13)
c--;
if (c == 0)
return "";
return Encoding.UTF8.GetString(lineBuffer, 0, c);
}
lineBuffer[c] = readBuffer[0];
}
throw new InvalidDataException("Header is too large");
}
var headers = new Dictionary<string, string>();
string line = null;
try
{
line = await ReadLineAsync();
var sp = line.Split(' ');
if (sp.Length != 3 || !sp[2].StartsWith("HTTP") || sp[0] != "GET")
return null;
var path = sp[1];
while (true)
{
line = await ReadLineAsync();
if (line == "")
break;
sp = line.Split(new[] {':'}, 2);
headers[sp[0]] = sp[1].TrimStart();
}
error = false;
return new SimpleWebSocketHttpRequest(stream, path, headers);
}
catch
{
error = true;
return null;
}
finally
{
if (error)
stream.Dispose();
}
}
public void Listen()
{
var listener = new TcpListener(_address, _port);
listener.Start();
_listener = listener;
}
public SimpleWebSocketHttpServer(IPAddress address, int port)
{
_address = address;
_port = port;
}
public void Dispose()
{
_listener?.Stop();
_listener = null;
}
}
public class SimpleWebSocketHttpRequest : IDisposable
{
public Dictionary<string, string> Headers { get; }
public string Path { get; }
private NetworkStream _stream;
public bool IsWebsocketRequest { get; }
public IReadOnlyList<string> WebSocketProtocols { get; }
private string _websocketKey;
public SimpleWebSocketHttpRequest(NetworkStream stream, string path, Dictionary<string, string> headers)
{
Path = path;
Headers = headers;
_stream = stream;
if (headers.TryGetValue("Connection", out var h)
&& h.Contains("Upgrade")
&& headers.TryGetValue("Upgrade", out h) &&
h == "websocket"
&& headers.TryGetValue("Sec-WebSocket-Key", out _websocketKey))
{
IsWebsocketRequest = true;
if (headers.TryGetValue("Sec-WebSocket-Protocol", out h))
WebSocketProtocols = h.Split(',').Select(x => x.Trim()).ToArray();
else WebSocketProtocols = new string[0];
}
}
public async Task RespondAsync(int code, byte[] data, string contentType)
{
var headers = Encoding.UTF8.GetBytes($"HTTP/1.1 {code} {(HttpStatusCode)code}\r\nConnection: close\r\nContent-Type: {contentType}\r\nContent-Length: {data.Length}\r\n\r\n");
await _stream.WriteAsync(headers, 0, headers.Length);
await _stream.WriteAsync(data, 0, data.Length);
_stream.Dispose();
_stream = null;
}
public async Task<SimpleWebSocket> AcceptWebSocket(string protocol = null)
{
var handshakeSource = _websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
string handshake;
using (var sha1 = SHA1.Create())
handshake = Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(handshakeSource)));
var headers =
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "
+ handshake + "\r\n";
if (protocol != null)
headers += protocol + "\r\n";
headers += "\r\n";
var bheaders = Encoding.UTF8.GetBytes(headers);
await _stream.WriteAsync(bheaders, 0, bheaders.Length);
var s = _stream;
_stream = null;
return new SimpleWebSocket(s);
}
public void Dispose() => _stream?.Dispose();
}
public class SimpleWebSocket : IDisposable
{
class AsyncLock
{
private object _syncRoot = new object();
private Queue<TaskCompletionSource<IDisposable>> _queue = new Queue<TaskCompletionSource<IDisposable>>();
private bool _locked;
public Task<IDisposable> LockAsync()
{
lock (_syncRoot)
{
if (!_locked)
{
_locked = true;
return Task.FromResult<IDisposable>(new Lock(this));
}
else
{
var tcs = new TaskCompletionSource<IDisposable>();
_queue.Enqueue(tcs);
return tcs.Task;
}
}
}
private void Unlock()
{
lock (_syncRoot)
{
if (_queue.Count != 0)
_queue.Dequeue().SetResult(new Lock(this));
else
_locked = false;
}
}
class Lock : IDisposable
{
private AsyncLock _parent;
private object _syncRoot = new object();
public Lock(AsyncLock parent)
{
_parent = parent;
}
public void Dispose()
{
lock (_syncRoot)
{
if (_parent == null)
return;
var p = _parent;
_parent = null;
p.Unlock();
}
}
}
}
private Stream _stream;
private AsyncLock _sendLock = new AsyncLock();
private AsyncLock _recvLock = new AsyncLock();
private const int WebsocketInitialHeaderLength = 2;
private const int WebsocketLen16Length = 4;
private const int WebsocketLen64Length = 10;
private const int WebsocketLen16Code = 126;
private const int WebsocketLen64Code = 127;
[StructLayout(LayoutKind.Explicit)]
struct WebSocketHeader
{
[FieldOffset(0)] public byte Mask;
[FieldOffset(1)] public byte Length8;
[FieldOffset(2)] public ushort Length16;
[FieldOffset(2)] public ulong Length64;
}
readonly byte[] _sendHeaderBuffer = new byte[10];
readonly MemoryStream _receiveFrameStream = new MemoryStream();
readonly MemoryStream _receiveMessageStream = new MemoryStream();
private FrameType _currentMessageFrameType;
enum FrameType
{
Continue = 0x0,
Text = 0x1,
Binary = 0x2,
Close = 0x8,
Ping = 0x9,
Pong = 0xA
}
internal SimpleWebSocket(Stream stream)
{
_stream = stream;
}
public void Dispose()
{
_stream?.Dispose();
_stream = null;
}
public Task SendMessage(string text)
{
var data = Encoding.UTF8.GetBytes(text);
return SendMessage(true, data);
}
public Task SendMessage(bool isText, byte[] data) => SendMessage(isText, data, 0, data.Length);
public Task SendMessage(bool isText, byte[] data, int offset, int length)
=> SendFrame(isText ? FrameType.Text : FrameType.Binary, data, offset, length);
async Task SendFrame(FrameType type, byte[] data, int offset, int length)
{
using (var l = await _sendLock.LockAsync())
{
var header = new WebSocketHeader();
int headerLength;
if (data.Length <= 125)
{
headerLength = WebsocketInitialHeaderLength;
header.Length8 = (byte) length;
}
else if (length <= 0xffff)
{
headerLength = WebsocketLen16Length;
header.Length8 = WebsocketLen16Code;
header.Length16 = (ushort) IPAddress.HostToNetworkOrder((short) (ushort) length);
}
else
{
headerLength = WebsocketLen64Length;
header.Length8 = WebsocketLen64Code;
header.Length64 = (ulong) IPAddress.HostToNetworkOrder((long) length);
}
var endOfMessage = true;
header.Mask = (byte) (((endOfMessage ? 1u : 0u) << 7) | ((byte) (type) & 0xf));
unsafe
{
Marshal.Copy(new IntPtr(&header), _sendHeaderBuffer, 0, headerLength);
}
await _stream.WriteAsync(_sendHeaderBuffer, 0, headerLength);
await _stream.WriteAsync(data, offset, length);
}
}
struct Frame
{
public byte[] Data;
public bool EndOfMessage;
public FrameType FrameType;
}
byte[] _recvHeaderBuffer = new byte[8];
byte[] _maskBuffer = new byte[4];
async Task<Frame> ReadFrame()
{
_receiveFrameStream.Position = 0;
_receiveFrameStream.SetLength(0);
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
var masked = (_recvHeaderBuffer[1] & 0x80) != 0;
var len0 = (_recvHeaderBuffer[1] & 0x7F);
var endOfMessage = (_recvHeaderBuffer[0] & 0x80) != 0;
var frameType = (FrameType) (_recvHeaderBuffer[0] & 0xf);
int length;
if (len0 <= 125)
length = len0;
else if (len0 == WebsocketLen16Code)
{
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
length = (ushort) IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_recvHeaderBuffer, 0));
}
else
{
await ReadExact(_stream, _recvHeaderBuffer, 0, 8);
length = (int) (ulong) IPAddress.NetworkToHostOrder((long) BitConverter.ToUInt64(_recvHeaderBuffer, 0));
}
if (masked)
await ReadExact(_stream, _maskBuffer, 0, 4);
await ReadExact(_stream, _receiveFrameStream, length);
var data = _receiveFrameStream.ToArray();
if(masked)
for (var c = 0; c < data.Length; c++)
data[c] = (byte) (data[c] ^ _maskBuffer[c % 4]);
return new Frame
{
Data = data,
EndOfMessage = endOfMessage,
FrameType = frameType
};
}
public async Task<SimpleWebSocketMessage> ReceiveMessage()
{
using (await _recvLock.LockAsync())
{
while (true)
{
var frame = await ReadFrame();
if (frame.FrameType == FrameType.Close)
return null;
if (frame.FrameType == FrameType.Ping)
await SendFrame(FrameType.Pong, frame.Data, 0, frame.Data.Length);
if (frame.FrameType == FrameType.Text || frame.FrameType == FrameType.Binary)
{
var isText = frame.FrameType == FrameType.Text;
if (_receiveMessageStream.Length == 0 && frame.EndOfMessage)
return new SimpleWebSocketMessage
{
IsText = isText,
Data = frame.Data
};
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
_currentMessageFrameType = frame.FrameType;
}
if (frame.FrameType == FrameType.Continue)
{
frame.FrameType = _currentMessageFrameType;
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
if (frame.EndOfMessage)
{
var isText = frame.FrameType == FrameType.Text;
var data = _receiveMessageStream.ToArray();
_receiveMessageStream.Position = 0;
_receiveMessageStream.SetLength(0);
return new SimpleWebSocketMessage
{
IsText = isText,
Data = data
};
}
}
}
}
}
byte[] _readExactBuffer = new byte[4096];
async Task ReadExact(Stream from, MemoryStream to, int length)
{
while (length>0)
{
var toRead = Math.Min(length, _readExactBuffer.Length);
var read = await from.ReadAsync(_readExactBuffer, 0, toRead);
to.Write(_readExactBuffer, 0, read);
if (read <= 0)
throw new EndOfStreamException();
length -= read;
}
}
async Task ReadExact(Stream from, byte[] to, int offset, int length)
{
while (length > 0)
{
var read = await from.ReadAsync(to, offset, length);
if (read <= 0)
throw new EndOfStreamException();
length -= read;
offset += read;
}
}
}
public class SimpleWebSocketMessage
{
public bool IsText { get; set; }
public byte[] Data { get; set; }
public string AsString()
{
return Encoding.UTF8.GetString(Data);
}
}
}

2
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/.gitignore

@ -0,0 +1,2 @@
build
node_modules

8878
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json

File diff suppressed because it is too large

41
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json

@ -0,0 +1,41 @@
{
"name": "simple",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"webpack-ver": "cross-env NODE_ENV=production webpack --version",
"dist": "cross-env NODE_ENV=production webpack --display-error-details",
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details"
},
"author": "",
"license": "ISC",
"devDependencies": {
"awesome-typescript-loader": "^5.0.0",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.1.6",
"css-loader": "^1.0.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.1",
"source-map-loader": "^0.2.3",
"style-loader": "^0.21.0",
"to-string-loader": "^1.1.5",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^2.9.2",
"url-loader": "^1.0.1",
"webpack": "~4.16.3",
"webpack-cli": "~2.1.3",
"webpack-livereload-plugin": "~2.1.1"
},
"dependencies": {
"@types/react": "^16.3.14",
"@types/react-dom": "^16.0.5",
"mobx": "4.3.0",
"mobx-react": "^5.1.2",
"react": "^16.3.2",
"react-dom": "^16.3.2"
}
}

57
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/FramePresenter.tsx

@ -0,0 +1,57 @@
import {PreviewerFrame, PreviewerServerConnection} from "src/PreviewerServerConnection";
import * as React from "react";
interface PreviewerPresenterProps {
conn: PreviewerServerConnection;
}
export class PreviewerPresenter extends React.Component<PreviewerPresenterProps> {
private canvasRef: React.RefObject<HTMLCanvasElement>;
constructor(props: PreviewerPresenterProps) {
super(props);
this.state = {width: 1, height: 1};
this.canvasRef = React.createRef()
this.componentDidUpdate({
conn: null!
}, this.state);
}
componentDidMount(): void {
this.updateCanvas(this.canvasRef.current, this.props.conn.currentFrame);
}
componentDidUpdate(prevProps: Readonly<PreviewerPresenterProps>, prevState: Readonly<{}>, snapshot?: any): void {
if(prevProps.conn != this.props.conn)
{
if(prevProps.conn)
prevProps.conn.removeFrameListener(this.frameHandler);
if(this.props.conn)
this.props.conn.addFrameListener(this.frameHandler);
}
}
private frameHandler = (frame: PreviewerFrame)=>{
this.updateCanvas(this.canvasRef.current, frame);
};
updateCanvas(canvas: HTMLCanvasElement | null, frame: PreviewerFrame | null) {
if (!canvas)
return;
if (frame == null){
canvas.width = canvas.height = 1;
canvas.getContext('2d')!.clearRect(0,0,1,1);
}
else {
canvas.width = frame.data.width;
canvas.height = frame.data.height;
const ctx = canvas.getContext('2d')!;
ctx.putImageData(frame.data, 0,0);
}
}
render() {
return <canvas ref={this.canvasRef}/>
}
}

78
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/PreviewerServerConnection.ts

@ -0,0 +1,78 @@
export interface PreviewerFrame {
data: ImageData;
dpiX: number;
dpiY: number;
}
export class PreviewerServerConnection {
private nextFrame = {
width: 0,
height: 0,
stride: 0,
dpiX: 0,
dpiY: 0,
sequenceId: "0"
};
public currentFrame: PreviewerFrame | null;
private handlers = new Set<(frame: PreviewerFrame | null) => void>();
private conn: WebSocket;
public addFrameListener(listener: (frame: PreviewerFrame | null) => void) {
this.handlers.add(listener);
if (this.currentFrame)
listener(this.currentFrame);
}
public removeFrameListener(listener: (frame: PreviewerFrame | null) => void) {
this.handlers.delete(listener);
}
constructor(uri: string) {
this.currentFrame = null;
var conn = this.conn = new WebSocket(uri);
conn.binaryType = 'arraybuffer';
const onMessage = this.onMessage;
conn.onmessage = msg => onMessage(msg);
const onClose = () => this.setFrame(null);
conn.onclose = () => onClose();
conn.onerror = (err: Event) => {
onClose();
console.log("Connection error: " + err);
}
}
private onMessage = (msg: MessageEvent) => {
if (typeof msg.data == 'string' || msg.data instanceof String) {
const parts = msg.data.split(':');
if (parts[0] == 'frame') {
this.nextFrame = {
sequenceId: parts[1],
width: parseInt(parts[2]),
height: parseInt(parts[3]),
stride: parseInt(parts[4]),
dpiX: parseInt(parts[5]),
dpiY: parseInt(parts[6])
}
}
} else if (msg.data instanceof ArrayBuffer) {
const arr = new Uint8ClampedArray(msg.data, 0);
const imageData = new ImageData(arr, this.nextFrame.width, this.nextFrame.height);
this.conn.send('frame-received:' + this.nextFrame.sequenceId);
this.setFrame({
data: imageData,
dpiX: this.nextFrame.dpiX,
dpiY: this.nextFrame.dpiY
});
}
};
private setFrame(frame: PreviewerFrame | null) {
this.currentFrame = frame;
this.handlers.forEach(h => h(this.currentFrame));
}
}

14
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Avalonia XAML previewer web edition</title>
</head>
<body>
<div id="app">
<center>Loading...</center>
</div>
<noscript>Javascript is required</noscript>
</body>
</html>

15
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.tsx

@ -0,0 +1,15 @@
import * as React from "react";
import {PreviewerPresenter} from './FramePresenter'
import {PreviewerServerConnection} from "src/PreviewerServerConnection";
import * as ReactDOM from "react-dom";
const loc = window.location;
const conn = new PreviewerServerConnection((loc.protocol === "https:" ? "wss" : "ws") + "://" + loc.host + "/ws");
const App = function(){
return <div style={{width: '100%'}}>
<PreviewerPresenter conn={conn}/>
</div>
};
ReactDOM.render(<App/>, document.getElementById("app"));

35
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json

@ -0,0 +1,35 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"baseUrl": ".",
"experimentalDecorators": true,
"paths": {
"*": ["./node_modules/@types/*", "./node_modules/*"],
"src/*": ["./src/*"]
}
},
"include": ["./src/**/*"],
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts"
]
}

117
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js

@ -0,0 +1,117 @@
const webpack = require('webpack');
const path = require('path');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const prod = process.env.NODE_ENV == 'production';
class Printer {
apply(compiler) {
compiler.hooks.afterEmit.tap("Printer", ()=> console.log("Build completed at " + new Date().toString()));
compiler.hooks.watchRun.tap("Printer", ()=> console.log("Watch triggered at " + new Date().toString()));
}
}
const config = {
entry: {
bundle: './src/index.tsx'
},
output: {
path: path.resolve(__dirname, 'build'),
publicPath: '/',
filename: '[name].[chunkhash].js'
},
performance: { hints: false },
mode: prod ? "production" : "development",
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
exclude: [
path.resolve(__dirname, 'node_modules/mobx-state-router')
]
},
{
"oneOf": [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'awesome-typescript-loader'
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.(jpg|png)$/,
use: {
loader: "url-loader",
options: {
limit: 25000,
},
},
},
{
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/', // where the fonts will go
}
}]
},
{
loader: require.resolve('file-loader'),
exclude: [/\.(js|jsx|mjs|tsx|ts)$/, /\.html$/, /\.json$/],
options: {
name: 'assets/[name].[hash:8].[ext]',
},
}]
},
]
},
devtool: "source-map",
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json", logLevel: 'info' })],
extensions: ['.ts', '.tsx', '.js', '.json'],
alias: {
'src': path.resolve(__dirname, 'src')
}
},
plugins:
[
new Printer(),
new CleanWebpackPlugin([path.resolve(__dirname, 'build')]),
new MiniCssExtractPlugin({
filename: "[name].[chunkhash]h" +
".css",
chunkFilename: "[id].[chunkhash].css"
}),
new LiveReloadPlugin({appendScriptTag: !prod}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
filename: 'index.html' //relative to root of the application
}),
new CopyWebpackPlugin([
// relative path from src
//{ from: './src/favicon.ico' },
//{ from: './src/assets' }
]),
new CompressionPlugin({
test: /(\?.*)?$/i
})
]
};
module.exports = config;

53
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -5,6 +5,7 @@ using System.Reflection;
using System.Threading;
using System.Xml;
using Avalonia.Controls;
using Avalonia.DesignerSupport.Remote.HtmlTransport;
using Avalonia.Input;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
@ -24,15 +25,16 @@ namespace Avalonia.DesignerSupport.Remote
{
public string AppPath { get; set; }
public Uri Transport { get; set; }
public Uri HtmlMethodListenUri { get; set; }
public string Method { get; set; } = Methods.AvaloniaRemote;
public string SessionId { get; set; } = Guid.NewGuid().ToString();
}
static class Methods
internal static class Methods
{
public const string AvaloniaRemote = "avalonia-remote";
public const string Win32 = "win32";
public const string Html = "html";
}
static Exception Die(string error)
@ -52,6 +54,19 @@ namespace Avalonia.DesignerSupport.Remote
{
Console.Error.WriteLine("Usage: --transport transport_spec --session-id sid --method method app");
Console.Error.WriteLine();
Console.Error.WriteLine("--transport: transport used for communication with the IDE");
Console.Error.WriteLine(" 'tcp-bson' (e. g. 'tcp-bson://127.0.0.1:30243/') - TCP-based transport with BSON serialization of messages defined in Avalonia.Remote.Protocol");
Console.Error.WriteLine(" 'file' (e. g. 'file://C://my/file.xaml' - pseudo-transport that triggers XAML updates on file changes, useful as a standalone previewer tool, always uses http preview method");
Console.Error.WriteLine();
Console.Error.WriteLine("--session-id: session id to be sent to IDE process");
Console.Error.WriteLine();
Console.Error.WriteLine("--method: the way the XAML is displayed");
Console.Error.WriteLine(" 'avalonia-remote' - binary image is sent via transport connection in FrameMessage");
Console.Error.WriteLine(" 'win32' - XAML is displayed in win32 window (handle could be obtained from UpdateXamlResultMessage), IDE is responsible to use user32!SetParent");
Console.Error.WriteLine(" 'html' - Previewer starts an HTML server and displays XAML previewer as a web page");
Console.Error.WriteLine();
Console.Error.WriteLine("--html-url - endpoint for HTML method to listen on, e. g. http://127.0.0.1:8081");
Console.Error.WriteLine();
Console.Error.WriteLine("Example: --transport tcp-bson://127.0.0.1:30243/ --session-id 123 --method avalonia-remote MyApp.exe");
Console.Error.Flush();
return Die(null);
@ -74,6 +89,8 @@ namespace Avalonia.DesignerSupport.Remote
next = a => rv.Transport = new Uri(a, UriKind.Absolute);
else if (arg == "--method")
next = a => rv.Method = a;
else if (arg == "--html-url")
next = a => rv.HtmlMethodListenUri = new Uri(a, UriKind.Absolute);
else if (arg == "--session-id")
next = a => rv.SessionId = a;
else if (rv.AppPath == null)
@ -89,6 +106,9 @@ namespace Avalonia.DesignerSupport.Remote
{
PrintUsage();
}
if (next != null)
PrintUsage();
return rv;
}
@ -98,27 +118,40 @@ namespace Avalonia.DesignerSupport.Remote
{
return new BsonTcpTransport().Connect(IPAddress.Parse(transport.Host), transport.Port).Result;
}
if (transport.Scheme == "file")
{
return new FileWatcherTransport(transport);
}
PrintUsage();
return null;
}
interface IAppInitializer
{
Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
}
class AppInitializer<T> : IAppInitializer where T : AppBuilderBase<T>, new()
{
public Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport,
public IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport,
CommandLineArgs args, object obj)
{
var builder = (AppBuilderBase<T>) obj;
var builder = (AppBuilderBase<T>)obj;
if (args.Method == Methods.AvaloniaRemote)
builder.UseWindowingSubsystem(() => PreviewerWindowingPlatform.Initialize(transport));
if (args.Method == Methods.Html)
{
transport = new HtmlWebSocketTransport(transport,
args.HtmlMethodListenUri ?? new Uri("http://localhost:5000"));
builder.UseWindowingSubsystem(() =>
PreviewerWindowingPlatform.Initialize(transport));
}
if (args.Method == Methods.Win32)
builder.UseWindowingSubsystem("Avalonia.Win32");
builder.SetupWithoutStarting();
return builder.Instance;
return transport;
}
}
@ -128,6 +161,8 @@ namespace Avalonia.DesignerSupport.Remote
{
var args = ParseCommandLineArgs(cmdline);
var transport = CreateTransport(args.Transport);
if (transport is ITransportWithEnforcedMethod enforcedMethod)
args.Method = enforcedMethod.PreviewerMethod;
var asm = Assembly.LoadFile(System.IO.Path.GetFullPath(args.AppPath));
var entryPoint = asm.EntryPoint;
if (entryPoint == null)
@ -141,12 +176,14 @@ namespace Avalonia.DesignerSupport.Remote
var appBuilder = builderMethod.Invoke(null, null);
Log($"Initializing application in design mode");
var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer<>).MakeGenericType(appBuilder.GetType()));
var app = initializer.GetConfiguredApp(transport, args, appBuilder);
transport = initializer.ConfigureApp(transport, args, appBuilder);
s_transport = transport;
transport.OnMessage += OnTransportMessage;
transport.OnException += (t, e) => Die(e.ToString());
transport.Start();
Log("Sending StartDesignerSessionMessage");
transport.Send(new StartDesignerSessionMessage {SessionId = args.SessionId});
Dispatcher.UIThread.MainLoop(CancellationToken.None);
}

86
src/Avalonia.Native/GlPlatformFeature.cs

@ -8,42 +8,58 @@ namespace Avalonia.Native
{
class GlPlatformFeature : IWindowingPlatformGlFeature
{
private readonly IAvnGlDisplay _display;
public GlPlatformFeature(IAvnGlDisplay display)
{
_display = display;
var immediate = display.CreateContext(null);
var deferred = display.CreateContext(immediate);
GlDisplay = new GlDisplay(display, immediate.SampleCount, immediate.StencilSize);
ImmediateContext = new GlContext(Display, immediate);
DeferredContext = new GlContext(Display, deferred);
int major, minor;
GlInterface glInterface;
using (immediate.MakeCurrent())
{
var basic = new GlBasicInfoInterface(display.GetProcAddress);
basic.GetIntegerv(GlConsts.GL_MAJOR_VERSION, out major);
basic.GetIntegerv(GlConsts.GL_MINOR_VERSION, out minor);
_version = new GlVersion(GlProfileType.OpenGL, major, minor);
glInterface = new GlInterface(_version, (name) =>
{
var rv = _display.GetProcAddress(name);
return rv;
});
}
GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize);
ImmediateContext = new GlContext(GlDisplay, immediate, _version);
DeferredContext = new GlContext(GlDisplay, deferred, _version);
}
public IGlContext ImmediateContext { get; }
internal IGlContext ImmediateContext { get; }
public IGlContext MainContext => DeferredContext;
internal GlContext DeferredContext { get; }
internal GlDisplay GlDisplay;
public GlDisplay Display => GlDisplay;
private readonly GlVersion _version;
public IGlContext CreateContext() => new GlContext(GlDisplay,
_display.CreateContext(((GlContext)ImmediateContext).Context), _version);
}
class GlDisplay : IGlDisplay
class GlDisplay
{
private readonly IAvnGlDisplay _display;
public GlDisplay(IAvnGlDisplay display, int sampleCount, int stencilSize)
public GlDisplay(IAvnGlDisplay display, GlInterface glInterface, int sampleCount, int stencilSize)
{
_display = display;
SampleCount = sampleCount;
StencilSize = stencilSize;
GlInterface = new GlInterface((name, optional) =>
{
var rv = _display.GetProcAddress(name);
if (rv == IntPtr.Zero && !optional)
throw new OpenGlException($"{name} not found in system OpenGL");
return rv;
});
GlInterface = glInterface;
}
public GlDisplayType Type => GlDisplayType.OpenGL2;
public GlInterface GlInterface { get; }
public int SampleCount { get; }
@ -55,19 +71,26 @@ namespace Avalonia.Native
class GlContext : IGlContext
{
public IAvnGlContext Context { get; }
private readonly GlDisplay _display;
public IAvnGlContext Context { get; private set; }
public GlContext(GlDisplay display, IAvnGlContext context)
public GlContext(GlDisplay display, IAvnGlContext context, GlVersion version)
{
Display = display;
_display = display;
Context = context;
Version = version;
}
public IGlDisplay Display { get; }
public GlVersion Version { get; }
public GlInterface GlInterface => _display.GlInterface;
public int SampleCount => _display.SampleCount;
public int StencilSize => _display.StencilSize;
public IDisposable MakeCurrent() => Context.MakeCurrent();
public void MakeCurrent()
public void Dispose()
{
Context.LegacyMakeCurrent();
Context.Dispose();
Context = null;
}
}
@ -75,15 +98,18 @@ namespace Avalonia.Native
class GlPlatformSurfaceRenderTarget : IGlPlatformSurfaceRenderTarget
{
private IAvnGlSurfaceRenderTarget _target;
public GlPlatformSurfaceRenderTarget(IAvnGlSurfaceRenderTarget target)
private readonly IGlContext _context;
public GlPlatformSurfaceRenderTarget(IAvnGlSurfaceRenderTarget target, IGlContext context)
{
_target = target;
_context = context;
}
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
var feature = (GlPlatformFeature)AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
return new GlPlatformSurfaceRenderingSession(feature.Display, _target.BeginDrawing());
return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing());
}
public void Dispose()
@ -97,13 +123,13 @@ namespace Avalonia.Native
{
private IAvnGlSurfaceRenderingSession _session;
public GlPlatformSurfaceRenderingSession(GlDisplay display, IAvnGlSurfaceRenderingSession session)
public GlPlatformSurfaceRenderingSession(IGlContext context, IAvnGlSurfaceRenderingSession session)
{
Display = display;
Context = context;
_session = session;
}
public IGlDisplay Display { get; }
public IGlContext Context { get; }
public PixelSize Size
{
@ -129,14 +155,16 @@ namespace Avalonia.Native
class GlPlatformSurface : IGlPlatformSurface
{
private readonly IAvnWindowBase _window;
private readonly IGlContext _context;
public GlPlatformSurface(IAvnWindowBase window)
public GlPlatformSurface(IAvnWindowBase window, IGlContext context)
{
_window = window;
_context = context;
}
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
{
return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget());
return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), _context);
}
}

3
src/Avalonia.Native/PopupImpl.cs

@ -21,7 +21,8 @@ namespace Avalonia.Native
_glFeature = glFeature;
using (var e = new PopupEvents(this))
{
Init(factory.CreatePopup(e, _opts.UseGpu ? glFeature?.DeferredContext.Context : null), factory.CreateScreens());
var context = _opts.UseGpu ? glFeature?.DeferredContext : null;
Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context);
}
PopupPositioner = new ManagedPopupPositioner(new OsxManagedPopupPositionerPopupImplHelper(parent, MoveResize));
}

5
src/Avalonia.Native/WindowImpl.cs

@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
@ -21,8 +22,8 @@ namespace Avalonia.Native
_glFeature = glFeature;
using (var e = new WindowEvents(this))
{
Init(_native = factory.CreateWindow(e,
_opts.UseGpu ? glFeature?.DeferredContext.Context : null), factory.CreateScreens());
var context = _opts.UseGpu ? glFeature?.DeferredContext : null;
Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context);
}
NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory);

6
src/Avalonia.Native/WindowImplBase.cs

@ -56,6 +56,7 @@ namespace Avalonia.Native
private Size _lastRenderedLogicalSize;
private double _savedScaling;
private GlPlatformSurface _glSurface;
private IGlContext _glContext;
internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature)
{
@ -67,13 +68,14 @@ namespace Avalonia.Native
_cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
}
protected void Init(IAvnWindowBase window, IAvnScreens screens)
protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext)
{
_native = window;
_glContext = glContext;
Handle = new MacOSTopLevelWindowHandle(window);
if (_gpu)
_glSurface = new GlPlatformSurface(window);
_glSurface = new GlPlatformSurface(window, _glContext);
Screen = new ScreenImpl(screens);
_savedLogicalSize = ClientSize;
_savedScaling = Scaling;

2
src/Avalonia.OpenGL/Avalonia.OpenGL.csproj

@ -2,10 +2,12 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
</ItemGroup>

55
src/Avalonia.OpenGL/EglContext.cs

@ -1,6 +1,7 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using static Avalonia.OpenGL.EglConsts;
namespace Avalonia.OpenGL
{
@ -10,17 +11,27 @@ namespace Avalonia.OpenGL
private readonly EglInterface _egl;
private readonly object _lock = new object();
public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface)
public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface,
GlVersion version, int sampleCount, int stencilSize)
{
_disp = display;
_egl = egl;
Context = ctx;
OffscreenSurface = offscreenSurface;
Version = version;
SampleCount = sampleCount;
StencilSize = stencilSize;
using (MakeCurrent())
GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, b => _egl.GetProcAddress(b));
}
public IntPtr Context { get; }
public EglSurface OffscreenSurface { get; }
public IGlDisplay Display => _disp;
public GlVersion Version { get; }
public GlInterface GlInterface { get; }
public int SampleCount { get; }
public int StencilSize { get; }
public EglDisplay Display => _disp;
public IDisposable Lock()
{
@ -28,17 +39,53 @@ namespace Avalonia.OpenGL
return Disposable.Create(() => Monitor.Exit(_lock));
}
public void MakeCurrent()
class RestoreContext : IDisposable
{
private readonly EglInterface _egl;
private readonly IntPtr _display;
private IntPtr _context, _read, _draw;
public RestoreContext(EglInterface egl, IntPtr defDisplay)
{
_egl = egl;
_display = _egl.GetCurrentDisplay();
if (_display == IntPtr.Zero)
_display = defDisplay;
_context = _egl.GetCurrentContext();
_read = _egl.GetCurrentSurface(EGL_READ);
_draw = _egl.GetCurrentSurface(EGL_DRAW);
}
public void Dispose()
{
_egl.MakeCurrent(_display, _draw, _read, _context);
}
}
public IDisposable MakeCurrent()
{
var old = new RestoreContext(_egl, _disp.Handle);
_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (!_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, Context))
throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl);
return old;
}
public void MakeCurrent(EglSurface surface)
public IDisposable MakeCurrent(EglSurface surface)
{
var old = new RestoreContext(_egl, _disp.Handle);
var surf = surface ?? OffscreenSurface;
_egl.MakeCurrent(_disp.Handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context))
throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl);
return old;
}
public void Dispose()
{
_egl.DestroyContext(_disp.Handle, Context);
OffscreenSurface?.Dispose();
}
}
}

56
src/Avalonia.OpenGL/EglDisplay.cs

@ -6,7 +6,7 @@ using static Avalonia.OpenGL.EglConsts;
namespace Avalonia.OpenGL
{
public class EglDisplay : IGlDisplay
public class EglDisplay
{
private readonly EglInterface _egl;
private readonly IntPtr _display;
@ -16,6 +16,9 @@ namespace Avalonia.OpenGL
public IntPtr Handle => _display;
private AngleOptions.PlatformApi? _angleApi;
private int _sampleCount;
private int _stencilSize;
private GlVersion _version;
public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null)
{
@ -76,13 +79,6 @@ namespace Avalonia.OpenGL
foreach (var cfg in new[]
{
new
{
Attributes = new[] {EGL_NONE},
Api = EGL_OPENGL_API,
RenderableTypeBit = EGL_OPENGL_BIT,
Type = GlDisplayType.OpenGL2
},
new
{
Attributes = new[]
@ -92,7 +88,7 @@ namespace Avalonia.OpenGL
},
Api = EGL_OPENGL_ES_API,
RenderableTypeBit = EGL_OPENGL_ES2_BIT,
Type = GlDisplayType.OpenGLES2
Version = new GlVersion(GlProfileType.OpenGLES, 2, 0)
}
})
{
@ -120,14 +116,16 @@ namespace Avalonia.OpenGL
continue;
_contextAttributes = cfg.Attributes;
_surfaceType = surfaceType;
Type = cfg.Type;
_version = cfg.Version;
_egl.GetConfigAttrib(_display, _config, EGL_SAMPLES, out _sampleCount);
_egl.GetConfigAttrib(_display, _config, EGL_STENCIL_SIZE, out _stencilSize);
goto Found;
}
}
}
Found:
if (_contextAttributes == null)
throw new OpenGlException("No suitable EGL config was found");
GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b));
}
public EglDisplay() : this(new EglInterface())
@ -135,8 +133,6 @@ namespace Avalonia.OpenGL
}
public GlDisplayType Type { get; }
public GlInterface GlInterface { get; }
public EglInterface EglInterface => _egl;
public EglContext CreateContext(IGlContext share)
{
@ -154,8 +150,8 @@ namespace Avalonia.OpenGL
});
if (surf == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl);
var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf));
rv.MakeCurrent(null);
var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf),
_version, _sampleCount, _stencilSize);
return rv;
}
@ -164,17 +160,11 @@ namespace Avalonia.OpenGL
var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes);
if (ctx == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglCreateContext", _egl);
var rv = new EglContext(this, _egl, ctx, offscreenSurface);
var rv = new EglContext(this, _egl, ctx, offscreenSurface, _version, _sampleCount, _stencilSize);
rv.MakeCurrent(null);
return rv;
}
public void ClearContext()
{
if (!_egl.MakeCurrent(_display, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero))
throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl);
}
public EglSurface CreateWindowSurface(IntPtr window)
{
var s = _egl.CreateWindowSurface(_display, _config, window, new[] {EGL_NONE, EGL_NONE});
@ -182,23 +172,5 @@ namespace Avalonia.OpenGL
throw OpenGlException.GetFormattedException("eglCreateWindowSurface", _egl);
return new EglSurface(this, _egl, s);
}
public int SampleCount
{
get
{
_egl.GetConfigAttrib(_display, _config, EGL_SAMPLES, out var rv);
return rv;
}
}
public int StencilSize
{
get
{
_egl.GetConfigAttrib(_display, _config, EGL_STENCIL_SIZE, out var rv);
return rv;
}
}
}
}

17
src/Avalonia.OpenGL/EglGlPlatformFeature.cs

@ -5,9 +5,14 @@ namespace Avalonia.OpenGL
{
public class EglGlPlatformFeature : IWindowingPlatformGlFeature
{
public IGlDisplay Display { get; set; }
public IGlContext ImmediateContext { get; set; }
public EglContext DeferredContext { get; set; }
private EglDisplay _display;
public EglDisplay Display => _display;
public IGlContext CreateContext()
{
return _display.CreateContext(DeferredContext);
}
public EglContext DeferredContext { get; private set; }
public IGlContext MainContext => DeferredContext;
public static void TryInitialize()
{
@ -21,12 +26,10 @@ namespace Avalonia.OpenGL
try
{
var disp = new EglDisplay();
var ctx = disp.CreateContext(null);
return new EglGlPlatformFeature
{
Display = disp,
ImmediateContext = ctx,
DeferredContext = (EglContext)disp.CreateContext(ctx)
_display = disp,
DeferredContext = disp.CreateContext(null)
};
}
catch(Exception e)

24
src/Avalonia.OpenGL/EglGlPlatformSurface.cs

@ -16,9 +16,9 @@ namespace Avalonia.OpenGL
private readonly EglContext _context;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
public EglGlPlatformSurface(EglDisplay display, EglContext context, IEglWindowGlPlatformSurfaceInfo info)
public EglGlPlatformSurface(EglContext context, IEglWindowGlPlatformSurfaceInfo info)
{
_display = display;
_display = context.Display;
_context = context;
_info = info;
}
@ -58,12 +58,12 @@ namespace Avalonia.OpenGL
{
if (IsCorrupted)
throw new RenderTargetCorruptedException();
_context.MakeCurrent(_glSurface);
var restoreContext = _context.MakeCurrent(_glSurface);
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative();
_display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE);
return new Session(_display, _context, _glSurface, _info, l);
return new Session(_display, _context, _glSurface, _info, l, restoreContext);
}
catch
{
@ -79,32 +79,34 @@ namespace Avalonia.OpenGL
private readonly IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglDisplay _display;
private IDisposable _lock;
private readonly IDisposable _restoreContext;
public Session(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info,
IDisposable @lock)
IDisposable @lock, IDisposable restoreContext)
{
_context = context;
_display = display;
_glSurface = glSurface;
_info = info;
_lock = @lock;
_restoreContext = restoreContext;
}
public void Dispose()
{
_context.Display.GlInterface.Flush();
_context.GlInterface.Flush();
_display.EglInterface.WaitGL();
_glSurface.SwapBuffers();
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative();
_context.Display.ClearContext();
_display.EglInterface.WaitNative(EglConsts.EGL_CORE_NATIVE_ENGINE);
_restoreContext.Dispose();
_lock.Dispose();
}
public IGlDisplay Display => _context.Display;
public IGlContext Context => _context;
public PixelSize Size => _info.Size;
public double Scaling => _info.Scaling;
public bool IsYFlipped { get; }

37
src/Avalonia.OpenGL/EglInterface.cs

@ -24,7 +24,7 @@ namespace Avalonia.OpenGL
[DllImport("libegl.dll", CharSet = CharSet.Ansi)]
static extern IntPtr eglGetProcAddress(string proc);
static Func<string, bool, IntPtr> Load()
static Func<string, IntPtr> Load()
{
var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem;
if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android)
@ -34,23 +34,17 @@ namespace Avalonia.OpenGL
var disp = eglGetProcAddress("eglGetPlatformDisplayEXT");
if (disp == IntPtr.Zero)
throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point");
return (name, optional) =>
{
var r = eglGetProcAddress(name);
if (r == IntPtr.Zero && !optional)
throw new OpenGlException($"Entry point {r} is not found");
return r;
};
return eglGetProcAddress;
}
throw new PlatformNotSupportedException();
}
static Func<string, bool, IntPtr> Load(string library)
static Func<string, IntPtr> Load(string library)
{
var dyn = AvaloniaLocator.Current.GetService<IDynamicLibraryLoader>();
var lib = dyn.LoadLibrary(library);
return (s, o) => dyn.GetProcAddress(lib, s, o);
return (s) => dyn.GetProcAddress(lib, s, true);
}
// ReSharper disable UnassignedGetOnlyAutoProperty
@ -63,7 +57,8 @@ namespace Avalonia.OpenGL
public EglGetDisplay GetDisplay { get; }
public delegate IntPtr EglGetPlatformDisplayEXT(int platform, IntPtr nativeDisplay, int[] attrs);
[GlEntryPoint("eglGetPlatformDisplayEXT", true)]
[GlEntryPoint("eglGetPlatformDisplayEXT")]
[GlOptionalEntryPoint]
public EglGetPlatformDisplayEXT GetPlatformDisplayEXT { get; }
public delegate bool EglInitialize(IntPtr display, out int major, out int minor);
@ -87,6 +82,10 @@ namespace Avalonia.OpenGL
IntPtr share, int[] attrs);
[GlEntryPoint("eglCreateContext")]
public EglCreateContext CreateContext { get; }
public delegate bool EglDestroyContext(IntPtr display, IntPtr context);
[GlEntryPoint("eglDestroyContext")]
public EglDestroyContext DestroyContext { get; }
public delegate IntPtr EglCreatePBufferSurface(IntPtr display, IntPtr config, int[] attrs);
[GlEntryPoint("eglCreatePbufferSurface")]
@ -96,6 +95,18 @@ namespace Avalonia.OpenGL
[GlEntryPoint("eglMakeCurrent")]
public EglMakeCurrent MakeCurrent { get; }
public delegate IntPtr EglGetCurrentContext();
[GlEntryPoint("eglGetCurrentContext")]
public EglGetCurrentContext GetCurrentContext { get; }
public delegate IntPtr EglGetCurrentDisplay();
[GlEntryPoint("eglGetCurrentDisplay")]
public EglGetCurrentContext GetCurrentDisplay { get; }
public delegate IntPtr EglGetCurrentSurface(int readDraw);
[GlEntryPoint("eglGetCurrentSurface")]
public EglGetCurrentSurface GetCurrentSurface { get; }
public delegate void EglDisplaySurfaceVoidDelegate(IntPtr display, IntPtr surface);
[GlEntryPoint("eglDestroySurface")]
public EglDisplaySurfaceVoidDelegate DestroySurface { get; }
@ -120,9 +131,9 @@ namespace Avalonia.OpenGL
[GlEntryPoint("eglWaitClient")]
public EglWaitGL WaitClient { get; }
public delegate bool EglWaitNative();
public delegate bool EglWaitNative(int engine);
[GlEntryPoint("eglWaitNative")]
public EglWaitGL WaitNative { get; }
public EglWaitNative WaitNative { get; }
public delegate IntPtr EglQueryString(IntPtr display, int i);

4
src/Avalonia.OpenGL/EglSurface.cs

@ -21,8 +21,6 @@ namespace Avalonia.OpenGL
}
public override bool IsInvalid => handle == IntPtr.Zero;
public IGlDisplay Display => _display;
public void SwapBuffers() => _egl.SwapBuffers(_display.Handle, handle);
}
}
}

72
src/Avalonia.OpenGL/GlBasicInfoInterface.cs

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Platform.Interop;
namespace Avalonia.OpenGL
{
public class GlBasicInfoInterface : GlBasicInfoInterface<object>
{
public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress) : base(getProcAddress, null)
{
}
public GlBasicInfoInterface(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : base(nativeGetProcAddress, null)
{
}
public delegate void GlGetIntegerv(int name, out int rv);
public delegate IntPtr GlGetString(int v);
public delegate IntPtr GlGetStringi(int v, int v1);
}
public class GlBasicInfoInterface<TContextInfo> : GlInterfaceBase<TContextInfo>
{
public GlBasicInfoInterface(Func<string, IntPtr> getProcAddress, TContextInfo context) : base(getProcAddress, context)
{
}
public GlBasicInfoInterface(Func<Utf8Buffer, IntPtr> nativeGetProcAddress, TContextInfo context) : base(nativeGetProcAddress, context)
{
}
[GlEntryPoint("glGetIntegerv")]
public GlBasicInfoInterface.GlGetIntegerv GetIntegerv { get; }
[GlEntryPoint("glGetString")]
public GlBasicInfoInterface.GlGetString GetStringNative { get; }
[GlEntryPoint("glGetStringi")]
public GlBasicInfoInterface.GlGetStringi GetStringiNative { get; }
public string GetString(int v)
{
var ptr = GetStringNative(v);
if (ptr != IntPtr.Zero)
return Marshal.PtrToStringAnsi(ptr);
return null;
}
public string GetString(int v, int index)
{
var ptr = GetStringiNative(v, index);
if (ptr != IntPtr.Zero)
return Marshal.PtrToStringAnsi(ptr);
return null;
}
public List<string> GetExtensions()
{
var sp = GetString(GlConsts.GL_EXTENSIONS);
if (sp != null)
return sp.Split(' ').ToList();
GetIntegerv(GlConsts.GL_NUM_EXTENSIONS, out int count);
var rv = new List<string>(count);
for (var c = 0; c < count; c++)
rv.Add(GetString(GlConsts.GL_EXTENSIONS, c));
return rv;
}
}
}

4674
src/Avalonia.OpenGL/GlConsts.cs

File diff suppressed because it is too large

8
src/Avalonia.OpenGL/GlDisplayType.cs

@ -1,8 +0,0 @@
namespace Avalonia.OpenGL
{
public enum GlDisplayType
{
OpenGL2,
OpenGLES2
}
}

113
src/Avalonia.OpenGL/GlEntryPointAttribute.cs

@ -2,16 +2,117 @@ using System;
namespace Avalonia.OpenGL
{
public interface IGlEntryPointAttribute
{
IntPtr GetProcAddress(Func<string, IntPtr> getProcAddress);
}
public interface IGlEntryPointAttribute<in TContext>
{
IntPtr GetProcAddress(TContext context, Func<string, IntPtr> getProcAddress);
}
[AttributeUsage(AttributeTargets.Property)]
public class GlEntryPointAttribute : Attribute
public class GlOptionalEntryPoint : Attribute
{
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class GlEntryPointAttribute : Attribute, IGlEntryPointAttribute
{
public string[] EntryPoints { get; }
public GlEntryPointAttribute(string entryPoint)
{
EntryPoints = new []{entryPoint};
}
/*
public GlEntryPointAttribute(params string[] entryPoints)
{
EntryPoints = entryPoints;
}
*/
public IntPtr GetProcAddress(Func<string, IntPtr> getProcAddress)
{
foreach (var name in EntryPoints)
{
var rv = getProcAddress(name);
if (rv != IntPtr.Zero)
return rv;
}
return IntPtr.Zero;
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class GlMinVersionEntryPoint : Attribute, IGlEntryPointAttribute<GlInterface.GlContextInfo>
{
public string EntryPoint { get; }
public bool Optional { get; }
private readonly string _entry;
private readonly GlProfileType? _profile;
private readonly int _minVersionMajor;
private readonly int _minVersionMinor;
public GlEntryPointAttribute(string entryPoint, bool optional = false)
public GlMinVersionEntryPoint(string entry, GlProfileType profile, int minVersionMajor,
int minVersionMinor)
{
EntryPoint = entryPoint;
Optional = optional;
_entry = entry;
_profile = profile;
_minVersionMajor = minVersionMajor;
_minVersionMinor = minVersionMinor;
}
public GlMinVersionEntryPoint(string entry, int minVersionMajor,
int minVersionMinor)
{
_entry = entry;
_minVersionMajor = minVersionMajor;
_minVersionMinor = minVersionMinor;
}
public IntPtr GetProcAddress(GlInterface.GlContextInfo context, Func<string, IntPtr> getProcAddress)
{
if(_profile.HasValue && context.Version.Type != _profile)
return IntPtr.Zero;
if(context.Version.Major<_minVersionMajor)
return IntPtr.Zero;
if (context.Version.Major == _minVersionMajor && context.Version.Minor < _minVersionMinor)
return IntPtr.Zero;
return getProcAddress(_entry);
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class GlExtensionEntryPoint : Attribute, IGlEntryPointAttribute<GlInterface.GlContextInfo>
{
private readonly string _entry;
private readonly GlProfileType? _profile;
private readonly string _extension;
public GlExtensionEntryPoint(string entry, GlProfileType profile, string extension)
{
_entry = entry;
_profile = profile;
_extension = extension;
}
public GlExtensionEntryPoint(string entry, string extension)
{
_entry = entry;
_extension = extension;
}
public IntPtr GetProcAddress(GlInterface.GlContextInfo context, Func<string, IntPtr> getProcAddress)
{
// Ignore different profile type
if (_profile.HasValue && _profile != context.Version.Type)
return IntPtr.Zero;
// Check if extension is supported by the current context
if (!context.Extensions.Contains(_extension))
return IntPtr.Zero;
return getProcAddress(_entry);
}
}
}

284
src/Avalonia.OpenGL/GlInterface.cs

@ -1,31 +1,60 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Platform.Interop;
using static Avalonia.OpenGL.GlConsts;
namespace Avalonia.OpenGL
{
public delegate IntPtr GlGetProcAddressDelegate(string procName);
public class GlInterface : GlInterfaceBase
public unsafe class GlInterface : GlBasicInfoInterface<GlInterface.GlContextInfo>
{
public string Version { get; }
public string Vendor { get; }
public string Renderer { get; }
public GlContextInfo ContextInfo { get; }
public GlInterface(Func<string, bool, IntPtr> getProcAddress) : base(getProcAddress)
public class GlContextInfo
{
public GlVersion Version { get; }
public HashSet<string> Extensions { get; }
public GlContextInfo(GlVersion version, HashSet<string> extensions)
{
Version = version;
Extensions = extensions;
}
public static GlContextInfo Create(GlVersion version, Func<string, IntPtr> getProcAddress)
{
var basicInfoInterface = new GlBasicInfoInterface(getProcAddress);
var exts = basicInfoInterface.GetExtensions();
return new GlContextInfo(version, new HashSet<string>(exts));
}
}
private GlInterface(GlContextInfo info, Func<string, IntPtr> getProcAddress) : base(getProcAddress, info)
{
ContextInfo = info;
Version = GetString(GlConsts.GL_VERSION);
Renderer = GetString(GlConsts.GL_RENDERER);
Vendor = GetString(GlConsts.GL_VENDOR);
Vendor = GetString(GlConsts.GL_VENDOR);
}
public GlInterface(Func<Utf8Buffer, IntPtr> n) : this(ConvertNative(n))
public GlInterface(GlVersion version, Func<string, IntPtr> getProcAddress) : this(
GlContextInfo.Create(version, getProcAddress), getProcAddress)
{
}
public GlInterface(GlVersion version, Func<Utf8Buffer, IntPtr> n) : this(version, ConvertNative(n))
{
}
public static GlInterface FromNativeUtf8GetProcAddress(Func<Utf8Buffer, IntPtr> getProcAddress) =>
new GlInterface(getProcAddress);
public static GlInterface FromNativeUtf8GetProcAddress(GlVersion version, Func<Utf8Buffer, IntPtr> getProcAddress) =>
new GlInterface(version, getProcAddress);
public T GetProcAddress<T>(string proc) => Marshal.GetDelegateForFunctionPointer<T>(GetProcAddress(proc));
@ -70,6 +99,249 @@ namespace Avalonia.OpenGL
[GlEntryPoint("glGetIntegerv")]
public GlGetIntegerv GetIntegerv { get; }
public delegate void GlGenFramebuffers(int count, int[] res);
[GlEntryPoint("glGenFramebuffers")]
public GlGenFramebuffers GenFramebuffers { get; }
public delegate void GlDeleteFramebuffers(int count, int[] framebuffers);
[GlEntryPoint("glDeleteFramebuffers")]
public GlDeleteFramebuffers DeleteFramebuffers { get; }
public delegate void GlBindFramebuffer(int target, int fb);
[GlEntryPoint("glBindFramebuffer")]
public GlBindFramebuffer BindFramebuffer { get; }
public delegate int GlCheckFramebufferStatus(int target);
[GlEntryPoint("glCheckFramebufferStatus")]
public GlCheckFramebufferStatus CheckFramebufferStatus { get; }
public delegate void GlGenRenderbuffers(int count, int[] res);
[GlEntryPoint("glGenRenderbuffers")]
public GlGenRenderbuffers GenRenderbuffers { get; }
public delegate void GlDeleteRenderbuffers(int count, int[] renderbuffers);
[GlEntryPoint("glDeleteRenderbuffers")]
public GlDeleteTextures DeleteRenderbuffers { get; }
public delegate void GlBindRenderbuffer(int target, int fb);
[GlEntryPoint("glBindRenderbuffer")]
public GlBindRenderbuffer BindRenderbuffer { get; }
public delegate void GlRenderbufferStorage(int target, int internalFormat, int width, int height);
[GlEntryPoint("glRenderbufferStorage")]
public GlRenderbufferStorage RenderbufferStorage { get; }
public delegate void GlFramebufferRenderbuffer(int target, int attachment,
int renderbufferTarget, int renderbuffer);
[GlEntryPoint("glFramebufferRenderbuffer")]
public GlFramebufferRenderbuffer FramebufferRenderbuffer { get; }
public delegate void GlGenTextures(int count, int[] res);
[GlEntryPoint("glGenTextures")]
public GlGenTextures GenTextures { get; }
public delegate void GlBindTexture(int target, int fb);
[GlEntryPoint("glBindTexture")]
public GlBindTexture BindTexture { get; }
public delegate void GlDeleteTextures(int count, int[] textures);
[GlEntryPoint("glDeleteTextures")]
public GlDeleteTextures DeleteTextures { get; }
public delegate void GlTexImage2D(int target, int level, int internalFormat, int width, int height, int border,
int format, int type, IntPtr data);
[GlEntryPoint("glTexImage2D")]
public GlTexImage2D TexImage2D { get; }
public delegate void GlTexParameteri(int target, int name, int value);
[GlEntryPoint("glTexParameteri")]
public GlTexParameteri TexParameteri { get; }
public delegate void GlFramebufferTexture2D(int target, int attachment,
int texTarget, int texture, int level);
[GlEntryPoint("glFramebufferTexture2D")]
public GlFramebufferTexture2D FramebufferTexture2D { get; }
public delegate int GlCreateShader(int shaderType);
[GlEntryPoint("glCreateShader")]
public GlCreateShader CreateShader { get; }
public delegate void GlShaderSource(int shader, int count, IntPtr strings, IntPtr lengths);
[GlEntryPoint("glShaderSource")]
public GlShaderSource ShaderSource { get; }
public void ShaderSourceString(int shader, string source)
{
using (var b = new Utf8Buffer(source))
{
var ptr = b.DangerousGetHandle();
var len = new IntPtr(b.ByteLen);
ShaderSource(shader, 1, new IntPtr(&ptr), new IntPtr(&len));
}
}
public delegate void GlCompileShader(int shader);
[GlEntryPoint("glCompileShader")]
public GlCompileShader CompileShader { get; }
public delegate void GlGetShaderiv(int shader, int name, int* parameters);
[GlEntryPoint("glGetShaderiv")]
public GlGetShaderiv GetShaderiv { get; }
public delegate void GlGetShaderInfoLog(int shader, int maxLength, out int length, void*infoLog);
[GlEntryPoint("glGetShaderInfoLog")]
public GlGetShaderInfoLog GetShaderInfoLog { get; }
public unsafe string CompileShaderAndGetError(int shader, string source)
{
ShaderSourceString(shader, source);
CompileShader(shader);
int compiled;
GetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled != 0)
return null;
int logLength;
GetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
if (logLength == 0)
logLength = 4096;
var logData = new byte[logLength];
int len;
fixed (void* ptr = logData)
GetShaderInfoLog(shader, logLength, out len, ptr);
return Encoding.UTF8.GetString(logData,0, len);
}
public delegate int GlCreateProgram();
[GlEntryPoint("glCreateProgram")]
public GlCreateProgram CreateProgram { get; }
public delegate void GlAttachShader(int program, int shader);
[GlEntryPoint("glAttachShader")]
public GlAttachShader AttachShader { get; }
public delegate void GlLinkProgram(int program);
[GlEntryPoint("glLinkProgram")]
public GlLinkProgram LinkProgram { get; }
public delegate void GlGetProgramiv(int program, int name, int* parameters);
[GlEntryPoint("glGetProgramiv")]
public GlGetProgramiv GetProgramiv { get; }
public delegate void GlGetProgramInfoLog(int program, int maxLength, out int len, void* infoLog);
[GlEntryPoint("glGetProgramInfoLog")]
public GlGetProgramInfoLog GetProgramInfoLog { get; }
public unsafe string LinkProgramAndGetError(int program)
{
LinkProgram(program);
int compiled;
GetProgramiv(program, GL_LINK_STATUS, &compiled);
if (compiled != 0)
return null;
int logLength;
GetProgramiv(program, GL_INFO_LOG_LENGTH, &logLength);
var logData = new byte[logLength];
int len;
fixed (void* ptr = logData)
GetProgramInfoLog(program, logLength, out len, ptr);
return Encoding.UTF8.GetString(logData,0, len);
}
public delegate void GlBindAttribLocation(int program, int index, IntPtr name);
[GlEntryPoint("glBindAttribLocation")]
public GlBindAttribLocation BindAttribLocation { get; }
public void BindAttribLocationString(int program, int index, string name)
{
using (var b = new Utf8Buffer(name))
BindAttribLocation(program, index, b.DangerousGetHandle());
}
public delegate void GlGenBuffers(int len, int[] rv);
[GlEntryPoint("glGenBuffers")]
public GlGenBuffers GenBuffers { get; }
public int GenBuffer()
{
var rv = new int[1];
GenBuffers(1, rv);
return rv[0];
}
public delegate void GlBindBuffer(int target, int buffer);
[GlEntryPoint("glBindBuffer")]
public GlBindBuffer BindBuffer { get; }
public delegate void GlBufferData(int target, IntPtr size, IntPtr data, int usage);
[GlEntryPoint("glBufferData")]
public GlBufferData BufferData { get; }
public delegate int GlGetAttribLocation(int program, IntPtr name);
[GlEntryPoint("glGetAttribLocation")]
public GlGetAttribLocation GetAttribLocation { get; }
public int GetAttribLocationString(int program, string name)
{
using (var b = new Utf8Buffer(name))
return GetAttribLocation(program, b.DangerousGetHandle());
}
public delegate void GlVertexAttribPointer(int index, int size, int type,
int normalized, int stride, IntPtr pointer);
[GlEntryPoint("glVertexAttribPointer")]
public GlVertexAttribPointer VertexAttribPointer { get; }
public delegate void GlEnableVertexAttribArray(int index);
[GlEntryPoint("glEnableVertexAttribArray")]
public GlEnableVertexAttribArray EnableVertexAttribArray { get; }
public delegate void GlUseProgram(int program);
[GlEntryPoint("glUseProgram")]
public GlUseProgram UseProgram { get; }
public delegate void GlDrawArrays(int mode, int first, IntPtr count);
[GlEntryPoint("glDrawArrays")]
public GlDrawArrays DrawArrays { get; }
public delegate void GlDrawElements(int mode, int count, int type, IntPtr indices);
[GlEntryPoint("glDrawElements")]
public GlDrawElements DrawElements { get; }
public delegate int GlGetUniformLocation(int program, IntPtr name);
[GlEntryPoint("glGetUniformLocation")]
public GlGetUniformLocation GetUniformLocation { get; }
public int GetUniformLocationString(int program, string name)
{
using (var b = new Utf8Buffer(name))
return GetUniformLocation(program, b.DangerousGetHandle());
}
public delegate void GlUniform1f(int location, float falue);
[GlEntryPoint("glUniform1f")]
public GlUniform1f Uniform1f { get; }
public delegate void GlUniformMatrix4fv(int location, int count, bool transpose, void* value);
[GlEntryPoint("glUniformMatrix4fv")]
public GlUniformMatrix4fv UniformMatrix4fv { get; }
public delegate void GlEnable(int what);
[GlEntryPoint("glEnable")]
public GlEnable Enable { get; }
public delegate void GlDeleteBuffers(int count, int[] buffers);
[GlEntryPoint("glDeleteBuffers")]
public GlDeleteBuffers DeleteBuffers { get; }
public delegate void GlDeleteProgram(int program);
[GlEntryPoint("glDeleteProgram")]
public GlDeleteProgram DeleteProgram { get; }
public delegate void GlDeleteShader(int shader);
[GlEntryPoint("glDeleteShader")]
public GlDeleteShader DeleteShader { get; }
// ReSharper restore UnassignedGetOnlyAutoProperty
}
}

68
src/Avalonia.OpenGL/GlInterfaceBase.cs

@ -1,53 +1,79 @@
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Avalonia.Platform.Interop;
namespace Avalonia.OpenGL
{
public class GlInterfaceBase
public class GlInterfaceBase : GlInterfaceBase<object>
{
private readonly Func<string, bool, IntPtr> _getProcAddress;
public GlInterfaceBase(Func<string, bool, IntPtr> getProcAddress)
public GlInterfaceBase(Func<string, IntPtr> getProcAddress) : base(getProcAddress, null)
{
}
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : base(nativeGetProcAddress, null)
{
}
}
public class GlInterfaceBase<TContext>
{
private readonly Func<string, IntPtr> _getProcAddress;
public GlInterfaceBase(Func<string, IntPtr> getProcAddress, TContext context)
{
_getProcAddress = getProcAddress;
foreach (var prop in this.GetType().GetProperties())
{
var a = prop.GetCustomAttribute<GlEntryPointAttribute>();
if (a != null)
var attrs = prop.GetCustomAttributes()
.Where(a =>
a is IGlEntryPointAttribute || a is IGlEntryPointAttribute<TContext>)
.ToList();
if(attrs.Count == 0)
continue;
var isOptional = prop.GetCustomAttribute<GlOptionalEntryPoint>() != null;
var fieldName = $"<{prop.Name}>k__BackingField";
var field = prop.DeclaringType.GetField(fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null)
throw new InvalidProgramException($"Expected property {prop.Name} to have {fieldName}");
IntPtr proc = IntPtr.Zero;
foreach (var attr in attrs)
{
var fieldName = $"<{prop.Name}>k__BackingField";
var field = prop.DeclaringType.GetField(fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null)
throw new InvalidProgramException($"Expected property {prop.Name} to have {fieldName}");
var proc = getProcAddress(a.EntryPoint, a.Optional);
if (attr is IGlEntryPointAttribute<TContext> typed)
proc = typed.GetProcAddress(context, getProcAddress);
else if (attr is IGlEntryPointAttribute untyped)
proc = untyped.GetProcAddress(getProcAddress);
if (proc != IntPtr.Zero)
field.SetValue(this, Marshal.GetDelegateForFunctionPointer(proc, prop.PropertyType));
break;
}
if (proc != IntPtr.Zero)
field.SetValue(this, Marshal.GetDelegateForFunctionPointer(proc, prop.PropertyType));
else if (!isOptional)
throw new OpenGlException("Unable to find a suitable GL function for " + prop.Name);
}
}
protected static Func<string, bool, IntPtr> ConvertNative(Func<Utf8Buffer, IntPtr> func) =>
(proc, optional) =>
protected static Func<string, IntPtr> ConvertNative(Func<Utf8Buffer, IntPtr> func) =>
(proc) =>
{
using (var u = new Utf8Buffer(proc))
{
var rv = func(u);
if (rv == IntPtr.Zero && !optional)
throw new OpenGlException("Missing function " + proc);
return rv;
}
};
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress) : this(ConvertNative(nativeGetProcAddress))
public GlInterfaceBase(Func<Utf8Buffer, IntPtr> nativeGetProcAddress, TContext context) : this(ConvertNative(nativeGetProcAddress), context)
{
}
public IntPtr GetProcAddress(string proc) => _getProcAddress(proc, true);
public IntPtr GetProcAddress(string proc, bool optional) => _getProcAddress(proc, optional);
public IntPtr GetProcAddress(string proc) => _getProcAddress(proc);
}
}

22
src/Avalonia.OpenGL/GlVersion.cs

@ -0,0 +1,22 @@
namespace Avalonia.OpenGL
{
public enum GlProfileType
{
OpenGL,
OpenGLES
}
public struct GlVersion
{
public GlProfileType Type { get; }
public int Major { get; }
public int Minor { get; }
public GlVersion(GlProfileType type, int major, int minor)
{
Type = type;
Major = major;
Minor = minor;
}
}
}

11
src/Avalonia.OpenGL/IGlContext.cs

@ -1,8 +1,13 @@
using System;
namespace Avalonia.OpenGL
{
public interface IGlContext
public interface IGlContext : IDisposable
{
IGlDisplay Display { get; }
void MakeCurrent();
GlVersion Version { get; }
GlInterface GlInterface { get; }
int SampleCount { get; }
int StencilSize { get; }
IDisposable MakeCurrent();
}
}

11
src/Avalonia.OpenGL/IGlDisplay.cs

@ -1,11 +0,0 @@
namespace Avalonia.OpenGL
{
public interface IGlDisplay
{
GlDisplayType Type { get; }
GlInterface GlInterface { get; }
void ClearContext();
int SampleCount { get; }
int StencilSize { get; }
}
}

2
src/Avalonia.OpenGL/IGlPlatformSurfaceRenderingSession.cs

@ -4,7 +4,7 @@ namespace Avalonia.OpenGL
{
public interface IGlPlatformSurfaceRenderingSession : IDisposable
{
IGlDisplay Display { get; }
IGlContext Context { get; }
PixelSize Size { get; }
double Scaling { get; }
bool IsYFlipped { get; }

9
src/Avalonia.OpenGL/IOpenGlAwarePlatformRenderInterface.cs

@ -0,0 +1,9 @@
using Avalonia.OpenGL.Imaging;
namespace Avalonia.OpenGL
{
public interface IOpenGlAwarePlatformRenderInterface
{
IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap();
}
}

3
src/Avalonia.OpenGL/IWindowingPlatformGlFeature.cs

@ -2,6 +2,7 @@ namespace Avalonia.OpenGL
{
public interface IWindowingPlatformGlFeature
{
IGlContext ImmediateContext { get; }
IGlContext CreateContext();
IGlContext MainContext { get; }
}
}

13
src/Avalonia.OpenGL/Imaging/IOpenGlTextureBitmapImpl.cs

@ -0,0 +1,13 @@
using System;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.OpenGL.Imaging
{
public interface IOpenGlTextureBitmapImpl : IBitmapImpl
{
IDisposable Lock();
void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling);
void SetDirty();
}
}

46
src/Avalonia.OpenGL/Imaging/OpenGlTextureBitmap.cs

@ -0,0 +1,46 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Threading;
namespace Avalonia.OpenGL.Imaging
{
public class OpenGlTextureBitmap : Bitmap, IAffectsRender
{
private IOpenGlTextureBitmapImpl _impl;
static IOpenGlTextureBitmapImpl CreateOrThrow()
{
if (!(AvaloniaLocator.Current.GetService<IPlatformRenderInterface>() is IOpenGlAwarePlatformRenderInterface
glAware))
throw new PlatformNotSupportedException("Rendering platform does not support OpenGL integration");
return glAware.CreateOpenGlTextureBitmap();
}
public OpenGlTextureBitmap()
: base(CreateOrThrow())
{
_impl = (IOpenGlTextureBitmapImpl)PlatformImpl.Item;
}
public IDisposable Lock() => _impl.Lock();
public void SetTexture(int textureId, int internalFormat, PixelSize size, double dpiScaling)
{
_impl.SetBackBuffer(textureId, internalFormat, size, dpiScaling);
SetIsDirty();
}
public void SetIsDirty()
{
if (Dispatcher.UIThread.CheckAccess())
CallInvalidated();
else
Dispatcher.UIThread.Post(CallInvalidated);
}
private void CallInvalidated() => Invalidated?.Invoke(this, EventArgs.Empty);
public event EventHandler Invalidated;
}
}

215
src/Avalonia.OpenGL/OpenGlControlBase.cs

@ -0,0 +1,215 @@
using System;
using Avalonia.Controls;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.OpenGL.Imaging;
using Avalonia.Rendering;
using Avalonia.VisualTree;
using static Avalonia.OpenGL.GlConsts;
namespace Avalonia.OpenGL
{
public abstract class OpenGlControlBase : Control
{
private IGlContext _context;
private int _fb, _texture, _renderBuffer;
private OpenGlTextureBitmap _bitmap;
private PixelSize _oldSize;
private bool _glFailed;
protected GlVersion GlVersion { get; private set; }
public sealed override void Render(DrawingContext context)
{
if(!EnsureInitialized())
return;
using (_context.MakeCurrent())
{
using (_bitmap.Lock())
{
var gl = _context.GlInterface;
gl.BindFramebuffer(GL_FRAMEBUFFER, _fb);
if (_oldSize != GetPixelSize())
ResizeTexture(gl);
OnOpenGlRender(gl, _fb);
gl.Flush();
}
}
context.DrawImage(_bitmap, new Rect(_bitmap.Size), Bounds);
base.Render(context);
}
void DoCleanup(bool callUserDeinit)
{
if (_context != null)
{
using (_context.MakeCurrent())
{
var gl = _context.GlInterface;
gl.BindTexture(GL_TEXTURE_2D, 0);
gl.BindFramebuffer(GL_FRAMEBUFFER, 0);
gl.DeleteFramebuffers(1, new[] { _fb });
using (_bitmap.Lock())
_bitmap.SetTexture(0, 0, new PixelSize(1, 1), 1);
gl.DeleteTextures(1, new[] { _texture });
gl.DeleteRenderbuffers(1, new[] { _renderBuffer });
_bitmap.Dispose();
try
{
if (callUserDeinit)
OnOpenGlDeinit(_context.GlInterface, _fb);
}
finally
{
_context.Dispose();
_context = null;
}
}
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
DoCleanup(true);
base.OnDetachedFromVisualTree(e);
}
bool EnsureInitialized()
{
if (_context != null)
return true;
if (_glFailed)
return false;
var feature = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
if (feature == null)
return false;
try
{
_context = feature.CreateContext();
}
catch (Exception e)
{
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
"Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e);
_glFailed = true;
return false;
}
GlVersion = _context.Version;
try
{
_bitmap = new OpenGlTextureBitmap();
}
catch (Exception e)
{
_context.Dispose();
_context = null;
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
"Unable to initialize OpenGL: unable to create OpenGlTextureBitmap: {exception}", e);
_glFailed = true;
return false;
}
using (_context.MakeCurrent())
{
try
{
_oldSize = GetPixelSize();
var gl = _context.GlInterface;
var oneArr = new int[1];
gl.GenFramebuffers(1, oneArr);
_fb = oneArr[0];
gl.BindFramebuffer(GL_FRAMEBUFFER, _fb);
gl.GenTextures(1, oneArr);
_texture = oneArr[0];
ResizeTexture(gl);
gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
{
int code;
while ((code = gl.GetError()) != 0)
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
"Unable to initialize OpenGL FBO: {code}", code);
_glFailed = true;
return false;
}
}
catch(Exception e)
{
Logger.TryGet(LogEventLevel.Error)?.Log("OpenGL", "OpenGlControlBase",
"Unable to initialize OpenGL FBO: {exception}", e);
_glFailed = true;
}
if (!_glFailed)
OnOpenGlInit(_context.GlInterface, _fb);
}
if (_glFailed)
{
DoCleanup(false);
}
return true;
}
void ResizeTexture(GlInterface gl)
{
var size = GetPixelSize();
gl.GetIntegerv( GL_TEXTURE_BINDING_2D, out var oldTexture);
gl.BindTexture(GL_TEXTURE_2D, _texture);
gl.TexImage2D(GL_TEXTURE_2D, 0,
GlVersion.Type == GlProfileType.OpenGLES ? GL_RGBA : GL_RGBA8,
size.Width, size.Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero);
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
gl.BindTexture(GL_TEXTURE_2D, oldTexture);
gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer);
gl.DeleteRenderbuffers(1, new[] { _renderBuffer });
var oneArr = new int[1];
gl.GenRenderbuffers(1, oneArr);
_renderBuffer = oneArr[0];
gl.BindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
gl.RenderbufferStorage(GL_RENDERBUFFER,
GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT,
size.Width, size.Height);
gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _renderBuffer);
gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer);
using (_bitmap.Lock())
_bitmap.SetTexture(_texture, GL_RGBA8, size, 1);
}
PixelSize GetPixelSize()
{
var scaling = VisualRoot.RenderScaling;
return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)),
Math.Max(1, (int)(Bounds.Height * scaling)));
}
protected virtual void OnOpenGlInit(GlInterface gl, int fb)
{
}
protected virtual void OnOpenGlDeinit(GlInterface gl, int fb)
{
}
protected abstract void OnOpenGlRender(GlInterface gl, int fb);
}
}

4
src/Avalonia.Remote.Protocol/BsonStreamTransport.cs

@ -144,5 +144,9 @@ namespace Avalonia.Remote.Protocol
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage;
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
public void Start()
{
}
}
}

1
src/Avalonia.Remote.Protocol/ITransport.cs

@ -8,5 +8,6 @@ namespace Avalonia.Remote.Protocol
Task Send(object data);
event Action<IAvaloniaRemoteTransportConnection, object> OnMessage;
event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
void Start();
}
}

2
src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs

@ -98,5 +98,7 @@ namespace Avalonia.Remote.Protocol
add => _onException.Add(value);
remove => _onException.Remove(value);
}
public void Start() => _conn.Start();
}
}

9
src/Avalonia.Remote.Protocol/TransportMessages.cs

@ -0,0 +1,9 @@
namespace Avalonia.Remote.Protocol
{
[AvaloniaRemoteMessageGuid("53778004-78fa-4381-8ec3-176a6f2328b6")]
public class HtmlTransportStartedMessage
{
public string Uri { get; set; }
}
}

2
src/Avalonia.Remote.Protocol/ViewportMessages.cs

@ -60,6 +60,8 @@
public int Width { get; set; }
public int Height { get; set; }
public int Stride { get; set; }
public double DpiX { get; set; }
public double DpiY { get; set; }
}
}

23
src/Avalonia.Visuals/Animation/Animators/BoxShadowAnimator.cs

@ -0,0 +1,23 @@
using Avalonia.Media;
namespace Avalonia.Animation.Animators
{
public class BoxShadowAnimator : Animator<BoxShadow>
{
static ColorAnimator s_colorAnimator = new ColorAnimator();
static DoubleAnimator s_doubleAnimator = new DoubleAnimator();
static BoolAnimator s_boolAnimator = new BoolAnimator();
public override BoxShadow Interpolate(double progress, BoxShadow oldValue, BoxShadow newValue)
{
return new BoxShadow
{
OffsetX = s_doubleAnimator.Interpolate(progress, oldValue.OffsetX, newValue.OffsetX),
OffsetY = s_doubleAnimator.Interpolate(progress, oldValue.OffsetY, newValue.OffsetY),
Blur = s_doubleAnimator.Interpolate(progress, oldValue.Blur, newValue.Blur),
Spread = s_doubleAnimator.Interpolate(progress, oldValue.Spread, newValue.Spread),
Color = s_colorAnimator.Interpolate(progress, oldValue.Color, newValue.Color),
IsInset = s_boolAnimator.Interpolate(progress, oldValue.IsInset, newValue.IsInset)
};
}
}
}

40
src/Avalonia.Visuals/Animation/Animators/BoxShadowsAnimator.cs

@ -0,0 +1,40 @@
using Avalonia.Media;
namespace Avalonia.Animation.Animators
{
public class BoxShadowsAnimator : Animator<BoxShadows>
{
private static readonly BoxShadowAnimator s_boxShadowAnimator = new BoxShadowAnimator();
public override BoxShadows Interpolate(double progress, BoxShadows oldValue, BoxShadows newValue)
{
int cnt = progress >= 1d ? newValue.Count : oldValue.Count;
if (cnt == 0)
return new BoxShadows();
BoxShadow first;
if (oldValue.Count > 0 && newValue.Count > 0)
first = s_boxShadowAnimator.Interpolate(progress, oldValue[0], newValue[0]);
else if (oldValue.Count > 0)
first = oldValue[0];
else
first = newValue[0];
if (cnt == 1)
return new BoxShadows(first);
var rest = new BoxShadow[cnt - 1];
for (var c = 0; c < rest.Length; c++)
{
var idx = c + 1;
if (oldValue.Count > idx && newValue.Count > idx)
rest[c] = s_boxShadowAnimator.Interpolate(progress, oldValue[idx], newValue[idx]);
else if (oldValue.Count > idx)
rest[c] = oldValue[idx];
else
rest[c] = newValue[idx];
}
return new BoxShadows(first, rest);
}
}
}

133
src/Avalonia.Visuals/Media/BoxShadow.cs

@ -0,0 +1,133 @@
using System;
using System.Globalization;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
namespace Avalonia.Media
{
public struct BoxShadow
{
public double OffsetX { get; set; }
public double OffsetY { get; set; }
public double Blur { get; set; }
public double Spread { get; set; }
public Color Color { get; set; }
public bool IsInset { get; set; }
static BoxShadow()
{
Animation.Animation.RegisterAnimator<BoxShadowAnimator>(prop =>
typeof(BoxShadow).IsAssignableFrom(prop.PropertyType));
}
public bool Equals(in BoxShadow other)
{
return OffsetX.Equals(other.OffsetX) && OffsetY.Equals(other.OffsetY) && Blur.Equals(other.Blur) && Spread.Equals(other.Spread) && Color.Equals(other.Color);
}
public override bool Equals(object obj)
{
return obj is BoxShadow other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = OffsetX.GetHashCode();
hashCode = (hashCode * 397) ^ OffsetY.GetHashCode();
hashCode = (hashCode * 397) ^ Blur.GetHashCode();
hashCode = (hashCode * 397) ^ Spread.GetHashCode();
hashCode = (hashCode * 397) ^ Color.GetHashCode();
return hashCode;
}
}
public bool IsEmpty => OffsetX == 0 && OffsetY == 0 && Blur == 0 && Spread == 0;
private readonly static char[] s_Separator = new char[] { ' ', '\t' };
struct ArrayReader
{
private int _index;
private string[] _arr;
public ArrayReader(string[] arr)
{
_arr = arr;
_index = 0;
}
public bool TryReadString(out string s)
{
s = null;
if (_index >= _arr.Length)
return false;
s = _arr[_index];
_index++;
return true;
}
public string ReadString()
{
if(!TryReadString(out var rv))
throw new FormatException();
return rv;
}
}
public static unsafe BoxShadow Parse(string s)
{
if(s == null)
throw new ArgumentNullException();
if (s.Length == 0)
throw new FormatException();
var p = s.Split(s_Separator, StringSplitOptions.RemoveEmptyEntries);
if (p.Length == 1 && p[0] == "none")
return default;
if (p.Length < 3 || p.Length > 6)
throw new FormatException();
bool inset = false;
var tokenizer = new ArrayReader(p);
string firstToken = tokenizer.ReadString();
if (firstToken == "inset")
{
inset = true;
firstToken = tokenizer.ReadString();
}
var offsetX = double.Parse(firstToken, CultureInfo.InvariantCulture);
var offsetY = double.Parse(tokenizer.ReadString(), CultureInfo.InvariantCulture);
double blur = 0;
double spread = 0;
tokenizer.TryReadString(out var token3);
tokenizer.TryReadString(out var token4);
tokenizer.TryReadString(out var token5);
if (token4 != null)
blur = double.Parse(token3, CultureInfo.InvariantCulture);
if (token5 != null)
spread = double.Parse(token4, CultureInfo.InvariantCulture);
var color = Color.Parse(token5 ?? token4 ?? token3);
return new BoxShadow
{
IsInset = inset,
OffsetX = offsetX,
OffsetY = offsetY,
Blur = blur,
Spread = spread,
Color = color
};
}
public Rect TransformBounds(in Rect rect)
=> IsInset ? rect : rect.Translate(new Vector(OffsetX, OffsetY)).Inflate(Spread + Blur);
}
}

137
src/Avalonia.Visuals/Media/BoxShadows.cs

@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Animation.Animators;
namespace Avalonia.Media
{
public struct BoxShadows
{
private readonly BoxShadow _first;
private readonly BoxShadow[] _list;
public int Count { get; }
static BoxShadows()
{
Animation.Animation.RegisterAnimator<BoxShadowsAnimator>(prop =>
typeof(BoxShadows).IsAssignableFrom(prop.PropertyType));
}
public BoxShadows(BoxShadow shadow)
{
_first = shadow;
_list = null;
Count = 1;
}
public BoxShadows(BoxShadow first, BoxShadow[] rest)
{
_first = first;
_list = rest;
Count = 1 + (rest?.Length ?? 0);
}
public BoxShadow this[int c]
{
get
{
if (c< 0 || c >= Count)
throw new IndexOutOfRangeException();
if (c == 0)
return _first;
return _list[c - 1];
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public struct BoxShadowsEnumerator
{
private int _index;
private BoxShadows _shadows;
public BoxShadowsEnumerator(BoxShadows shadows)
{
_shadows = shadows;
_index = -1;
}
public BoxShadow Current => _shadows[_index];
public bool MoveNext()
{
_index++;
return _index < _shadows.Count;
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
public BoxShadowsEnumerator GetEnumerator() => new BoxShadowsEnumerator(this);
private static readonly char[] s_Separators = new[] { ',' };
public static BoxShadows Parse(string s)
{
var sp = s.Split(s_Separators, StringSplitOptions.RemoveEmptyEntries);
if (sp.Length == 0
|| (sp.Length == 1 &&
(string.IsNullOrWhiteSpace(sp[0])
|| sp[0] == "none")))
return new BoxShadows();
var first = BoxShadow.Parse(sp[0]);
if (sp.Length == 1)
return new BoxShadows(first);
var rest = new BoxShadow[sp.Length - 1];
for (var c = 0; c < rest.Length; c++)
rest[c] = BoxShadow.Parse(sp[c + 1]);
return new BoxShadows(first, rest);
}
public Rect TransformBounds(in Rect rect)
{
var final = rect;
foreach (var shadow in this)
final = final.Union(shadow.TransformBounds(rect));
return final;
}
public bool HasInsetShadows
{
get
{
foreach(var boxShadow in this)
if (!boxShadow.IsEmpty && boxShadow.IsInset)
return true;
return false;
}
}
public static implicit operator BoxShadows(BoxShadow shadow) => new BoxShadows(shadow);
public bool Equals(BoxShadows other)
{
if (other.Count != Count)
return false;
for(var c=0; c<Count ; c++)
if (!this[c].Equals(other[c]))
return false;
return true;
}
public override bool Equals(object obj)
{
return obj is BoxShadows other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
int hashCode = 0;
foreach (var s in this)
hashCode = (hashCode * 397) ^ s.GetHashCode();
return hashCode;
}
}
}
}

44
src/Avalonia.Visuals/Media/Color.cs

@ -118,6 +118,50 @@ namespace Avalonia.Media
throw new FormatException($"Invalid color string: '{s}'.");
}
/// <summary>
/// Parses a color string.
/// </summary>
/// <param name="s">The color string.</param>
/// <param name="color">The parsed color</param>
/// <returns>The status of the operation.</returns>
public static bool TryParse(ReadOnlySpan<char> s, out Color color)
{
color = default;
if (s == null)
return false;
if (s.Length == 0)
return false;
if (s[0] == '#')
{
var or = 0u;
if (s.Length == 7)
{
or = 0xff000000;
}
else if (s.Length != 9)
{
return false;
}
if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
return false;
color = FromUInt32(parsed| or);
return true;
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
return false;
}
/// <summary>
/// Returns the string representation of the color.
/// </summary>

6
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -141,11 +141,13 @@ namespace Avalonia.Media
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <param name="boxShadow">Box shadow effect parameters</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0)
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0,
BoxShadow boxShadow = default)
{
if (brush == null && !PenIsVisible(pen))
{
@ -162,7 +164,7 @@ namespace Avalonia.Media
radiusY = Math.Min(radiusY, rect.Height / 2);
}
PlatformImpl.DrawRectangle(brush, pen, rect, radiusX, radiusY);
PlatformImpl.DrawRectangle(brush, pen, new RoundedRect(rect, radiusX, radiusY), boxShadow);
}
/// <summary>

10
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -63,17 +63,13 @@ namespace Avalonia.Platform
/// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
/// <param name="rect">The rectangle bounds.</param>
/// <param name="radiusX">The radius in the X dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Width/2
/// </param>
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.
/// This value will be clamped to the range of 0 to Height/2
/// </param>
/// <param name="boxShadows">Box shadow effect parameters</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0);
void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect,
BoxShadows boxShadow = default);
/// <summary>
/// Draws text.

2
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -116,5 +116,7 @@ namespace Avalonia.Platform
/// <param name="width">The glyph run's width.</param>
/// <returns></returns>
IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width);
bool SupportsIndividualRoundRects { get; }
}
}

10
src/Avalonia.Visuals/Rect.cs

@ -132,6 +132,16 @@ namespace Avalonia
/// Gets the bottom position of the rectangle.
/// </summary>
public double Bottom => _y + _height;
/// <summary>
/// Gets the left position.
/// </summary>
public double Left => _x;
/// <summary>
/// Gets the top position.
/// </summary>
public double Top => _y;
/// <summary>
/// Gets the top left point of the rectangle.

45
src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs

@ -1,50 +1,11 @@
using System;
using System.Threading;
using Avalonia.Utilities;
namespace Avalonia.Rendering
{
public class ManagedDeferredRendererLock : IDeferredRendererLock
public class ManagedDeferredRendererLock : DisposableLock, IDeferredRendererLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to lock the target surface or window
/// </summary>
/// <returns>IDisposable if succeeded to obtain the lock</returns>
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return new UnlockDisposable(_lock);
return null;
}
/// <summary>
/// Enters a waiting lock, only use from platform code, not from the renderer
/// </summary>
public IDisposable Lock()
{
Monitor.Enter(_lock);
return new UnlockDisposable(_lock);
}
private sealed class UnlockDisposable : IDisposable
{
private object _lock;
public UnlockDisposable(object @lock)
{
_lock = @lock;
}
public void Dispose()
{
object @lock = Interlocked.Exchange(ref _lock, null);
if (@lock != null)
{
Monitor.Exit(@lock);
}
}
}
}
}

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

@ -149,13 +149,14 @@ namespace Avalonia.Rendering.SceneGraph
}
/// <inheritdoc/>
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX = 0, double radiusY = 0)
public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect,
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, radiusX, radiusY))
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{
Add(new RectangleNode(Transform, brush, pen, rect, radiusX, radiusY, CreateChildScene(brush)));
Add(new RectangleNode(Transform, brush, pen, rect, boxShadows, CreateChildScene(brush)));
}
else
{

10
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@ -19,8 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="pen">The stroke pen.</param>
/// <param name="geometry">The geometry.</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public GeometryNode(
Matrix transform,
public GeometryNode(Matrix transform,
IBrush brush,
IPen pen,
IGeometryImpl geometry,
@ -64,6 +63,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill of the other draw operation.</param>
/// <param name="pen">The stroke of the other draw operation.</param>
/// <param name="geometry">The geometry of the other draw operation.</param>
/// <param name="boxShadow">The box shadow parameters</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
@ -72,9 +72,9 @@ namespace Avalonia.Rendering.SceneGraph
public bool Equals(Matrix transform, IBrush brush, IPen pen, IGeometryImpl geometry)
{
return transform == Transform &&
Equals(brush, Brush) &&
Equals(Pen, pen) &&
Equals(geometry, Geometry);
Equals(brush, Brush) &&
Equals(Pen, pen) &&
Equals(geometry, Geometry);
}
/// <inheritdoc/>

44
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@ -19,26 +19,23 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill brush.</param>
/// <param name="pen">The stroke pen.</param>
/// <param name="rect">The rectangle to draw.</param>
/// <param name="radiusY">The radius in the Y dimension of the rounded corners.</param>
/// <param name="radiusX">The radius in the X dimension of the rounded corners.</param>
/// <param name="boxShadow">The box shadow parameters</param>
/// <param name="childScenes">Child scenes for drawing visual brushes.</param>
public RectangleNode(
Matrix transform,
IBrush brush,
IPen pen,
Rect rect,
double radiusX,
double radiusY,
RoundedRect rect,
BoxShadows boxShadows,
IDictionary<IVisual, Scene> childScenes = null)
: base(rect, transform, pen)
: base(boxShadows.TransformBounds(rect.Rect), transform, pen)
{
Transform = transform;
Brush = brush?.ToImmutable();
Pen = pen?.ToImmutable();
Rect = rect;
RadiusX = radiusX;
RadiusY = radiusY;
ChildScenes = childScenes;
BoxShadows = boxShadows;
}
/// <summary>
@ -59,17 +56,12 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Gets the rectangle to draw.
/// </summary>
public Rect Rect { get; }
/// <summary>
/// The radius in the X dimension of the rounded corners.
/// </summary>
public double RadiusX { get; }
public RoundedRect Rect { get; }
/// <summary>
/// The radius in the Y dimension of the rounded corners.
/// The parameters for the box-shadow effect
/// </summary>
public double RadiusY { get; }
public BoxShadows BoxShadows { get; }
/// <inheritdoc/>
public override IDictionary<IVisual, Scene> ChildScenes { get; }
@ -81,21 +73,19 @@ namespace Avalonia.Rendering.SceneGraph
/// <param name="brush">The fill of the other draw operation.</param>
/// <param name="pen">The stroke of the other draw operation.</param>
/// <param name="rect">The rectangle of the other draw operation.</param>
/// <param name="radiusX"></param>
/// <param name="radiusY"></param>
/// <param name="boxShadow">The box shadow parameters of the other draw operation</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
public bool Equals(Matrix transform, IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows)
{
return transform == Transform &&
Equals(brush, Brush) &&
Equals(Pen, pen) &&
rect == Rect &&
Math.Abs(radiusX - RadiusX) < double.Epsilon &&
Math.Abs(radiusY - RadiusY) < double.Epsilon;
Media.BoxShadows.Equals(BoxShadows, boxShadows) &&
rect.Equals(Rect);
}
/// <inheritdoc/>
@ -103,7 +93,7 @@ namespace Avalonia.Rendering.SceneGraph
{
context.Transform = Transform;
context.DrawRectangle(Brush, Pen, Rect, RadiusX, RadiusY);
context.DrawRectangle(Brush, Pen, Rect, BoxShadows);
}
/// <inheritdoc/>
@ -116,13 +106,13 @@ namespace Avalonia.Rendering.SceneGraph
if (Brush != null)
{
var rect = Rect.Inflate((Pen?.Thickness / 2) ?? 0);
var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
return rect.Contains(p);
}
else
{
var borderRect = Rect.Inflate((Pen?.Thickness / 2) ?? 0);
var emptyRect = Rect.Deflate((Pen?.Thickness / 2) ?? 0);
var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0);
var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0);
return borderRect.Contains(p) && !emptyRect.Contains(p);
}
}

136
src/Avalonia.Visuals/RoundedRect.cs

@ -0,0 +1,136 @@
using System;
namespace Avalonia
{
public struct RoundedRect
{
public bool Equals(RoundedRect other)
{
return Rect.Equals(other.Rect) && RadiiTopLeft.Equals(other.RadiiTopLeft) && RadiiTopRight.Equals(other.RadiiTopRight) && RadiiBottomLeft.Equals(other.RadiiBottomLeft) && RadiiBottomRight.Equals(other.RadiiBottomRight);
}
public override bool Equals(object obj)
{
return obj is RoundedRect other && Equals(other);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = Rect.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiTopLeft.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiTopRight.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiBottomLeft.GetHashCode();
hashCode = (hashCode * 397) ^ RadiiBottomRight.GetHashCode();
return hashCode;
}
}
public Rect Rect { get; }
public Vector RadiiTopLeft { get; }
public Vector RadiiTopRight { get; }
public Vector RadiiBottomLeft { get; }
public Vector RadiiBottomRight { get; }
public RoundedRect(Rect rect, Vector radiiTopLeft, Vector radiiTopRight, Vector radiiBottomRight, Vector radiiBottomLeft)
{
Rect = rect;
RadiiTopLeft = radiiTopLeft;
RadiiTopRight = radiiTopRight;
RadiiBottomRight = radiiBottomRight;
RadiiBottomLeft = radiiBottomLeft;
}
public RoundedRect(Rect rect, double radiusTopLeft, double radiusTopRight, double radiusBottomRight,
double radiusBottomLeft)
: this(rect,
new Vector(radiusTopLeft, radiusTopLeft),
new Vector(radiusTopRight, radiusTopRight),
new Vector(radiusBottomRight, radiusBottomRight),
new Vector(radiusBottomLeft, radiusBottomLeft)
)
{
}
public RoundedRect(Rect rect, Vector radii) : this(rect, radii, radii, radii, radii)
{
}
public RoundedRect(Rect rect, double radiusX, double radiusY) : this(rect, new Vector(radiusX, radiusY))
{
}
public RoundedRect(Rect rect, double radius) : this(rect, radius, radius)
{
}
public RoundedRect(Rect rect) : this(rect, 0)
{
}
public static implicit operator RoundedRect(Rect r) => new RoundedRect(r);
public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default ||
RadiiBottomLeft != default;
public bool IsUniform =>
RadiiTopLeft.Equals(RadiiTopRight) &&
RadiiTopLeft.Equals(RadiiBottomRight) &&
RadiiTopLeft.Equals(RadiiBottomLeft);
public RoundedRect Inflate(double dx, double dy)
{
return Deflate(-dx, -dy);
}
public unsafe RoundedRect Deflate(double dx, double dy)
{
if (!IsRounded)
return new RoundedRect(Rect.Deflate(new Thickness(dx, dy)));
// Ported from SKRRect
var left = Rect.X + dx;
var top = Rect.Y + dy;
var right = left + Rect.Width - dx * 2;
var bottom = top + Rect.Height - dy * 2;
var radii = stackalloc Vector[4];
radii[0] = RadiiTopLeft;
radii[1] = RadiiTopRight;
radii[2] = RadiiBottomRight;
radii[3] = RadiiBottomLeft;
bool degenerate = false;
if (right <= left) {
degenerate = true;
left = right = (left + right)*0.5;
}
if (bottom <= top) {
degenerate = true;
top = bottom = (top + bottom) * 0.5;
}
if (degenerate)
{
return new RoundedRect(new Rect(left, top, right - left, bottom - top));
}
for (var c = 0; c < 4; c++)
{
var rx = Math.Max(0, radii[c].X - dx);
var ry = Math.Max(0, radii[c].Y - dy);
if (rx == 0 || ry == 0)
radii[c] = default;
else
radii[c] = new Vector(rx, ry);
}
return new RoundedRect(new Rect(left, top, right - left, bottom - top),
radii[0], radii[1], radii[2], radii[3]);
}
}
}

41
src/Avalonia.X11/Glx/Glx.cs

@ -15,11 +15,30 @@ namespace Avalonia.X11.Glx
public GlxMakeContextCurrent MakeContextCurrent { get; }
public delegate bool GlxMakeContextCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
[GlEntryPoint("glXGetCurrentContext")]
public GlxGetCurrentContext GetCurrentContext { get; }
public delegate IntPtr GlxGetCurrentContext();
[GlEntryPoint("glXGetCurrentDisplay")]
public GlxGetCurrentDisplay GetCurrentDisplay { get; }
public delegate IntPtr GlxGetCurrentDisplay();
[GlEntryPoint("glXGetCurrentDrawable")]
public GlxGetCurrentDrawable GetCurrentDrawable { get; }
public delegate IntPtr GlxGetCurrentDrawable();
[GlEntryPoint("glXGetCurrentReadDrawable")]
public GlxGetCurrentReadDrawable GetCurrentReadDrawable { get; }
public delegate IntPtr GlxGetCurrentReadDrawable();
[GlEntryPoint("glXCreatePbuffer")]
public GlxCreatePbuffer CreatePbuffer { get; }
public delegate IntPtr GlxCreatePbuffer(IntPtr dpy, IntPtr fbc, int[] attrib_list);
[GlEntryPoint("glXDestroyPbuffer")]
public GlxDestroyPbuffer DestroyPbuffer { get; }
public delegate IntPtr GlxDestroyPbuffer(IntPtr dpy, IntPtr fb);
[GlEntryPointAttribute("glXChooseVisual")]
public GlxChooseVisual ChooseVisual { get; }
public delegate XVisualInfo* GlxChooseVisual(IntPtr dpy, int screen, int[] attribList);
@ -78,12 +97,16 @@ namespace Avalonia.X11.Glx
[GlEntryPointAttribute("glXWaitGL")]
public GlxWaitGL WaitGL { get; }
public delegate void GlxWaitGL();
public delegate void GlxWaitGL();
public delegate int GlGetError();
[GlEntryPoint("glGetError")]
public GlGetError GetError { get; }
public delegate IntPtr GlxQueryExtensionsString(IntPtr display, int screen);
[GlEntryPoint("glXQueryExtensionsString")]
public GlxQueryExtensionsString QueryExtensionsString { get; }
public GlxInterface() : base(SafeGetProcAddress)
{
}
@ -92,16 +115,24 @@ namespace Avalonia.X11.Glx
// On some Linux systems, glXGetProcAddress will return valid pointers for even EGL functions.
// This makes Skia try to load some data from EGL,
// which can then cause segmentation faults because they return garbage.
public static IntPtr SafeGetProcAddress(string proc, bool optional)
public static IntPtr SafeGetProcAddress(string proc)
{
if (proc.StartsWith("egl", StringComparison.InvariantCulture))
{
return IntPtr.Zero;
}
return GlxConverted(proc, optional);
return GlxConverted(proc);
}
private static readonly Func<string, bool, IntPtr> GlxConverted = ConvertNative(GlxGetProcAddress);
private static readonly Func<string, IntPtr> GlxConverted = ConvertNative(GlxGetProcAddress);
public string[] GetExtensions(IntPtr display)
{
var s = Marshal.PtrToStringAnsi(QueryExtensionsString(display, 0));
return s.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim()).ToArray();
}
}
}

2
src/Avalonia.X11/Glx/GlxConsts.cs

@ -100,6 +100,8 @@ namespace Avalonia.X11.Glx
public const int GLX_CONTEXT_FLAGS_ARB = 0x2094;
public const int GLX_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001;
public const int GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002;
public const int GLX_CONTEXT_ES2_PROFILE_BIT_EXT = 0x00000004;
public const int GLX_CONTEXT_PROFILE_MASK_ARB = 0x9126;
}

56
src/Avalonia.X11/Glx/GlxContext.cs

@ -10,32 +10,80 @@ namespace Avalonia.X11.Glx
public GlxInterface Glx { get; }
private readonly X11Info _x11;
private readonly IntPtr _defaultXid;
private readonly bool _ownsPBuffer;
private readonly object _lock = new object();
public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, X11Info x11, IntPtr defaultXid)
public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display,
GlVersion version, int sampleCount, int stencilSize,
X11Info x11, IntPtr defaultXid,
bool ownsPBuffer)
{
Handle = handle;
Glx = glx;
_x11 = x11;
_defaultXid = defaultXid;
_ownsPBuffer = ownsPBuffer;
Display = display;
Version = version;
SampleCount = sampleCount;
StencilSize = stencilSize;
using (MakeCurrent())
GlInterface = new GlInterface(version, GlxInterface.SafeGetProcAddress);
}
public GlxDisplay Display { get; }
IGlDisplay IGlContext.Display => Display;
public GlVersion Version { get; }
public GlInterface GlInterface { get; }
public int SampleCount { get; }
public int StencilSize { get; }
public IDisposable Lock()
{
Monitor.Enter(_lock);
return Disposable.Create(() => Monitor.Exit(_lock));
}
class RestoreContext : IDisposable
{
private GlxInterface _glx;
private IntPtr _defaultDisplay;
private IntPtr _display;
private IntPtr _context;
private IntPtr _read;
private IntPtr _draw;
public RestoreContext(GlxInterface glx, IntPtr defaultDisplay)
{
_glx = glx;
_defaultDisplay = defaultDisplay;
_display = _glx.GetCurrentDisplay();
_context = _glx.GetCurrentContext();
_read = _glx.GetCurrentReadDrawable();
_draw = _glx.GetCurrentDrawable();
}
public void Dispose()
{
var disp = _display == IntPtr.Zero ? _defaultDisplay : _display;
_glx.MakeContextCurrent(disp, _draw, _read, _context);
}
}
public void MakeCurrent() => MakeCurrent(_defaultXid);
public IDisposable MakeCurrent() => MakeCurrent(_defaultXid);
public void MakeCurrent(IntPtr xid)
public IDisposable MakeCurrent(IntPtr xid)
{
var old = new RestoreContext(Glx, _x11.Display);
if (!Glx.MakeContextCurrent(_x11.Display, xid, xid, Handle))
throw new OpenGlException("glXMakeContextCurrent failed ");
return old;
}
public void Dispose()
{
Glx.DestroyContext(_x11.Display, Handle);
if (_ownsPBuffer)
Glx.DestroyPbuffer(_x11.Display, _defaultXid);
}
}
}

134
src/Avalonia.X11/Glx/GlxDisplay.cs

@ -1,28 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.OpenGL;
using static Avalonia.X11.Glx.GlxConsts;
namespace Avalonia.X11.Glx
{
unsafe class GlxDisplay : IGlDisplay
unsafe class GlxDisplay
{
private readonly X11Info _x11;
private readonly List<GlVersion> _probeProfiles;
private readonly IntPtr _fbconfig;
private readonly XVisualInfo* _visual;
public GlDisplayType Type => GlDisplayType.OpenGL2;
public GlInterface GlInterface { get; }
private string[] _displayExtensions;
private GlVersion? _version;
public XVisualInfo* VisualInfo => _visual;
public int SampleCount { get; }
public int StencilSize { get; }
public GlxContext ImmediateContext { get; }
public GlxContext DeferredContext { get; }
public GlxInterface Glx { get; } = new GlxInterface();
public GlxDisplay(X11Info x11)
public GlxDisplay(X11Info x11, List<GlVersion> probeProfiles)
{
_x11 = x11;
_probeProfiles = probeProfiles.ToList();
_displayExtensions = Glx.GetExtensions(_x11.Display);
var baseAttribs = new[]
{
@ -38,7 +38,8 @@ namespace Avalonia.X11.Glx
GLX_STENCIL_SIZE, 8,
};
int sampleCount = 0;
int stencilSize = 0;
foreach (var attribs in new[]
{
//baseAttribs.Concat(multiattribs),
@ -71,9 +72,9 @@ namespace Avalonia.X11.Glx
if (_visual == null)
throw new OpenGlException("Unable to get visual info from FBConfig");
if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_SAMPLES, out var samples) == 0)
SampleCount = samples;
sampleCount = samples;
if (Glx.GetFBConfigAttrib(_x11.Display, _fbconfig, GLX_STENCIL_SIZE, out var stencil) == 0)
StencilSize = stencil;
stencilSize = stencil;
var pbuffers = Enumerable.Range(0, 2).Select(_ => Glx.CreatePbuffer(_x11.Display, _fbconfig, new[]
{
@ -81,67 +82,104 @@ namespace Avalonia.X11.Glx
})).ToList();
XLib.XFlush(_x11.Display);
ImmediateContext = CreateContext(pbuffers[0],null);
DeferredContext = CreateContext(pbuffers[1], ImmediateContext);
ImmediateContext.MakeCurrent();
var err = Glx.GetError();
GlInterface = new GlInterface(GlxInterface.SafeGetProcAddress);
if (GlInterface.Version == null)
throw new OpenGlException("GL version string is null, aborting");
if (GlInterface.Renderer == null)
throw new OpenGlException("GL renderer string is null, aborting");
if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1")
DeferredContext = CreateContext(CreatePBuffer(), null,
sampleCount, stencilSize, true);
using (DeferredContext.MakeCurrent())
{
var blacklist = AvaloniaLocator.Current.GetService<X11PlatformOptions>()
?.GlxRendererBlacklist;
if (blacklist != null)
foreach(var item in blacklist)
if (GlInterface.Renderer.Contains(item))
throw new OpenGlException($"Renderer '{GlInterface.Renderer}' is blacklisted by '{item}'");
var glInterface = DeferredContext.GlInterface;
if (glInterface.Version == null)
throw new OpenGlException("GL version string is null, aborting");
if (glInterface.Renderer == null)
throw new OpenGlException("GL renderer string is null, aborting");
if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1")
{
var blacklist = AvaloniaLocator.Current.GetService<X11PlatformOptions>()
?.GlxRendererBlacklist;
if (blacklist != null)
foreach (var item in blacklist)
if (glInterface.Renderer.Contains(item))
throw new OpenGlException(
$"Renderer '{glInterface.Renderer}' is blacklisted by '{item}'");
}
}
}
IntPtr CreatePBuffer()
{
return Glx.CreatePbuffer(_x11.Display, _fbconfig, new[] { GLX_PBUFFER_WIDTH, 1, GLX_PBUFFER_HEIGHT, 1, 0 });
}
public void ClearContext() => Glx.MakeContextCurrent(_x11.Display,
IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
public GlxContext CreateContext(IGlContext share) => CreateContext(IntPtr.Zero, share);
public GlxContext CreateContext(IntPtr defaultXid, IGlContext share)
public GlxContext CreateContext() => CreateContext(DeferredContext);
GlxContext CreateContext(IGlContext share) => CreateContext(CreatePBuffer(), share,
share.SampleCount, share.StencilSize, true);
GlxContext CreateContext(IntPtr defaultXid, IGlContext share,
int sampleCount, int stencilSize, bool ownsPBuffer)
{
var sharelist = ((GlxContext)share)?.Handle ?? IntPtr.Zero;
IntPtr handle = default;
foreach (var ver in new[]
{
new Version(4, 0), new Version(3, 2),
new Version(3, 0), new Version(2, 0)
})
GlxContext Create(GlVersion profile)
{
var profileMask = GLX_CONTEXT_CORE_PROFILE_BIT_ARB;
if (profile.Type == GlProfileType.OpenGLES)
profileMask = GLX_CONTEXT_ES2_PROFILE_BIT_EXT;
var attrs = new[]
var attrs = new int[]
{
GLX_CONTEXT_MAJOR_VERSION_ARB, ver.Major,
GLX_CONTEXT_MINOR_VERSION_ARB, ver.Minor,
GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
GLX_CONTEXT_MAJOR_VERSION_ARB, profile.Major,
GLX_CONTEXT_MINOR_VERSION_ARB, profile.Minor,
GLX_CONTEXT_PROFILE_MASK_ARB, profileMask,
0
};
try
{
handle = Glx.CreateContextAttribsARB(_x11.Display, _fbconfig, sharelist, true, attrs);
if (handle != IntPtr.Zero)
break;
{
_version = profile;
return new GlxContext(new GlxInterface(), handle, this, profile,
sampleCount, stencilSize, _x11, defaultXid, ownsPBuffer);
}
}
catch
{
break;
return null;
}
return null;
}
GlxContext rv = null;
if (_version.HasValue)
{
rv = Create(_version.Value);
}
if (handle == IntPtr.Zero)
throw new OpenGlException("Unable to create direct GLX context");
return new GlxContext(new GlxInterface(), handle, this, _x11, defaultXid);
foreach (var v in _probeProfiles)
{
if (v.Type == GlProfileType.OpenGLES
&& !_displayExtensions.Contains("GLX_EXT_create_context_es2_profile"))
continue;
rv = Create(v);
if (rv != null)
{
_version = v;
break;
}
}
if (rv != null)
return rv;
throw new OpenGlException("Unable to create direct GLX context");
}
public void SwapBuffers(IntPtr xid) => Glx.SwapBuffers(_x11.Display, xid);

14
src/Avalonia.X11/Glx/GlxGlPlatformSurface.cs

@ -43,8 +43,8 @@ namespace Avalonia.X11.Glx
var l = _context.Lock();
try
{
_context.MakeCurrent(_info.Handle);
return new Session(_context, _info, l);
return new Session(_context, _info, l, _context.MakeCurrent(_info.Handle));
}
catch
{
@ -58,26 +58,28 @@ namespace Avalonia.X11.Glx
private readonly GlxContext _context;
private readonly EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo _info;
private IDisposable _lock;
private readonly IDisposable _clearContext;
public IGlContext Context => _context;
public Session(GlxContext context, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo info,
IDisposable @lock)
IDisposable @lock, IDisposable clearContext)
{
_context = context;
_info = info;
_lock = @lock;
_clearContext = clearContext;
}
public void Dispose()
{
_context.Display.GlInterface.Flush();
_context.GlInterface.Flush();
_context.Glx.WaitGL();
_context.Display.SwapBuffers(_info.Handle);
_context.Glx.WaitX();
_context.Display.ClearContext();
_clearContext.Dispose();
_lock.Dispose();
}
public IGlDisplay Display => _context.Display;
public PixelSize Size => _info.Size;
public double Scaling => _info.Scaling;
public bool IsYFlipped { get; }

13
src/Avalonia.X11/Glx/GlxPlatformFeature.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Logging;
using Avalonia.OpenGL;
@ -7,12 +8,13 @@ namespace Avalonia.X11.Glx
class GlxGlPlatformFeature : IWindowingPlatformGlFeature
{
public GlxDisplay Display { get; private set; }
public IGlContext ImmediateContext { get; private set; }
public IGlContext CreateContext() => Display.CreateContext();
public GlxContext DeferredContext { get; private set; }
public IGlContext MainContext => DeferredContext;
public static bool TryInitialize(X11Info x11)
public static bool TryInitialize(X11Info x11, List<GlVersion> glProfiles)
{
var feature = TryCreate(x11);
var feature = TryCreate(x11, glProfiles);
if (feature != null)
{
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>().ToConstant(feature);
@ -22,15 +24,14 @@ namespace Avalonia.X11.Glx
return false;
}
public static GlxGlPlatformFeature TryCreate(X11Info x11)
public static GlxGlPlatformFeature TryCreate(X11Info x11, List<GlVersion> glProfiles)
{
try
{
var disp = new GlxDisplay(x11);
var disp = new GlxDisplay(x11, glProfiles);
return new GlxGlPlatformFeature
{
Display = disp,
ImmediateContext = disp.ImmediateContext,
DeferredContext = disp.DeferredContext
};
}

12
src/Avalonia.X11/X11Platform.cs

@ -67,7 +67,7 @@ namespace Avalonia.X11
if (options.UseEGL)
EglGlPlatformFeature.TryInitialize();
else
GlxGlPlatformFeature.TryInitialize(Info);
GlxGlPlatformFeature.TryInitialize(Info, Options.GlProfiles);
}
@ -98,6 +98,16 @@ namespace Avalonia
public bool UseDBusMenu { get; set; }
public bool UseDeferredRendering { get; set; } = true;
public List<GlVersion> GlProfiles { get; set; } = new List<GlVersion>
{
new GlVersion(GlProfileType.OpenGL, 4, 0),
new GlVersion(GlProfileType.OpenGL, 3, 2),
new GlVersion(GlProfileType.OpenGL, 3, 0),
new GlVersion(GlProfileType.OpenGLES, 3, 2),
new GlVersion(GlProfileType.OpenGLES, 3, 0),
new GlVersion(GlProfileType.OpenGLES, 2, 0)
};
public List<string> GlxRendererBlacklist { get; set; } = new List<string>
{
// llvmpipe is a software GL rasterizer. If it's returned by glGetString,

2
src/Avalonia.X11/X11Window.cs

@ -165,7 +165,7 @@ namespace Avalonia.X11
if (egl != null)
surfaces.Insert(0,
new EglGlPlatformSurface((EglDisplay)egl.Display, egl.DeferredContext,
new EglGlPlatformSurface(egl.DeferredContext,
new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle)));
if (glx != null)
surfaces.Insert(0, new GlxGlPlatformSurface(glx.Display, glx.DeferredContext,

44
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs

@ -15,6 +15,8 @@ namespace Avalonia.LinuxFramebuffer.Output
private readonly EglGlPlatformSurface _eglPlatformSurface;
public PixelSize PixelSize => _mode.Resolution;
public double Scaling { get; set; }
public IGlContext MainContext => _deferredContext;
public DrmOutput(string path = null)
{
var card = new DrmCard(path);
@ -48,7 +50,6 @@ namespace Avalonia.LinuxFramebuffer.Output
private drmModeModeInfo _mode;
private EglDisplay _eglDisplay;
private EglSurface _eglSurface;
private EglContext _immediateContext;
private EglContext _deferredContext;
private IntPtr _currentBo;
private IntPtr _gbmTargetSurface;
@ -129,13 +130,15 @@ namespace Avalonia.LinuxFramebuffer.Output
return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf));
}
_immediateContext = CreateContext(null);
_deferredContext = CreateContext(_immediateContext);
_immediateContext.MakeCurrent(_eglSurface);
_eglDisplay.GlInterface.ClearColor(0, 0, 0, 0);
_eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
_eglSurface.SwapBuffers();
_deferredContext = CreateContext(null);
using (_deferredContext.MakeCurrent(_eglSurface))
{
_deferredContext.GlInterface.ClearColor(0, 0, 0, 0);
_deferredContext.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
_eglSurface.SwapBuffers();
}
var bo = gbm_surface_lock_front_buffer(_gbmTargetSurface);
var fbId = GetFbIdForBo(bo);
var connectorId = connector.Id;
@ -153,9 +156,10 @@ namespace Avalonia.LinuxFramebuffer.Output
for(var c=0;c<2;c++)
using (CreateGlRenderTarget().BeginDraw())
{
_eglDisplay.GlInterface.ClearColor(0, 0, 0, 0);
_eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
_deferredContext.GlInterface.ClearColor(0, 0, 0, 0);
_deferredContext.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
}
}
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
@ -179,15 +183,17 @@ namespace Avalonia.LinuxFramebuffer.Output
class RenderSession : IGlPlatformSurfaceRenderingSession
{
private readonly DrmOutput _parent;
private readonly IDisposable _clearContext;
public RenderSession(DrmOutput parent)
public RenderSession(DrmOutput parent, IDisposable clearContext)
{
_parent = parent;
_clearContext = clearContext;
}
public void Dispose()
{
_parent._eglDisplay.GlInterface.Flush();
_parent._deferredContext.GlInterface.Flush();
_parent._eglSurface.SwapBuffers();
var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface);
@ -225,11 +231,11 @@ namespace Avalonia.LinuxFramebuffer.Output
gbm_surface_release_buffer(_parent._gbmTargetSurface, _parent._currentBo);
_parent._currentBo = nextBo;
}
_parent._eglDisplay.ClearContext();
_clearContext.Dispose();
}
public IGlDisplay Display => _parent._eglDisplay;
public IGlContext Context => _parent._deferredContext;
public PixelSize Size => _parent._mode.Resolution;
@ -240,14 +246,14 @@ namespace Avalonia.LinuxFramebuffer.Output
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
_parent._deferredContext.MakeCurrent(_parent._eglSurface);
return new RenderSession(_parent);
return new RenderSession(_parent, _parent._deferredContext.MakeCurrent(_parent._eglSurface));
}
}
IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext;
public IGlContext CreateContext()
{
throw new NotImplementedException();
}
}

164
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -28,11 +28,12 @@ namespace Avalonia.Skia
private double _currentOpacity = 1.0f;
private readonly bool _canTextUseLcdRendering;
private Matrix _currentTransform;
private GRContext _grContext;
private bool _disposed;
private GRContext _grContext;
public GRContext GrContext => _grContext;
private readonly SKPaint _strokePaint = new SKPaint();
private readonly SKPaint _fillPaint = new SKPaint();
private readonly SKPaint _boxShadowPaint = new SKPaint();
/// <summary>
/// Context create info.
@ -184,19 +185,136 @@ namespace Avalonia.Skia
}
}
struct BoxShadowFilter : IDisposable
{
public SKPaint Paint;
private SKImageFilter _filter;
public SKClipOperation ClipOperation;
static float SkBlurRadiusToSigma(double radius) {
if (radius <= 0)
return 0.0f;
return 0.288675f * (float)radius + 0.5f;
}
public static BoxShadowFilter Create(SKPaint paint, BoxShadow shadow, double opacity)
{
var ac = shadow.Color;
SKImageFilter filter = null;
filter = SKImageFilter.CreateBlur(SkBlurRadiusToSigma(shadow.Blur), SkBlurRadiusToSigma(shadow.Blur));
var color = new SKColor(ac.R, ac.G, ac.B, (byte)(ac.A * opacity));
paint.Reset();
paint.IsAntialias = true;
paint.Color = color;
paint.ImageFilter = filter;
return new BoxShadowFilter
{
Paint = paint, _filter = filter,
ClipOperation = shadow.IsInset ? SKClipOperation.Intersect : SKClipOperation.Difference
};
}
public void Dispose()
{
Paint.Reset();
Paint = null;
_filter?.Dispose();
}
}
SKRect AreaCastingShadowInHole(
SKRect hole_rect,
float shadow_blur,
float shadow_spread,
float offsetX, float offsetY)
{
// Adapted from Chromium
var bounds = hole_rect;
bounds.Inflate(shadow_blur, shadow_blur);
if (shadow_spread < 0)
bounds.Inflate(-shadow_spread, -shadow_spread);
var offset_bounds = bounds;
offset_bounds.Offset(-offsetX, -offsetY);
bounds.Union(offset_bounds);
return bounds;
}
/// <inheritdoc />
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default)
{
var rc = rect.ToSKRect();
var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0)
return;
// Arbitrary chosen values
// On OSX Skia breaks OpenGL context when asked to draw, e. g. (0, 0, 623, 6666600) rect
if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192)
boxShadows = default;
var rc = rect.Rect.ToSKRect();
var isRounded = rect.IsRounded;
var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows);
using var skRoundRect = needRoundRect ? new SKRoundRect() : null;
if (needRoundRect)
skRoundRect.SetRectRadii(rc,
new[]
{
rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(),
rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(),
});
foreach (var boxShadow in boxShadows)
{
if (!boxShadow.IsEmpty && !boxShadow.IsInset)
{
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{
var spread = (float)boxShadow.Spread;
if (boxShadow.IsInset)
spread = -spread;
Canvas.Save();
if (isRounded)
{
using var shadowRect = new SKRoundRect(skRoundRect);
if (spread != 0)
shadowRect.Inflate(spread, spread);
Canvas.ClipRoundRect(skRoundRect,
shadow.ClipOperation, true);
var oldTransform = Transform;
Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
Canvas.DrawRoundRect(shadowRect, shadow.Paint);
Transform = oldTransform;
}
else
{
var shadowRect = rc;
if (spread != 0)
shadowRect.Inflate(spread, spread);
Canvas.ClipRect(rc, shadow.ClipOperation);
var oldTransform = Transform;
Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
Canvas.DrawRect(shadowRect, shadow.Paint);
Transform = oldTransform;
}
Canvas.Restore();
}
}
}
if (brush != null)
{
using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size))
{
if (isRounded)
{
Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint);
Canvas.DrawRoundRect(skRoundRect, paint.Paint);
}
else
{
@ -206,13 +324,41 @@ namespace Avalonia.Skia
}
}
foreach (var boxShadow in boxShadows)
{
if (!boxShadow.IsEmpty && boxShadow.IsInset)
{
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{
var spread = (float)boxShadow.Spread;
var offsetX = (float)boxShadow.OffsetX;
var offsetY = (float)boxShadow.OffsetY;
var outerRect = AreaCastingShadowInHole(rc, (float)boxShadow.Blur, spread, offsetX, offsetY);
Canvas.Save();
using var shadowRect = new SKRoundRect(skRoundRect);
if (spread != 0)
shadowRect.Deflate(spread, spread);
Canvas.ClipRoundRect(skRoundRect,
shadow.ClipOperation, true);
var oldTransform = Transform;
Transform = oldTransform * Matrix.CreateTranslation(boxShadow.OffsetX, boxShadow.OffsetY);
using (var outerRRect = new SKRoundRect(outerRect))
Canvas.DrawRoundRectDifference(outerRRect, shadowRect, shadow.Paint);
Transform = oldTransform;
Canvas.Restore();
}
}
}
if (pen?.Brush != null)
{
using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size))
{
if (isRounded)
{
Canvas.DrawRoundRect(rc, (float)radiusX, (float)radiusY, paint.Paint);
Canvas.DrawRoundRect(skRoundRect, paint.Paint);
}
else
{

15
src/Skia/Avalonia.Skia/ICustomSkiaGpu.cs → src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using Avalonia.OpenGL.Imaging;
using SkiaSharp;
namespace Avalonia.Skia
@ -6,18 +7,18 @@ namespace Avalonia.Skia
/// <summary>
/// Custom Skia gpu instance.
/// </summary>
public interface ICustomSkiaGpu
public interface ISkiaGpu
{
/// <summary>
/// Skia GrContext used.
/// </summary>
GRContext GrContext { get; }
/// <summary>
/// Attempts to create custom render target from given surfaces.
/// </summary>
/// <param name="surfaces">Surfaces.</param>
/// <returns>Created render target or <see langword="null"/> if it fails.</returns>
ICustomSkiaRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces);
ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces);
}
public interface IOpenGlAwareSkiaGpu : ISkiaGpu
{
IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap();
}
}

2
src/Skia/Avalonia.Skia/ICustomSkiaRenderSession.cs → src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderSession.cs

@ -6,7 +6,7 @@ namespace Avalonia.Skia
/// <summary>
/// Custom render session for Skia render target.
/// </summary>
public interface ICustomSkiaRenderSession : IDisposable
public interface ISkiaGpuRenderSession : IDisposable
{
/// <summary>
/// GrContext used by this session.

7
src/Skia/Avalonia.Skia/ICustomSkiaRenderTarget.cs → src/Skia/Avalonia.Skia/Gpu/ISkiaGpuRenderTarget.cs

@ -1,16 +1,19 @@
using System;
using SkiaSharp;
namespace Avalonia.Skia
{
/// <summary>
/// Custom Skia render target.
/// </summary>
public interface ICustomSkiaRenderTarget : IDisposable
public interface ISkiaGpuRenderTarget : IDisposable
{
/// <summary>
/// Start rendering to this render target.
/// </summary>
/// <returns></returns>
ICustomSkiaRenderSession BeginRendering();
ISkiaGpuRenderSession BeginRenderingSession();
bool IsCorrupted { get; }
}
}

73
src/Skia/Avalonia.Skia/GlRenderTarget.cs → src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs

@ -8,7 +8,7 @@ using static Avalonia.OpenGL.GlConsts;
namespace Avalonia.Skia
{
internal class GlRenderTarget : IRenderTargetWithCorruptionInfo
internal class GlRenderTarget : ISkiaGpuRenderTarget
{
private readonly GRContext _grContext;
private IGlPlatformSurfaceRenderTarget _surface;
@ -22,22 +22,52 @@ namespace Avalonia.Skia
public void Dispose() => _surface.Dispose();
public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true;
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
class GlGpuSession : ISkiaGpuRenderSession
{
private readonly GRBackendRenderTarget _backendRenderTarget;
private readonly SKSurface _surface;
private readonly IGlPlatformSurfaceRenderingSession _glSession;
public GlGpuSession(GRContext grContext,
GRBackendRenderTarget backendRenderTarget,
SKSurface surface,
IGlPlatformSurfaceRenderingSession glSession)
{
GrContext = grContext;
_backendRenderTarget = backendRenderTarget;
_surface = surface;
_glSession = glSession;
}
public void Dispose()
{
_surface.Canvas.Flush();
_surface.Dispose();
_backendRenderTarget.Dispose();
GrContext.Flush();
_glSession.Dispose();
}
public GRContext GrContext { get; }
public SKCanvas Canvas => _surface.Canvas;
public double ScaleFactor => _glSession.Scaling;
}
public ISkiaGpuRenderSession BeginRenderingSession()
{
var session = _surface.BeginDraw();
var glSession = _surface.BeginDraw();
bool success = false;
try
{
var disp = session.Display;
var disp = glSession.Context;
var gl = disp.GlInterface;
gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var fb);
var size = session.Size;
var scaling = session.Scaling;
var size = glSession.Size;
var scaling = glSession.Scaling;
if (size.Width <= 0 || size.Height <= 0 || scaling < 0)
{
session.Dispose();
glSession.Dispose();
throw new InvalidOperationException(
$"Can't create drawing context for surface with {size} size and {scaling} scaling");
}
@ -50,40 +80,21 @@ namespace Avalonia.Skia
{
_grContext.ResetContext();
GRBackendRenderTarget renderTarget =
var renderTarget =
new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize,
new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat()));
var surface = SKSurface.Create(_grContext, renderTarget,
session.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft,
glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft,
GRPixelConfig.Rgba8888.ToColorType());
var nfo = new DrawingContextImpl.CreateInfo
{
GrContext = _grContext,
Canvas = surface.Canvas,
Dpi = SkiaPlatform.DefaultDpi * scaling,
VisualBrushRenderer = visualBrushRenderer,
DisableTextLcdRendering = true
};
var ctx = new DrawingContextImpl(nfo, Disposable.Create(() =>
{
surface.Canvas.Flush();
surface.Dispose();
renderTarget.Dispose();
_grContext.Flush();
session.Dispose();
}));
success = true;
return ctx;
return new GlGpuSession(_grContext, renderTarget, surface, glSession);
}
}
finally
{
if(!success)
session.Dispose();
glSession.Dispose();
}
}
}

46
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs

@ -0,0 +1,46 @@
using System.Collections.Generic;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Imaging;
using SkiaSharp;
namespace Avalonia.Skia
{
class GlSkiaGpu : IOpenGlAwareSkiaGpu
{
private GRContext _grContext;
public GlSkiaGpu(IWindowingPlatformGlFeature gl, long? maxResourceBytes)
{
var context = gl.MainContext;
using (context.MakeCurrent())
{
using (var iface = context.Version.Type == GlProfileType.OpenGL ?
GRGlInterface.AssembleGlInterface((_, proc) => context.GlInterface.GetProcAddress(proc)) :
GRGlInterface.AssembleGlesInterface((_, proc) => context.GlInterface.GetProcAddress(proc)))
{
_grContext = GRContext.Create(GRBackend.OpenGL, iface);
if (maxResourceBytes.HasValue)
{
_grContext.GetResourceCacheLimits(out var maxResources, out _);
_grContext.SetResourceCacheLimits(maxResources, maxResourceBytes.Value);
}
}
}
}
public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces)
{
foreach (var surface in surfaces)
{
if (surface is IGlPlatformSurface glSurface)
{
return new GlRenderTarget(_grContext, glSurface);
}
}
return null;
}
public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap() => new OpenGlTextureBitmapImpl();
}
}

81
src/Skia/Avalonia.Skia/Gpu/OpenGlTextureBitmapImpl.cs

@ -0,0 +1,81 @@
using System;
using System.IO;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Imaging;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities;
using SkiaSharp;
namespace Avalonia.Skia
{
class OpenGlTextureBitmapImpl : IOpenGlTextureBitmapImpl, IDrawableBitmapImpl
{
private DisposableLock _lock = new DisposableLock();
private int _textureId;
private int _internalFormat;
public void Dispose()
{
using (Lock())
{
_textureId = 0;
PixelSize = new PixelSize(1, 1);
Version++;
}
}
public Vector Dpi { get; private set; } = new Vector(96, 96);
public PixelSize PixelSize { get; private set; } = new PixelSize(1, 1);
public int Version { get; private set; } = 0;
public void Save(string fileName) => throw new System.NotSupportedException();
public void Save(Stream stream) => throw new System.NotSupportedException();
public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint)
{
// For now silently ignore
if (context.GrContext == null)
return;
using (Lock())
{
if (_textureId == 0)
return;
using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false,
new GRGlTextureInfo(
GlConsts.GL_TEXTURE_2D, (uint)_textureId,
(uint)_internalFormat)))
using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.TopLeft,
SKColorType.Rgba8888))
{
// Again, silently ignore, if something went wrong it's not our fault
if (surface == null)
return;
using (var snapshot = surface.Snapshot())
context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint);
}
}
}
public IDisposable Lock() => _lock.Lock();
public void SetBackBuffer(int textureId, int internalFormat, PixelSize pixelSize, double dpiScaling)
{
using (_lock.Lock())
{
_textureId = textureId;
_internalFormat = internalFormat;
PixelSize = pixelSize;
Dpi = new Vector(96 * dpiScaling, 96 * dpiScaling);
Version++;
}
}
public void SetDirty()
{
using (_lock.Lock())
Version++;
}
}
}

12
src/Skia/Avalonia.Skia/CustomRenderTarget.cs → src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs

@ -4,13 +4,13 @@ using Avalonia.Rendering;
namespace Avalonia.Skia
{
/// <summary>
/// Adapts <see cref="ICustomSkiaRenderTarget"/> to be used within Skia rendering pipeline.
/// Adapts <see cref="ISkiaGpuRenderTarget"/> to be used within our rendering pipeline.
/// </summary>
internal class CustomRenderTarget : IRenderTarget
internal class SkiaGpuRenderTarget : IRenderTargetWithCorruptionInfo
{
private readonly ICustomSkiaRenderTarget _renderTarget;
private readonly ISkiaGpuRenderTarget _renderTarget;
public CustomRenderTarget(ICustomSkiaRenderTarget renderTarget)
public SkiaGpuRenderTarget(ISkiaGpuRenderTarget renderTarget)
{
_renderTarget = renderTarget;
}
@ -22,7 +22,7 @@ namespace Avalonia.Skia
public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer)
{
ICustomSkiaRenderSession session = _renderTarget.BeginRendering();
var session = _renderTarget.BeginRenderingSession();
var nfo = new DrawingContextImpl.CreateInfo
{
@ -35,5 +35,7 @@ namespace Avalonia.Skia
return new DrawingContextImpl(nfo, session);
}
public bool IsCorrupted => _renderTarget.IsCorrupted;
}
}

70
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -1,9 +1,12 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Media;
using Avalonia.OpenGL;
using Avalonia.OpenGL.Imaging;
using Avalonia.Platform;
using SkiaSharp;
@ -12,44 +15,21 @@ namespace Avalonia.Skia
/// <summary>
/// Skia platform render interface.
/// </summary>
internal class PlatformRenderInterface : IPlatformRenderInterface
internal class PlatformRenderInterface : IPlatformRenderInterface, IOpenGlAwarePlatformRenderInterface
{
private readonly ICustomSkiaGpu _customSkiaGpu;
private readonly ISkiaGpu _skiaGpu;
private GRContext GrContext { get; }
public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu, long maxResourceBytes = 100663296)
public PlatformRenderInterface(ISkiaGpu skiaGpu, long? maxResourceBytes = null)
{
if (customSkiaGpu != null)
if (skiaGpu != null)
{
_customSkiaGpu = customSkiaGpu;
GrContext = _customSkiaGpu.GrContext;
GrContext.GetResourceCacheLimits(out var maxResources, out _);
GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
_skiaGpu = skiaGpu;
return;
}
var gl = AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
if (gl != null)
{
var display = gl.ImmediateContext.Display;
gl.ImmediateContext.MakeCurrent();
using (var iface = display.Type == GlDisplayType.OpenGL2
? GRGlInterface.AssembleGlInterface((_, proc) => display.GlInterface.GetProcAddress(proc))
: GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc)))
{
GrContext = GRContext.Create(GRBackend.OpenGL, iface);
GrContext.GetResourceCacheLimits(out var maxResources, out _);
GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
}
display.ClearContext();
}
if (gl != null)
_skiaGpu = new GlSkiaGpu(gl, maxResourceBytes);
}
/// <inheritdoc />
@ -125,26 +105,18 @@ namespace Avalonia.Skia
/// <inheritdoc />
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
if (_customSkiaGpu != null)
if (!(surfaces is IList))
surfaces = surfaces.ToList();
var gpuRenderTarget = _skiaGpu?.TryCreateRenderTarget(surfaces);
if (gpuRenderTarget != null)
{
ICustomSkiaRenderTarget customRenderTarget = _customSkiaGpu.TryCreateRenderTarget(surfaces);
if (customRenderTarget != null)
{
return new CustomRenderTarget(customRenderTarget);
}
return new SkiaGpuRenderTarget(gpuRenderTarget);
}
foreach (var surface in surfaces)
{
if (surface is IGlPlatformSurface glSurface && GrContext != null)
{
return new GlRenderTarget(GrContext, glSurface);
}
if (surface is IFramebufferPlatformSurface framebufferSurface)
{
return new FramebufferRenderTarget(framebufferSurface);
}
}
throw new NotSupportedException(
@ -255,5 +227,17 @@ namespace Avalonia.Skia
return new GlyphRunImpl(textBlob);
}
public IOpenGlTextureBitmapImpl CreateOpenGlTextureBitmap()
{
if (_skiaGpu is IOpenGlAwareSkiaGpu glAware)
return glAware.CreateOpenGlTextureBitmap();
if (_skiaGpu == null)
throw new PlatformNotSupportedException("GPU acceleration is not available");
throw new PlatformNotSupportedException(
"Current GPU acceleration backend does not support OpenGL integration");
}
public bool SupportsIndividualRoundRects => true;
}
}

8
src/Skia/Avalonia.Skia/SkiaOptions.cs

@ -8,18 +8,14 @@ namespace Avalonia
/// </summary>
public class SkiaOptions
{
public SkiaOptions()
{
MaxGpuResourceSizeBytes = 100663296; // Value taken from skia.
}
/// <summary>
/// Custom gpu factory to use. Can be used to customize behavior of Skia renderer.
/// </summary>
public Func<ICustomSkiaGpu> CustomGpuFactory { get; set; }
public Func<ISkiaGpu> CustomGpuFactory { get; set; }
/// <summary>
/// The maximum number of bytes for video memory to store textures and resources.
/// </summary>
public long MaxGpuResourceSizeBytes { get; set; }
public long? MaxGpuResourceSizeBytes { get; set; }
}
}

5
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -11,6 +11,11 @@ namespace Avalonia.Skia
{
return new SKPoint((float)p.X, (float)p.Y);
}
public static SKPoint ToSKPoint(this Vector p)
{
return new SKPoint((float)p.X, (float)p.Y);
}
public static SKRect ToSKRect(this Rect r)
{

2
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -240,5 +240,7 @@ namespace Avalonia.Direct2D1
return new GlyphRunImpl(run);
}
public bool SupportsIndividualRoundRects => false;
}
}

9
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -228,9 +228,14 @@ namespace Avalonia.Direct2D1.Media
}
/// <inheritdoc />
public void DrawRectangle(IBrush brush, IPen pen, Rect rect, double radiusX, double radiusY)
public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rrect, BoxShadows boxShadow = default)
{
var rc = rect.ToDirect2D();
var rc = rrect.Rect.ToDirect2D();
var rect = rrect.Rect;
var radiusX = Math.Max(rrect.RadiiTopLeft.X,
Math.Max(rrect.RadiiTopRight.X, Math.Max(rrect.RadiiBottomRight.X, rrect.RadiiBottomLeft.X)));
var radiusY = Math.Max(rrect.RadiiTopLeft.Y,
Math.Max(rrect.RadiiTopRight.Y, Math.Max(rrect.RadiiBottomRight.Y, rrect.RadiiBottomLeft.Y)));
var isRounded = Math.Abs(radiusX) > double.Epsilon || Math.Abs(radiusY) > double.Epsilon;
if (brush != null)

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

Loading…
Cancel
Save