From dd90d6ac338e20a0e2b50886f009ee4e3d422886 Mon Sep 17 00:00:00 2001 From: davidw Date: Tue, 15 Apr 2025 14:41:18 +0100 Subject: [PATCH 1/8] On Frame-Buffer-Orientation: Add screen orientation support for Linux frame buffer and DRM applications. This commit introduces support for screen orientation across various components by adding a new `Orientation` property of type `SurfaceOrientation` in `DrmOutputOptions` and `FbDevOutputOptions`. The `IScreenInfoProvider` interface is updated to inherit from `ISurfaceOrientation`, ensuring consistent orientation management. Key changes include: - Enhanced `FramebufferToplevelImpl` to handle orientation in size calculations. - Updated `LibInputBackend` for device input coordinate rotation based on screen orientation. - Implemented `ISurfaceOrientation` in `DrmOutput` and `FbdevOutput` classes. - Modified `DrawingContextImpl` to support canvas rotation based on orientation. - Improved `FramebufferRenderTarget` and `PixelFormatConversionShim` to respect framebuffer orientation during surface creation and pixel format conversions. - Streamlined rendering methods to ensure drawing operations align with the current surface orientation. - Removed redundant code related to framebuffer handling. --- .../DrmOutputOptions.cs | 9 +- .../FramebufferToplevelImpl.cs | 26 ++++- .../Input/IScreenInfoProvider.cs | 4 +- .../Input/LibInput/LibInputBackend.cs | 25 ++++- .../LibInput/LibInputNativeUnsafeMethods.cs | 3 + .../LinuxFramebufferPlatform.cs | 5 +- .../Output/DrmOutput.cs | 15 ++- .../Output/FbDevOutputOptions.cs | 7 ++ .../Output/FbdevOutput.cs | 6 ++ .../Output/IOutputBackend.cs | 4 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 14 +-- .../Avalonia.Skia/FramebufferRenderTarget.cs | 95 +++++++++++++++---- .../Gpu/OpenGl/GlRenderTarget.cs | 30 +++++- src/Skia/Avalonia.Skia/ISurfaceOrientation.cs | 15 +++ src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 6 +- 15 files changed, 221 insertions(+), 43 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/ISurfaceOrientation.cs diff --git a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs index f733fe4d725..f545f61ba96 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs @@ -1,4 +1,5 @@ using Avalonia.Media; +using Avalonia.Skia; namespace Avalonia.LinuxFramebuffer { @@ -12,7 +13,13 @@ public class DrmOutputOptions /// Default: 1.0 /// public double Scaling { get; set; } = 1.0; - + + /// + /// The orientation of the screen relative to the frame buffer memory orientation + /// Default: Normal + /// + public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Normal; + /// /// If true an two cycle buffer swapping is processed at init. /// Default: True diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 2a086b353ba..07680500e6b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -7,9 +7,10 @@ using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; using Avalonia.Rendering.Composition; - using Avalonia.Threading; +using Avalonia.Skia; +using Avalonia.Threading; - namespace Avalonia.LinuxFramebuffer +namespace Avalonia.LinuxFramebuffer { class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider { @@ -71,7 +72,14 @@ public void SetCursor(ICursorImpl? cursor) public Action? Closed { get; set; } public Action? LostFocus { get; set; } - public Size ScaledSize => _outputBackend.PixelSize.ToSize(RenderScaling); + public PixelSize RotatedSize => Orientation switch + { + SurfaceOrientation.Rotated90 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), + SurfaceOrientation.Rotated270 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), + _ => _outputBackend.PixelSize, + }; + + public Size ScaledSize => RotatedSize.ToSize(RenderScaling); public void SetTransparencyLevelHint(IReadOnlyList transparencyLevel) { } @@ -80,6 +88,18 @@ public void SetTransparencyLevelHint(IReadOnlyList tran public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); + + // implements ISurfaceOrientation + public SurfaceOrientation Orientation + { + get => _outputBackend.Orientation; + set + { + _outputBackend.Orientation = value; + Resized?.Invoke(ScaledSize, WindowResizeReason.Unspecified); + } + } + public object? TryGetFeature(Type featureType) => null; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs index cb0e51862a0..fd4b26a9423 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs @@ -1,6 +1,8 @@ +using Avalonia.Skia; + namespace Avalonia.LinuxFramebuffer.Input { - public interface IScreenInfoProvider + public interface IScreenInfoProvider : ISurfaceOrientation { Size ScaledSize { get; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index abff7f09367..11b43944b44 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,8 +1,10 @@ using System; using System.IO; +using System.Linq; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Skia; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { @@ -30,12 +32,31 @@ private IInputRoot InputRoot private unsafe void InputThread(IntPtr ctx, LibInputBackendOptions options) { var fd = libinput_get_fd(ctx); + IntPtr[] devices = [.. options.Events!.Select(f => libinput_path_add_device(ctx, f))]; + SurfaceOrientation screenOrientation = SurfaceOrientation.Unknown; - foreach (var f in options.Events!) - libinput_path_add_device(ctx, f); while (true) { IntPtr ev; + + if (_screen!.Orientation != screenOrientation) + { + screenOrientation = _screen.Orientation; + + float[] matrix = screenOrientation switch + { + SurfaceOrientation.Rotated90 => [0, 1, 0, -1, 0, 1], + SurfaceOrientation.Rotated180 => [-1, 0, 1, 0, -1, 1], + SurfaceOrientation.Rotated270 => [0, -1, 1, 1, 0, 0], + _ => [1, 0, 0, 0, 1, 0], // Normal + }; + + foreach (var device in devices) + { + libinput_device_config_calibration_set_matrix(device, matrix); + } + } + libinput_dispatch(ctx); while ((ev = libinput_get_event(ctx)) != IntPtr.Zero) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs index c0274407089..fbb11905f93 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs @@ -55,6 +55,9 @@ public static IntPtr libinput_path_create_context() => [DllImport(LibInput)] public extern static IntPtr libinput_path_remove_device(IntPtr device); + [DllImport(LibInput)] + public extern static int libinput_device_config_calibration_set_matrix(IntPtr device, float[] matrix); + [DllImport(LibInput)] public extern static int libinput_get_fd(IntPtr ctx); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 92ec8b475d4..0d7d74e01ca 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -142,7 +142,10 @@ public TopLevel? TopLevel { get { - EnsureTopLevel(); + if (_topLevel == null) + { + EnsureTopLevel(); + } return _topLevel; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 12ab5ad8212..5b0584323c1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -8,6 +8,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; +using Avalonia.Skia; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; @@ -17,7 +18,7 @@ public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmOutputOptions _outputOptions = new(); private DrmCard _card; - public PixelSize PixelSize => _mode.Resolution; + public PixelSize PixelSize => _mode.Resolution; public double Scaling { @@ -25,6 +26,14 @@ public double Scaling set => _outputOptions.Scaling = value; } + // implements ISurfaceOrientation + public SurfaceOrientation Orientation + { + get => _outputOptions.Orientation; + set => _outputOptions.Orientation = value; + } + + class SharedContextGraphics : IPlatformGraphics { private readonly IPlatformGraphicsContext _context; @@ -275,7 +284,7 @@ public void Dispose() // We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim } - class RenderSession : IGlPlatformSurfaceRenderingSession + class RenderSession : IGlPlatformSurfaceRenderingSession, ISurfaceOrientation { private readonly DrmOutput _parent; private readonly IDisposable _clearContext; @@ -338,6 +347,8 @@ public void Dispose() public double Scaling => _parent.Scaling; public bool IsYFlipped => false; + + public SurfaceOrientation Orientation { get => _parent.Orientation; set => _parent.Orientation = value; } } public IGlPlatformSurfaceRenderingSession BeginDraw() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs index fbd2c9dc14e..8f931881ca1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs @@ -1,4 +1,5 @@ using Avalonia.Platform; +using Avalonia.Skia; namespace Avalonia.LinuxFramebuffer.Output; @@ -28,6 +29,12 @@ public class FbDevOutputOptions /// public double Scaling { get; set; } = 1; + /// + /// The orientation of the screen relative to the frame buffer memory orientation + /// Default: Normal + /// + public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Normal; + /// /// If set to true, FBIO_WAITFORVSYNC ioctl and following memcpy call will run on a dedicated thread /// saving current one from doing nothing in a blocking call diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 08a99d7c078..ad5e812cdb9 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; +using Avalonia.Skia; namespace Avalonia.LinuxFramebuffer { @@ -20,6 +21,9 @@ public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposabl private bool _lockedAtLeastOnce; public double Scaling { get; set; } + // implements ISurfaceOrientation + public SurfaceOrientation Orientation { get; set; } + /// /// Create a Linux frame buffer device output /// @@ -58,6 +62,8 @@ public FbdevOutput(FbDevOutputOptions options) throw new Exception("Error: " + Marshal.GetLastWin32Error()); _options = options; Scaling = options.Scaling; + Orientation = options.Orientation; + try { Init(options.PixelFormat); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs index 17a39b0219c..c17f5f19fc3 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -1,6 +1,8 @@ +using Avalonia.Skia; + namespace Avalonia.LinuxFramebuffer.Output { - public interface IOutputBackend + public interface IOutputBackend : ISurfaceOrientation { PixelSize PixelSize { get; } double Scaling { get; set; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 46c06d2672c..341f7e8a0e7 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -30,6 +30,7 @@ internal partial class DrawingContextImpl : IDrawingContextImpl, private readonly Matrix? _postTransform; private double _currentOpacity = 1.0f; private readonly bool _disableSubpixelTextRendering; + private Matrix _baseTransform; // default canvas rotation private Matrix? _currentTransform; private bool _disposed; private GRContext? _grContext; @@ -187,6 +188,7 @@ public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposab Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); + _baseTransform = Canvas.TotalMatrix.ToAvaloniaMatrix(); _intermediateSurfaceDpi = createInfo.Dpi; _disposables = disposables; _disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering; @@ -374,7 +376,7 @@ public void DrawRectangle(IExperimentalAcrylicMaterial? material, RoundedRect re if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; CheckLease(); - + var rc = rect.Rect.ToSKRect(); SKRoundRect? skRoundRect = null; @@ -542,7 +544,7 @@ public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion if(r.IsEmpty) return; CheckLease(); - + if (brush != null) { using (var fill = CreatePaint(_fillPaint, brush, r.Bounds.ToRectUnscaled())) @@ -567,7 +569,7 @@ public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) if (rect.Height <= 0 || rect.Width <= 0) return; CheckLease(); - + var rc = rect.ToSKRect(); if (brush != null) @@ -677,7 +679,7 @@ private void RestoreCanvas() _currentTransform = null; Canvas.Restore(); } - + /// public void PopClip() { @@ -844,7 +846,7 @@ public void PopOpacityMask() /// public Matrix Transform { - get { return _currentTransform ??= Canvas.TotalMatrix.ToAvaloniaMatrix(); } + get { return _currentTransform ??= _baseTransform.Invert() * Canvas.TotalMatrix.ToAvaloniaMatrix(); } set { CheckLease(); @@ -860,7 +862,7 @@ public Matrix Transform transform *= _postTransform.Value; } - Canvas.SetMatrix(transform.ToSKMatrix()); + Canvas.SetMatrix((_baseTransform * transform).ToSKMatrix()); } } diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 318cdac22c0..0fbd611d32c 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -16,17 +16,20 @@ internal class FramebufferRenderTarget : IRenderTarget2 private IntPtr _currentFramebufferAddress; private SKSurface? _framebufferSurface; private PixelFormatConversionShim? _conversionShim; - private IDisposable? _preFramebufferCopyHandler; + private IFramebufferPlatformSurface _platformSurface; private IFramebufferRenderTarget? _renderTarget; private IFramebufferRenderTargetWithProperties? _renderTargetWithProperties; private bool _hadConversionShim; + protected SurfaceOrientation _orientation => _platformSurface is ISurfaceOrientation o ? o.Orientation : SurfaceOrientation.Normal; + /// /// Create new framebuffer render target using a target surface. /// /// Target surface. public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) { + _platformSurface = platformSurface; _renderTarget = platformSurface.CreateFramebufferRenderTarget(); _renderTargetWithProperties = _renderTarget as IFramebufferRenderTargetWithProperties; } @@ -90,7 +93,7 @@ IDrawingContextImpl CreateDrawingContextCore(bool scaleDrawingToDpi, PreviousFrameIsRetained = !_hadConversionShim && lockProperties.PreviousFrameIsRetained }; - return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); + return new DrawingContextImpl(createInfo, _conversionShim?.SurfaceCopyHandler, canvas, framebuffer); } public bool IsCorrupted => false; @@ -116,7 +119,10 @@ private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImag [MemberNotNull(nameof(_framebufferSurface))] private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) { - if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) + var orientation = _orientation; + + if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) + && _currentFramebufferAddress == framebuffer.Address && _conversionShim?.Orientation == orientation) { return; } @@ -125,14 +131,18 @@ private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer fram _currentFramebufferAddress = framebuffer.Address; - var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, - framebuffer.RowBytes, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); + // Create a surface using the framebuffer address unless we need to rotate the display + SKSurface? surface = null; + if (orientation == SurfaceOrientation.Normal) + { + surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, + framebuffer.RowBytes, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); + } // If surface cannot be created - try to create a compatibility shim first if (surface == null) { - _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address); - _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler; + _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address, orientation); surface = _conversionShim.Surface; } @@ -150,7 +160,6 @@ private void FreeSurface() { _conversionShim?.Dispose(); _conversionShim = null; - _preFramebufferCopyHandler = null; _framebufferSurface?.Dispose(); _framebufferSurface = null; @@ -163,16 +172,23 @@ private void FreeSurface() private class PixelFormatConversionShim : IDisposable { private readonly SKBitmap _bitmap; + private readonly SurfaceOrientation _orientation; private readonly SKImageInfo _destinationInfo; private readonly IntPtr _framebufferAddress; - public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress) + public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress, SurfaceOrientation orientation) { + _orientation = orientation; _destinationInfo = destinationInfo; _framebufferAddress = framebufferAddress; // Create bitmap using default platform settings - _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height); + _bitmap = orientation switch + { + SurfaceOrientation.Rotated90 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), + SurfaceOrientation.Rotated270 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), + _ => new SKBitmap(destinationInfo.Width, destinationInfo.Height), + }; SKColorType bitmapColorType; if (!_bitmap.CanCopyTo(destinationInfo.ColorType)) @@ -194,8 +210,6 @@ public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebuffer throw new Exception( $"Unable to create pixel format shim surface for conversion from {bitmapColorType} to {destinationInfo.ColorType}"); } - - SurfaceCopyHandler = Disposable.Create(CopySurface); } /// @@ -206,7 +220,9 @@ public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebuffer /// /// Handler to start conversion via surface copy. /// - public IDisposable SurfaceCopyHandler { get; } + public IDisposable SurfaceCopyHandler { get => Disposable.Create(CopySurface); } + + public SurfaceOrientation Orientation => _orientation; /// public void Dispose() @@ -214,7 +230,6 @@ public void Dispose() Surface.Dispose(); _bitmap.Dispose(); } - /// /// Convert and copy surface to a framebuffer. /// @@ -222,8 +237,56 @@ private void CopySurface() { using (var snapshot = Surface.Snapshot()) { - snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, - SKImageCachingHint.Disallow); + if (Orientation != SurfaceOrientation.Normal) + { + // rotation or flipping required + int width; + int height; + + if (Orientation == SurfaceOrientation.Rotated180) + { + width = snapshot.Width; + height = snapshot.Height; + } + else + { + width = snapshot.Height; + height = snapshot.Width; + } + + // Create a new surface with swapped width and height + using var rotatedSurface = SKSurface.Create(new SKImageInfo(width, height)); + var rotatedCanvas = rotatedSurface.Canvas; + + // Apply transformation + rotatedCanvas.RotateDegrees(Orientation switch + { + SurfaceOrientation.Rotated90 => 90, + SurfaceOrientation.Rotated180 => 180, + SurfaceOrientation.Rotated270 => -90, + _ => 0 + }); + rotatedCanvas.Translate(Orientation switch + { + SurfaceOrientation.Rotated90 => new SKPoint(0, -width), + SurfaceOrientation.Rotated180 => new SKPoint(-width, -height), + SurfaceOrientation.Rotated270 => new SKPoint(-height, 0), + _ => new SKPoint(0, 0) + }); + + // Draw the original image onto the rotated canvas + rotatedCanvas.DrawImage(snapshot, 0, 0); + + // Return the rotated image + using var rotateSnapshot = rotatedSurface.Snapshot(); + rotateSnapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, + SKImageCachingHint.Disallow); + } + else + { + snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, + SKImageCachingHint.Disallow); + } } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 56ce4a71945..d50f3e29ac6 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,9 +1,6 @@ using System; -using Avalonia.Reactive; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; -using Avalonia.Platform; -using Avalonia.Rendering; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; @@ -64,11 +61,11 @@ public void Dispose() ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) { - var glSession = + IGlPlatformSurfaceRenderingSession glSession = expectedSize != null && _surface is IGlPlatformSurfaceRenderTarget2 surface2 ? surface2.BeginDraw(expectedSize.Value) : _surface.BeginDraw(); - + bool success = false; try { @@ -101,6 +98,29 @@ ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, colorType, _surfaceProperties); + // Apply rotation to the canvas if supported bt the backend and it's not the native hardware orientation + if (glSession is ISurfaceOrientation orientation && orientation.Orientation != SurfaceOrientation.Normal) + { + var canvas = surface.Canvas; + var width = size.Width; + var height = size.Height; + canvas.Translate(width / 2, height / 2); + canvas.RotateDegrees(orientation.Orientation switch + { + SurfaceOrientation.Rotated90 => 90, + SurfaceOrientation.Rotated180 => 180, + SurfaceOrientation.Rotated270 => -90, + _ => 0 + }); + canvas.Translate(orientation.Orientation switch + { + SurfaceOrientation.Rotated180 => new SKPoint(-width / 2, -height / 2), + SurfaceOrientation.Rotated90 => new SKPoint(-height / 2, -width / 2), + SurfaceOrientation.Rotated270 => new SKPoint(-height / 2, -width / 2), + _ => new SKPoint() + }); + } + success = true; return new GlGpuSession(_grContext, renderTarget, surface, glSession); diff --git a/src/Skia/Avalonia.Skia/ISurfaceOrientation.cs b/src/Skia/Avalonia.Skia/ISurfaceOrientation.cs new file mode 100644 index 00000000000..bf57b9a8073 --- /dev/null +++ b/src/Skia/Avalonia.Skia/ISurfaceOrientation.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Skia; + +public enum SurfaceOrientation +{ + Normal, + Rotated90, + Rotated180, + Rotated270, + Unknown, +} + +public interface ISurfaceOrientation +{ + SurfaceOrientation Orientation { get; set; } +} diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 27e36d99b33..33c03698456 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -2,7 +2,6 @@ using System.IO; using Avalonia.Reactive; using Avalonia.Platform; -using Avalonia.Rendering; using Avalonia.Skia.Helpers; using SkiaSharp; @@ -38,7 +37,7 @@ public void Dispose() _surface = null; } } - + /// /// Create new surface render target. /// @@ -154,10 +153,7 @@ public void Blit(IDrawingContextImpl contextImpl) } else { - var oldMatrix = context.Canvas.TotalMatrix; - context.Canvas.ResetMatrix(); _surface.Surface.Draw(context.Canvas, 0, 0, null); - context.Canvas.SetMatrix(oldMatrix); } } From dedb6bdd985bfa34be33d5623aeec5bdc1df36ae Mon Sep 17 00:00:00 2001 From: Thad House Date: Tue, 25 Nov 2025 14:06:50 -0800 Subject: [PATCH 2/8] Fix review comments, add ability to test --- samples/ControlCatalog.Desktop/Program.cs | 28 +++++++++++++-- .../Platform/AndroidScreens.cs | 18 +++++----- .../Platform/ISurfaceOrientation.cs | 6 ++++ .../Platform/SurfaceOrientation.cs | 9 +++++ .../DrmOutputOptions.cs | 5 +-- .../FramebufferToplevelImpl.cs | 11 +++--- .../Input/IScreenInfoProvider.cs | 2 +- .../Input/LibInput/LibInputBackend.cs | 13 +++---- .../Output/DrmOutput.cs | 3 +- .../Output/FbDevOutputOptions.cs | 2 +- .../Output/FbdevOutput.cs | 9 +++-- .../Output/IOutputBackend.cs | 2 +- .../Avalonia.Skia/FramebufferRenderTarget.cs | 36 +++++++++---------- .../Gpu/OpenGl/GlRenderTarget.cs | 23 ++++++------ src/Skia/Avalonia.Skia/ISurfaceOrientation.cs | 15 -------- 15 files changed, 102 insertions(+), 80 deletions(-) create mode 100644 src/Avalonia.Base/Platform/ISurfaceOrientation.cs create mode 100644 src/Avalonia.Base/Platform/SurfaceOrientation.cs delete mode 100644 src/Skia/Avalonia.Skia/ISurfaceOrientation.cs diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 707ead5f1d0..1514e971ddc 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -9,8 +9,10 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Fonts.Inter; using Avalonia.Headless; +using Avalonia.LinuxFramebuffer; using Avalonia.LinuxFramebuffer.Output; using Avalonia.LogicalTree; +using Avalonia.Platform; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Vulkan; @@ -21,7 +23,7 @@ namespace ControlCatalog.Desktop static class Program { private static bool s_useFramebuffer; - + [STAThread] static int Main(string[] args) { @@ -51,12 +53,28 @@ double GetScaling() return scaling; return 1; } + SurfaceOrientation GetOrientation() + { + var idx = Array.IndexOf(args, "--orientation"); + if (idx != 0 && args.Length > idx + 1 && + Enum.TryParse(args[idx + 1], true, out var orientation)) + return orientation; + return SurfaceOrientation.Rotation0; + } + string GetCard() + { + var idx = Array.IndexOf(args, "--card"); + if (idx != 0 && args.Length > idx + 1) + return args[idx + 1]; + return null; + } if (s_useFramebuffer) { SilenceConsole(); return builder.StartLinuxFbDev(args, new FbDevOutputOptions() { - Scaling = GetScaling() + Scaling = GetScaling(), + Orientation = GetOrientation(), }); } else if (args.Contains("--vnc")) @@ -109,7 +127,11 @@ void FormatMem(string metric, long bytes) else if (args.Contains("--drm")) { SilenceConsole(); - return builder.StartLinuxDrm(args, scaling: GetScaling()); + return builder.StartLinuxDrm(args, card: GetCard(), options: new DrmOutputOptions() + { + Scaling = GetScaling(), + Orientation = GetOrientation(), + }); } else if (args.Contains("--dxgi")) { diff --git a/src/Android/Avalonia.Android/Platform/AndroidScreens.cs b/src/Android/Avalonia.Android/Platform/AndroidScreens.cs index 3f1f9bf44e4..5b98bc9b94e 100644 --- a/src/Android/Avalonia.Android/Platform/AndroidScreens.cs +++ b/src/Android/Avalonia.Android/Platform/AndroidScreens.cs @@ -53,7 +53,7 @@ public void Refresh(Context context) var orientation = displayContext.Resources?.Configuration?.Orientation; if (orientation == AndroidOrientation.Square) naturalOrientation = ScreenOrientation.None; - else if (rotation is SurfaceOrientation.Rotation0 or SurfaceOrientation.Rotation180) + else if (rotation is global::Android.Views.SurfaceOrientation.Rotation0 or global::Android.Views.SurfaceOrientation.Rotation180) naturalOrientation = orientation == AndroidOrientation.Landscape ? ScreenOrientation.Landscape : ScreenOrientation.Portrait; @@ -73,14 +73,14 @@ public void Refresh(Context context) CurrentOrientation = (display.Rotation, naturalOrientation) switch { (_, ScreenOrientation.None) => ScreenOrientation.None, - (SurfaceOrientation.Rotation0, ScreenOrientation.Landscape) => ScreenOrientation.Landscape, - (SurfaceOrientation.Rotation90, ScreenOrientation.Landscape) => ScreenOrientation.Portrait, - (SurfaceOrientation.Rotation180, ScreenOrientation.Landscape) => ScreenOrientation.LandscapeFlipped, - (SurfaceOrientation.Rotation270, ScreenOrientation.Landscape) => ScreenOrientation.PortraitFlipped, - (SurfaceOrientation.Rotation0, _) => ScreenOrientation.Portrait, - (SurfaceOrientation.Rotation90, _) => ScreenOrientation.Landscape, - (SurfaceOrientation.Rotation180, _) => ScreenOrientation.PortraitFlipped, - (SurfaceOrientation.Rotation270, _) => ScreenOrientation.LandscapeFlipped, + (global::Android.Views.SurfaceOrientation.Rotation0, ScreenOrientation.Landscape) => ScreenOrientation.Landscape, + (global::Android.Views.SurfaceOrientation.Rotation90, ScreenOrientation.Landscape) => ScreenOrientation.Portrait, + (global::Android.Views.SurfaceOrientation.Rotation180, ScreenOrientation.Landscape) => ScreenOrientation.LandscapeFlipped, + (global::Android.Views.SurfaceOrientation.Rotation270, ScreenOrientation.Landscape) => ScreenOrientation.PortraitFlipped, + (global::Android.Views.SurfaceOrientation.Rotation0, _) => ScreenOrientation.Portrait, + (global::Android.Views.SurfaceOrientation.Rotation90, _) => ScreenOrientation.Landscape, + (global::Android.Views.SurfaceOrientation.Rotation180, _) => ScreenOrientation.PortraitFlipped, + (global::Android.Views.SurfaceOrientation.Rotation270, _) => ScreenOrientation.LandscapeFlipped, _ => ScreenOrientation.Portrait }; } diff --git a/src/Avalonia.Base/Platform/ISurfaceOrientation.cs b/src/Avalonia.Base/Platform/ISurfaceOrientation.cs new file mode 100644 index 00000000000..499b3c01aae --- /dev/null +++ b/src/Avalonia.Base/Platform/ISurfaceOrientation.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Platform; + +internal interface ISurfaceOrientation +{ + SurfaceOrientation Orientation { get; set; } +} diff --git a/src/Avalonia.Base/Platform/SurfaceOrientation.cs b/src/Avalonia.Base/Platform/SurfaceOrientation.cs new file mode 100644 index 00000000000..71835fb6ff6 --- /dev/null +++ b/src/Avalonia.Base/Platform/SurfaceOrientation.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Platform; + +public enum SurfaceOrientation +{ + Rotation0, + Rotation90, + Rotation180, + Rotation270, +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs index f545f61ba96..e61d2f73471 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs @@ -1,4 +1,5 @@ using Avalonia.Media; +using Avalonia.Platform; using Avalonia.Skia; namespace Avalonia.LinuxFramebuffer @@ -18,14 +19,14 @@ public class DrmOutputOptions /// The orientation of the screen relative to the frame buffer memory orientation /// Default: Normal /// - public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Normal; + public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Rotation0; /// /// If true an two cycle buffer swapping is processed at init. /// Default: True /// public bool EnableInitialBufferSwapping { get; set; } = true; - + /// /// Color for /// Default: R0 G0 B0 A0 diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 07680500e6b..33f435968ab 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -12,7 +12,7 @@ namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider + class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider, ISurfaceOrientation { private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; @@ -74,8 +74,8 @@ public void SetCursor(ICursorImpl? cursor) public PixelSize RotatedSize => Orientation switch { - SurfaceOrientation.Rotated90 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), - SurfaceOrientation.Rotated270 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), + SurfaceOrientation.Rotation90 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), + SurfaceOrientation.Rotation270 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), _ => _outputBackend.PixelSize, }; @@ -89,13 +89,12 @@ public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); - // implements ISurfaceOrientation public SurfaceOrientation Orientation { - get => _outputBackend.Orientation; + get; set { - _outputBackend.Orientation = value; + field = value; Resized?.Invoke(ScaledSize, WindowResizeReason.Unspecified); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs index fd4b26a9423..677a2a9da9c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs @@ -2,7 +2,7 @@ namespace Avalonia.LinuxFramebuffer.Input { - public interface IScreenInfoProvider : ISurfaceOrientation + public interface IScreenInfoProvider { Size ScaledSize { get; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 11b43944b44..c69d7d1c8f4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -4,6 +4,7 @@ using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Platform; using Avalonia.Skia; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput @@ -33,21 +34,21 @@ private unsafe void InputThread(IntPtr ctx, LibInputBackendOptions options) { var fd = libinput_get_fd(ctx); IntPtr[] devices = [.. options.Events!.Select(f => libinput_path_add_device(ctx, f))]; - SurfaceOrientation screenOrientation = SurfaceOrientation.Unknown; + SurfaceOrientation screenOrientation = SurfaceOrientation.Rotation0; while (true) { IntPtr ev; - if (_screen!.Orientation != screenOrientation) + if (_screen is ISurfaceOrientation surfaceOrientationProvider && surfaceOrientationProvider.Orientation != screenOrientation) { - screenOrientation = _screen.Orientation; + screenOrientation = surfaceOrientationProvider.Orientation; float[] matrix = screenOrientation switch { - SurfaceOrientation.Rotated90 => [0, 1, 0, -1, 0, 1], - SurfaceOrientation.Rotated180 => [-1, 0, 1, 0, -1, 1], - SurfaceOrientation.Rotated270 => [0, -1, 1, 1, 0, 0], + SurfaceOrientation.Rotation90 => [0, 1, 0, -1, 0, 1], + SurfaceOrientation.Rotation180 => [-1, 0, 1, 0, -1, 1], + SurfaceOrientation.Rotation270 => [0, -1, 1, 1, 0, 0], _ => [1, 0, 0, 0, 1, 0], // Normal }; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 5b0584323c1..1ec6cc02d6a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -14,7 +14,7 @@ namespace Avalonia.LinuxFramebuffer.Output { - public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface + public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface, ISurfaceOrientation { private DrmOutputOptions _outputOptions = new(); private DrmCard _card; @@ -26,7 +26,6 @@ public double Scaling set => _outputOptions.Scaling = value; } - // implements ISurfaceOrientation public SurfaceOrientation Orientation { get => _outputOptions.Orientation; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs index 8f931881ca1..4879b6d06b5 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs @@ -33,7 +33,7 @@ public class FbDevOutputOptions /// The orientation of the screen relative to the frame buffer memory orientation /// Default: Normal /// - public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Normal; + public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Rotation0; /// /// If set to true, FBIO_WAITFORVSYNC ioctl and following memcpy call will run on a dedicated thread diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index ad5e812cdb9..fbc51cb412b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -9,7 +9,7 @@ namespace Avalonia.LinuxFramebuffer { - public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend + public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend, ISurfaceOrientation { private int _fd; private fb_fix_screeninfo _fixedInfo; @@ -21,7 +21,6 @@ public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposabl private bool _lockedAtLeastOnce; public double Scaling { get; set; } - // implements ISurfaceOrientation public SurfaceOrientation Orientation { get; set; } /// @@ -47,7 +46,7 @@ public FbdevOutput(string? fileName, PixelFormat? format) : this(new FbDevOutput PixelFormat = format }) { - + } /// @@ -176,7 +175,7 @@ private ILockedFramebuffer Lock(out FramebufferLockProperties properties) throw new ObjectDisposedException("LinuxFramebuffer"); var dpi = new Vector(96, 96) * Scaling; - + if (_options.RenderDirectlyToMappedMemory) { properties = new FramebufferLockProperties(_lockedAtLeastOnce); @@ -192,7 +191,7 @@ private ILockedFramebuffer Lock(out FramebufferLockProperties properties) new FbDevBackBuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _options.UseAsyncFrontBufferBlit == true)) .Lock(new Vector(96, 96) * Scaling); } - + public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncRetainedFramebufferRenderTarget(Lock); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs index c17f5f19fc3..3a1c03c475f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -2,7 +2,7 @@ namespace Avalonia.LinuxFramebuffer.Output { - public interface IOutputBackend : ISurfaceOrientation + public interface IOutputBackend { PixelSize PixelSize { get; } double Scaling { get; set; } diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 0fbd611d32c..01e58e2cb72 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -21,7 +21,7 @@ internal class FramebufferRenderTarget : IRenderTarget2 private IFramebufferRenderTargetWithProperties? _renderTargetWithProperties; private bool _hadConversionShim; - protected SurfaceOrientation _orientation => _platformSurface is ISurfaceOrientation o ? o.Orientation : SurfaceOrientation.Normal; + private SurfaceOrientation Orientation => _platformSurface is ISurfaceOrientation o ? o.Orientation : SurfaceOrientation.Rotation0; /// /// Create new framebuffer render target using a target surface. @@ -59,7 +59,7 @@ public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi) => public IDrawingContextImpl CreateDrawingContext(PixelSize expectedPixelSize, out RenderTargetDrawingContextProperties properties) => CreateDrawingContextCore(false, out properties); - + IDrawingContextImpl CreateDrawingContextCore(bool scaleDrawingToDpi, out RenderTargetDrawingContextProperties properties) { @@ -92,7 +92,7 @@ IDrawingContextImpl CreateDrawingContextCore(bool scaleDrawingToDpi, { PreviousFrameIsRetained = !_hadConversionShim && lockProperties.PreviousFrameIsRetained }; - + return new DrawingContextImpl(createInfo, _conversionShim?.SurfaceCopyHandler, canvas, framebuffer); } @@ -119,21 +119,21 @@ private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImag [MemberNotNull(nameof(_framebufferSurface))] private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) { - var orientation = _orientation; + var orientation = Orientation; - if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) + if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address && _conversionShim?.Orientation == orientation) { return; } - + FreeSurface(); - + _currentFramebufferAddress = framebuffer.Address; // Create a surface using the framebuffer address unless we need to rotate the display SKSurface? surface = null; - if (orientation == SurfaceOrientation.Normal) + if (orientation == SurfaceOrientation.Rotation0) { surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, framebuffer.RowBytes, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); @@ -185,8 +185,8 @@ public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebuffer // Create bitmap using default platform settings _bitmap = orientation switch { - SurfaceOrientation.Rotated90 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), - SurfaceOrientation.Rotated270 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), + SurfaceOrientation.Rotation90 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), + SurfaceOrientation.Rotation270 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), _ => new SKBitmap(destinationInfo.Width, destinationInfo.Height), }; SKColorType bitmapColorType; @@ -237,13 +237,13 @@ private void CopySurface() { using (var snapshot = Surface.Snapshot()) { - if (Orientation != SurfaceOrientation.Normal) + if (Orientation != SurfaceOrientation.Rotation0) { // rotation or flipping required int width; int height; - if (Orientation == SurfaceOrientation.Rotated180) + if (Orientation == SurfaceOrientation.Rotation180) { width = snapshot.Width; height = snapshot.Height; @@ -261,16 +261,16 @@ private void CopySurface() // Apply transformation rotatedCanvas.RotateDegrees(Orientation switch { - SurfaceOrientation.Rotated90 => 90, - SurfaceOrientation.Rotated180 => 180, - SurfaceOrientation.Rotated270 => -90, + SurfaceOrientation.Rotation90 => 90, + SurfaceOrientation.Rotation180 => 180, + SurfaceOrientation.Rotation270 => -90, _ => 0 }); rotatedCanvas.Translate(Orientation switch { - SurfaceOrientation.Rotated90 => new SKPoint(0, -width), - SurfaceOrientation.Rotated180 => new SKPoint(-width, -height), - SurfaceOrientation.Rotated270 => new SKPoint(-height, 0), + SurfaceOrientation.Rotation90 => new SKPoint(0, -width), + SurfaceOrientation.Rotation180 => new SKPoint(-width, -height), + SurfaceOrientation.Rotation270 => new SKPoint(-height, 0), _ => new SKPoint(0, 0) }); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index d50f3e29ac6..c3146a1e4a8 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,6 +1,7 @@ using System; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; +using Avalonia.Platform; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; @@ -37,7 +38,7 @@ public GlGpuSession(GRContext grContext, _backendRenderTarget = backendRenderTarget; _surface = surface; _glSession = glSession; - + SurfaceOrigin = glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft; } public void Dispose() @@ -48,7 +49,7 @@ public void Dispose() GrContext.Flush(); _glSession.Dispose(); } - + public GRSurfaceOrigin SurfaceOrigin { get; } public GRContext GrContext { get; } @@ -58,7 +59,7 @@ public void Dispose() public ISkiaGpuRenderSession BeginRenderingSession(PixelSize size) => BeginRenderingSessionCore(size); public ISkiaGpuRenderSession BeginRenderingSession() => BeginRenderingSessionCore(null); - + ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) { IGlPlatformSurfaceRenderingSession glSession = @@ -99,24 +100,24 @@ ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) colorType, _surfaceProperties); // Apply rotation to the canvas if supported bt the backend and it's not the native hardware orientation - if (glSession is ISurfaceOrientation orientation && orientation.Orientation != SurfaceOrientation.Normal) - { + if (glSession is ISurfaceOrientation orientation && orientation.Orientation != SurfaceOrientation.Rotation0) + { var canvas = surface.Canvas; var width = size.Width; var height = size.Height; canvas.Translate(width / 2, height / 2); canvas.RotateDegrees(orientation.Orientation switch { - SurfaceOrientation.Rotated90 => 90, - SurfaceOrientation.Rotated180 => 180, - SurfaceOrientation.Rotated270 => -90, + SurfaceOrientation.Rotation90 => 90, + SurfaceOrientation.Rotation180 => 180, + SurfaceOrientation.Rotation270 => -90, _ => 0 }); canvas.Translate(orientation.Orientation switch { - SurfaceOrientation.Rotated180 => new SKPoint(-width / 2, -height / 2), - SurfaceOrientation.Rotated90 => new SKPoint(-height / 2, -width / 2), - SurfaceOrientation.Rotated270 => new SKPoint(-height / 2, -width / 2), + SurfaceOrientation.Rotation180 => new SKPoint(-width / 2, -height / 2), + SurfaceOrientation.Rotation90 => new SKPoint(-height / 2, -width / 2), + SurfaceOrientation.Rotation270 => new SKPoint(-height / 2, -width / 2), _ => new SKPoint() }); } diff --git a/src/Skia/Avalonia.Skia/ISurfaceOrientation.cs b/src/Skia/Avalonia.Skia/ISurfaceOrientation.cs deleted file mode 100644 index bf57b9a8073..00000000000 --- a/src/Skia/Avalonia.Skia/ISurfaceOrientation.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Avalonia.Skia; - -public enum SurfaceOrientation -{ - Normal, - Rotated90, - Rotated180, - Rotated270, - Unknown, -} - -public interface ISurfaceOrientation -{ - SurfaceOrientation Orientation { get; set; } -} From f2af5690dba3b482b9bacbfa998214211d18bd30 Mon Sep 17 00:00:00 2001 From: Thad House Date: Tue, 25 Nov 2025 16:38:26 -0800 Subject: [PATCH 3/8] Some formatting --- samples/ControlCatalog.Desktop/Program.cs | 11 ++- .../DrmOutputOptions.cs | 3 +- .../FramebufferToplevelImpl.cs | 5 +- .../Input/IScreenInfoProvider.cs | 2 - .../Input/LibInput/LibInputBackend.cs | 1 - .../Output/DrmOutput.cs | 4 +- .../Output/FbDevOutputOptions.cs | 1 - .../Output/FbdevOutput.cs | 7 +- .../Output/IOutputBackend.cs | 2 - src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 80 +++++++++---------- .../Avalonia.Skia/FramebufferRenderTarget.cs | 8 +- .../Gpu/OpenGl/GlRenderTarget.cs | 10 +-- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 2 +- 13 files changed, 65 insertions(+), 71 deletions(-) diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 1514e971ddc..2f347ec7cc8 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -23,7 +23,8 @@ namespace ControlCatalog.Desktop static class Program { private static bool s_useFramebuffer; - + private static bool s_useDrm; + [STAThread] static int Main(string[] args) { @@ -31,6 +32,10 @@ static int Main(string[] args) { s_useFramebuffer = true; } + if (args.Contains("--drm")) + { + s_useDrm = true; + } if (args.Contains("--wait-for-attach")) { @@ -124,7 +129,7 @@ void FormatMem(string metric, long bytes) }) .StartWithClassicDesktopLifetime(args); } - else if (args.Contains("--drm")) + else if (s_useDrm) { SilenceConsole(); return builder.StartLinuxDrm(args, card: GetCard(), options: new DrmOutputOptions() @@ -173,7 +178,7 @@ public static AppBuilder BuildAvaloniaApp() .WithInterFont() .AfterSetup(builder => { - if (!s_useFramebuffer) + if (!s_useFramebuffer && !s_useDrm) { builder.Instance!.AttachDevTools(new Avalonia.Diagnostics.DevToolsOptions() { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs index e61d2f73471..42a8ed4f0de 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs @@ -1,6 +1,5 @@ using Avalonia.Media; using Avalonia.Platform; -using Avalonia.Skia; namespace Avalonia.LinuxFramebuffer { @@ -26,7 +25,7 @@ public class DrmOutputOptions /// Default: True /// public bool EnableInitialBufferSwapping { get; set; } = true; - + /// /// Color for /// Default: R0 G0 B0 A0 diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 33f435968ab..2d6e09516e9 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -7,10 +7,9 @@ using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; using Avalonia.Rendering.Composition; -using Avalonia.Skia; -using Avalonia.Threading; + using Avalonia.Threading; -namespace Avalonia.LinuxFramebuffer + namespace Avalonia.LinuxFramebuffer { class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider, ISurfaceOrientation { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs index 677a2a9da9c..cb0e51862a0 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs @@ -1,5 +1,3 @@ -using Avalonia.Skia; - namespace Avalonia.LinuxFramebuffer.Input { public interface IScreenInfoProvider diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index c69d7d1c8f4..9bb62bd3c4c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -5,7 +5,6 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Platform; -using Avalonia.Skia; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 1ec6cc02d6a..a7e0c51c363 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -8,7 +8,6 @@ using Avalonia.OpenGL.Egl; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; -using Avalonia.Skia; using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; using static Avalonia.LinuxFramebuffer.Output.LibDrm; @@ -18,7 +17,7 @@ public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface, ISurfaceOr { private DrmOutputOptions _outputOptions = new(); private DrmCard _card; - public PixelSize PixelSize => _mode.Resolution; + public PixelSize PixelSize => _mode.Resolution; public double Scaling { @@ -32,7 +31,6 @@ public SurfaceOrientation Orientation set => _outputOptions.Orientation = value; } - class SharedContextGraphics : IPlatformGraphics { private readonly IPlatformGraphicsContext _context; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs index 4879b6d06b5..6e7b04bcb31 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs @@ -1,5 +1,4 @@ using Avalonia.Platform; -using Avalonia.Skia; namespace Avalonia.LinuxFramebuffer.Output; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index fbc51cb412b..721d248e318 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -5,7 +5,6 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; -using Avalonia.Skia; namespace Avalonia.LinuxFramebuffer { @@ -46,7 +45,7 @@ public FbdevOutput(string? fileName, PixelFormat? format) : this(new FbDevOutput PixelFormat = format }) { - + } /// @@ -175,7 +174,7 @@ private ILockedFramebuffer Lock(out FramebufferLockProperties properties) throw new ObjectDisposedException("LinuxFramebuffer"); var dpi = new Vector(96, 96) * Scaling; - + if (_options.RenderDirectlyToMappedMemory) { properties = new FramebufferLockProperties(_lockedAtLeastOnce); @@ -191,7 +190,7 @@ private ILockedFramebuffer Lock(out FramebufferLockProperties properties) new FbDevBackBuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _options.UseAsyncFrontBufferBlit == true)) .Lock(new Vector(96, 96) * Scaling); } - + public IFramebufferRenderTarget CreateFramebufferRenderTarget() => new FuncRetainedFramebufferRenderTarget(Lock); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs index 3a1c03c475f..17a39b0219c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -1,5 +1,3 @@ -using Avalonia.Skia; - namespace Avalonia.LinuxFramebuffer.Output { public interface IOutputBackend diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2600db4facb..99e876f4c71 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -63,7 +63,7 @@ public struct CreateInfo /// Makes DPI to be applied as a hidden matrix transform /// public bool ScaleDrawingToDpi; - + /// /// Dpi for intermediate surfaces /// @@ -157,7 +157,7 @@ public PlatformApiLease(ApiLease parent, IPlatformGraphicsContext context) Context = context; _parent._leased = true; } - + public void Dispose() { _parent._leased = false; @@ -166,7 +166,7 @@ public void Dispose() public IPlatformGraphicsContext Context { get; } } - + public ISkiaSharpPlatformGraphicsApiLease? TryLeasePlatformGraphicsApi() { CheckLease(); @@ -177,7 +177,7 @@ public void Dispose() } } } - + /// /// Create new drawing context. /// @@ -188,7 +188,7 @@ public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposab Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); - _baseTransform = Canvas.TotalMatrix.ToAvaloniaMatrix(); + _baseTransform = Canvas.TotalMatrix44.ToAvaloniaMatrix(); _intermediateSurfaceDpi = createInfo.Dpi; _disposables = disposables; _disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering; @@ -200,7 +200,7 @@ public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposab _session = createInfo.CurrentSession; - + if (createInfo.ScaleDrawingToDpi && !createInfo.Dpi.NearlyEquals(SkiaPlatform.DefaultDpi)) { _postTransform = @@ -217,7 +217,7 @@ public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposab _useOpacitySaveLayer = options.UseOpacitySaveLayer; } } - + /// /// Skia canvas. /// @@ -231,7 +231,7 @@ private void CheckLease() if (_leased) throw new InvalidOperationException("The underlying graphics API is currently leased"); } - + /// public void Clear(Color color) { @@ -314,7 +314,7 @@ private static float SkBlurRadiusToSigma(double radius) { return 0.0f; return 0.288675f * (float)radius + 0.5f; } - + private struct BoxShadowFilter : IDisposable { public readonly SKPaint Paint; @@ -378,7 +378,7 @@ public void DrawRectangle(IExperimentalAcrylicMaterial? material, RoundedRect re if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) return; CheckLease(); - + var rc = rect.Rect.ToSKRect(); SKRoundRect? skRoundRect = null; @@ -451,7 +451,7 @@ public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows 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); @@ -507,7 +507,7 @@ public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows 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)) @@ -546,7 +546,7 @@ public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion if(r.IsEmpty) return; CheckLease(); - + if (brush != null) { using (var fill = CreatePaint(_fillPaint, brush, r.Bounds.ToRectUnscaled())) @@ -571,7 +571,7 @@ public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) if (rect.Height <= 0 || rect.Width <= 0) return; CheckLease(); - + var rc = rect.ToSKRect(); if (brush != null) @@ -591,7 +591,7 @@ public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) } } } - + /// public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun) { @@ -681,7 +681,7 @@ private void RestoreCanvas() _currentTransform = null; Canvas.Restore(); } - + /// public void PopClip() { @@ -827,10 +827,10 @@ public void PopOpacityMask() var paint = SKPaintCache.Shared.Get(); paint.BlendMode = SKBlendMode.DstIn; - + Canvas.SaveLayer(paint); SKPaintCache.Shared.ReturnReset(paint); - + var (transform, paintWrapper) = _maskStack.Pop(); Canvas.SetMatrix(transform); using (paintWrapper) @@ -1007,7 +1007,7 @@ private static void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect t reversedStops[reversedIndex] = (float)offset; reversedColors[reversedIndex] = stopColors[i]; } - + stopColors = reversedColors; stopOffsets = reversedStops; } @@ -1082,9 +1082,9 @@ private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Rect targetBox, I context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.PushRenderOptions(RenderOptions); - + context.Transform = calc.IntermediateTransform; - + context.DrawBitmap( tileBrushImage, 1, @@ -1152,7 +1152,7 @@ private void ConfigureSceneBrushContent(ref PaintWrapper paintWrapper, ISceneBru else ConfigureSceneBrushContentWithSurface(ref paintWrapper, content, targetRect); } - + private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper, ISceneBrushContent content, Rect targetRect) { @@ -1175,15 +1175,15 @@ private void ConfigureSceneBrushContentWithSurface(ref PaintWrapper paintWrapper ConfigureTileBrush(ref paintWrapper, targetRect, content.Brush, intermediate); } } - + private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper, ISceneBrushContent content, Rect targetRect) { // To understand what happens here, read // https://learn.microsoft.com/en-us/dotnet/api/system.windows.media.tilebrush // and the rest of the docs - - // Avalonia follows WPF and WPF's brushes completely ignore whatever layout bounds visuals have, + + // Avalonia follows WPF and WPF's brushes completely ignore whatever layout bounds visuals have, // and instead are using content bounds, e. g. // ╔════════════════════════════════════╗ <--- target control // ║ ║ layout bounds @@ -1197,10 +1197,10 @@ private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper // ╚════════════════════════════════════╝ // // Source Rect (aka ViewBox) is relative to the content bounds, not to the visual/drawing - + var contentRect = content.Rect; var sourceRect = content.Brush.SourceRect.ToPixels(contentRect); - + // Early escape if (contentRect.Size.Width <= 0 || contentRect.Size.Height <= 0 || sourceRect.Size.Width <= 0 || sourceRect.Size.Height <= 0) @@ -1208,17 +1208,17 @@ private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper paintWrapper.Paint.Color = SKColor.Empty; return; } - + // We are moving the render area to make the top-left corner of the SourceRect (ViewBox) to be at (0,0) // of the tile var contentRenderTransform = Matrix.CreateTranslation(-sourceRect.X, -sourceRect.Y); - + // DestinationRect (aka Viewport) is specified relative to the target rect var destinationRect = content.Brush.DestinationRect.ToPixels(targetRect); - + // Tile size matches the destination rect size var tileSize = destinationRect.Size; - + // Apply transforms to stretch content to match the tile if (sourceRect.Size != tileSize) { @@ -1233,7 +1233,7 @@ private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper contentRenderTransform = contentRenderTransform * Matrix.CreateScale(scale) * Matrix.CreateTranslation(alignmentTranslate); } - + // Pre-rasterize the tile into SKPicture using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi); using (var ctx = pictureTarget.CreateDrawingContext(tileSize, false)) @@ -1243,14 +1243,14 @@ private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper ctx.PopRenderOptions(); } using var tile = pictureTarget.GetPicture(); - + // If there is no BrushTransform and destinationRect is at (0,0) we don't need any transforms Matrix shaderTransform = Matrix.Identity; - + // Apply Brush.Transform to SKShader if (content.Transform != null) { - + var transformOrigin = content.TransformOrigin.ToPixels(targetRect); var offset = Matrix.CreateTranslation(transformOrigin); shaderTransform = (-offset) * content.Transform.Value * (offset); @@ -1259,10 +1259,10 @@ private void ConfigureSceneBrushContentWithPicture(ref PaintWrapper paintWrapper // Apply destinationRect position if (destinationRect.Position != default) shaderTransform *= Matrix.CreateTranslation(destinationRect.X, destinationRect.Y); - + // Create shader var (tileX, tileY) = GetTileModes(content.Brush.TileMode); - using(var shader = tile.ToShader(tileX, tileY, shaderTransform.ToSKMatrix(), + using(var shader = tile.ToShader(tileX, tileY, shaderTransform.ToSKMatrix(), new SKRect(0, 0, tile.CullRect.Width, tile.CullRect.Height))) { paintWrapper.Paint.Shader = shader; @@ -1483,7 +1483,7 @@ private SurfaceRenderTarget CreateRenderTarget(PixelSize pixelSize, bool isLayer }; return new SurfaceRenderTarget(createInfo); - } + } /// /// Skia cached paint state. @@ -1493,7 +1493,7 @@ private SurfaceRenderTarget CreateRenderTarget(PixelSize pixelSize, bool isLayer private readonly SKColor _color; private readonly SKShader _shader; private readonly SKPaint _paint; - + public PaintState(SKPaint paint, SKColor color, SKShader shader) { _paint = paint; @@ -1557,7 +1557,7 @@ public void AddDisposable(IDisposable disposable) "PaintWrapper disposable object limit reached. You need to add extra struct fields to support more disposables."); } } - + /// public void Dispose() { diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index 01e58e2cb72..ca16a1a1172 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -59,7 +59,7 @@ public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi) => public IDrawingContextImpl CreateDrawingContext(PixelSize expectedPixelSize, out RenderTargetDrawingContextProperties properties) => CreateDrawingContextCore(false, out properties); - + IDrawingContextImpl CreateDrawingContextCore(bool scaleDrawingToDpi, out RenderTargetDrawingContextProperties properties) { @@ -92,7 +92,7 @@ IDrawingContextImpl CreateDrawingContextCore(bool scaleDrawingToDpi, { PreviousFrameIsRetained = !_hadConversionShim && lockProperties.PreviousFrameIsRetained }; - + return new DrawingContextImpl(createInfo, _conversionShim?.SurfaceCopyHandler, canvas, framebuffer); } @@ -126,9 +126,9 @@ private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer fram { return; } - + FreeSurface(); - + _currentFramebufferAddress = framebuffer.Address; // Create a surface using the framebuffer address unless we need to rotate the display diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index c3146a1e4a8..144da3d94d4 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -38,7 +38,7 @@ public GlGpuSession(GRContext grContext, _backendRenderTarget = backendRenderTarget; _surface = surface; _glSession = glSession; - + SurfaceOrigin = glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft; } public void Dispose() @@ -49,7 +49,7 @@ public void Dispose() GrContext.Flush(); _glSession.Dispose(); } - + public GRSurfaceOrigin SurfaceOrigin { get; } public GRContext GrContext { get; } @@ -59,14 +59,14 @@ public void Dispose() public ISkiaGpuRenderSession BeginRenderingSession(PixelSize size) => BeginRenderingSessionCore(size); public ISkiaGpuRenderSession BeginRenderingSession() => BeginRenderingSessionCore(null); - + ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) { - IGlPlatformSurfaceRenderingSession glSession = + var glSession = expectedSize != null && _surface is IGlPlatformSurfaceRenderTarget2 surface2 ? surface2.BeginDraw(expectedSize.Value) : _surface.BeginDraw(); - + bool success = false; try { diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 1cb968efb52..41380fdbd13 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -37,7 +37,7 @@ public void Dispose() _surface = null; } } - + /// /// Create new surface render target. /// From b56bfb567e432b10bd9fdf71570ca93223e65c54 Mon Sep 17 00:00:00 2001 From: Thad House Date: Tue, 25 Nov 2025 21:22:21 -0800 Subject: [PATCH 4/8] Should be working now --- .../Platform/ISurfaceOrientation.cs | 2 +- .../FramebufferToplevelImpl.cs | 19 ++++++++----------- .../Input/LibInput/LibInputBackend.cs | 2 +- .../Gpu/OpenGl/GlRenderTarget.cs | 7 +++---- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Base/Platform/ISurfaceOrientation.cs b/src/Avalonia.Base/Platform/ISurfaceOrientation.cs index 499b3c01aae..2f4f2d3241d 100644 --- a/src/Avalonia.Base/Platform/ISurfaceOrientation.cs +++ b/src/Avalonia.Base/Platform/ISurfaceOrientation.cs @@ -2,5 +2,5 @@ internal interface ISurfaceOrientation { - SurfaceOrientation Orientation { get; set; } + SurfaceOrientation Orientation { get; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 2d6e09516e9..27ee1ddc1bf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -16,11 +16,16 @@ class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider, ISurfaceOrie private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; private readonly RawEventGrouper _inputQueue; + private readonly SurfaceOrientation _orientation = SurfaceOrientation.Rotation0; public IInputRoot? InputRoot { get; private set; } public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { + if (outputBackend is ISurfaceOrientation surfaceOrientation) + { + _orientation = surfaceOrientation.Orientation; + } _outputBackend = outputBackend; _inputBackend = inputBackend; _inputQueue = new RawEventGrouper(groupedInput => Input?.Invoke(groupedInput), @@ -71,7 +76,7 @@ public void SetCursor(ICursorImpl? cursor) public Action? Closed { get; set; } public Action? LostFocus { get; set; } - public PixelSize RotatedSize => Orientation switch + public PixelSize RotatedSize => _orientation switch { SurfaceOrientation.Rotation90 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), SurfaceOrientation.Rotation270 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), @@ -88,16 +93,8 @@ public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); - public SurfaceOrientation Orientation - { - get; - set - { - field = value; - Resized?.Invoke(ScaledSize, WindowResizeReason.Unspecified); - } - } - public object? TryGetFeature(Type featureType) => null; + + SurfaceOrientation ISurfaceOrientation.Orientation => _orientation; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 9bb62bd3c4c..ee43af0a64d 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -32,7 +32,7 @@ private IInputRoot InputRoot private unsafe void InputThread(IntPtr ctx, LibInputBackendOptions options) { var fd = libinput_get_fd(ctx); - IntPtr[] devices = [.. options.Events!.Select(f => libinput_path_add_device(ctx, f))]; + IntPtr[] devices = [.. options.Events!.Select(f => libinput_path_add_device(ctx, f)).Where(d => d != IntPtr.Zero)]; SurfaceOrientation screenOrientation = SurfaceOrientation.Rotation0; while (true) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 144da3d94d4..17e2708381f 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -105,7 +105,6 @@ ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) var canvas = surface.Canvas; var width = size.Width; var height = size.Height; - canvas.Translate(width / 2, height / 2); canvas.RotateDegrees(orientation.Orientation switch { SurfaceOrientation.Rotation90 => 90, @@ -115,9 +114,9 @@ ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) }); canvas.Translate(orientation.Orientation switch { - SurfaceOrientation.Rotation180 => new SKPoint(-width / 2, -height / 2), - SurfaceOrientation.Rotation90 => new SKPoint(-height / 2, -width / 2), - SurfaceOrientation.Rotation270 => new SKPoint(-height / 2, -width / 2), + SurfaceOrientation.Rotation90 => new SKPoint(0, -width), + SurfaceOrientation.Rotation180 => new SKPoint(-width, -height), + SurfaceOrientation.Rotation270 => new SKPoint(-height, 0), _ => new SKPoint() }); } From ce955140b2c359bc0a199e08886a0f0a51bfe13c Mon Sep 17 00:00:00 2001 From: Thad House Date: Wed, 26 Nov 2025 10:48:35 -0800 Subject: [PATCH 5/8] Remove fbdev changes Theres really no point to do this in fbdev. --- samples/ControlCatalog.Desktop/Program.cs | 3 +- .../Output/FbDevOutputOptions.cs | 6 -- .../Output/FbdevOutput.cs | 6 +- .../Avalonia.Skia/FramebufferRenderTarget.cs | 95 ++++--------------- 4 files changed, 18 insertions(+), 92 deletions(-) diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 2f347ec7cc8..0604cc46406 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -78,8 +78,7 @@ string GetCard() SilenceConsole(); return builder.StartLinuxFbDev(args, new FbDevOutputOptions() { - Scaling = GetScaling(), - Orientation = GetOrientation(), + Scaling = GetScaling() }); } else if (args.Contains("--vnc")) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs index 6e7b04bcb31..fbd2c9dc14e 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevOutputOptions.cs @@ -28,12 +28,6 @@ public class FbDevOutputOptions /// public double Scaling { get; set; } = 1; - /// - /// The orientation of the screen relative to the frame buffer memory orientation - /// Default: Normal - /// - public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Rotation0; - /// /// If set to true, FBIO_WAITFORVSYNC ioctl and following memcpy call will run on a dedicated thread /// saving current one from doing nothing in a blocking call diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 721d248e318..08a99d7c078 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -8,7 +8,7 @@ namespace Avalonia.LinuxFramebuffer { - public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend, ISurfaceOrientation + public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend { private int _fd; private fb_fix_screeninfo _fixedInfo; @@ -20,8 +20,6 @@ public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposabl private bool _lockedAtLeastOnce; public double Scaling { get; set; } - public SurfaceOrientation Orientation { get; set; } - /// /// Create a Linux frame buffer device output /// @@ -60,8 +58,6 @@ public FbdevOutput(FbDevOutputOptions options) throw new Exception("Error: " + Marshal.GetLastWin32Error()); _options = options; Scaling = options.Scaling; - Orientation = options.Orientation; - try { Init(options.PixelFormat); diff --git a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs index ca16a1a1172..318cdac22c0 100644 --- a/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs @@ -16,20 +16,17 @@ internal class FramebufferRenderTarget : IRenderTarget2 private IntPtr _currentFramebufferAddress; private SKSurface? _framebufferSurface; private PixelFormatConversionShim? _conversionShim; - private IFramebufferPlatformSurface _platformSurface; + private IDisposable? _preFramebufferCopyHandler; private IFramebufferRenderTarget? _renderTarget; private IFramebufferRenderTargetWithProperties? _renderTargetWithProperties; private bool _hadConversionShim; - private SurfaceOrientation Orientation => _platformSurface is ISurfaceOrientation o ? o.Orientation : SurfaceOrientation.Rotation0; - /// /// Create new framebuffer render target using a target surface. /// /// Target surface. public FramebufferRenderTarget(IFramebufferPlatformSurface platformSurface) { - _platformSurface = platformSurface; _renderTarget = platformSurface.CreateFramebufferRenderTarget(); _renderTargetWithProperties = _renderTarget as IFramebufferRenderTargetWithProperties; } @@ -93,7 +90,7 @@ IDrawingContextImpl CreateDrawingContextCore(bool scaleDrawingToDpi, PreviousFrameIsRetained = !_hadConversionShim && lockProperties.PreviousFrameIsRetained }; - return new DrawingContextImpl(createInfo, _conversionShim?.SurfaceCopyHandler, canvas, framebuffer); + return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); } public bool IsCorrupted => false; @@ -119,10 +116,7 @@ private static bool AreImageInfosCompatible(SKImageInfo currentImageInfo, SKImag [MemberNotNull(nameof(_framebufferSurface))] private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer framebuffer) { - var orientation = Orientation; - - if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) - && _currentFramebufferAddress == framebuffer.Address && _conversionShim?.Orientation == orientation) + if (_framebufferSurface != null && AreImageInfosCompatible(_currentImageInfo, desiredImageInfo) && _currentFramebufferAddress == framebuffer.Address) { return; } @@ -131,18 +125,14 @@ private void CreateSurface(SKImageInfo desiredImageInfo, ILockedFramebuffer fram _currentFramebufferAddress = framebuffer.Address; - // Create a surface using the framebuffer address unless we need to rotate the display - SKSurface? surface = null; - if (orientation == SurfaceOrientation.Rotation0) - { - surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, - framebuffer.RowBytes, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); - } + var surface = SKSurface.Create(desiredImageInfo, _currentFramebufferAddress, + framebuffer.RowBytes, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal)); // If surface cannot be created - try to create a compatibility shim first if (surface == null) { - _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address, orientation); + _conversionShim = new PixelFormatConversionShim(desiredImageInfo, framebuffer.Address); + _preFramebufferCopyHandler = _conversionShim.SurfaceCopyHandler; surface = _conversionShim.Surface; } @@ -160,6 +150,7 @@ private void FreeSurface() { _conversionShim?.Dispose(); _conversionShim = null; + _preFramebufferCopyHandler = null; _framebufferSurface?.Dispose(); _framebufferSurface = null; @@ -172,23 +163,16 @@ private void FreeSurface() private class PixelFormatConversionShim : IDisposable { private readonly SKBitmap _bitmap; - private readonly SurfaceOrientation _orientation; private readonly SKImageInfo _destinationInfo; private readonly IntPtr _framebufferAddress; - public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress, SurfaceOrientation orientation) + public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebufferAddress) { - _orientation = orientation; _destinationInfo = destinationInfo; _framebufferAddress = framebufferAddress; // Create bitmap using default platform settings - _bitmap = orientation switch - { - SurfaceOrientation.Rotation90 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), - SurfaceOrientation.Rotation270 => new SKBitmap(destinationInfo.Height, destinationInfo.Width), - _ => new SKBitmap(destinationInfo.Width, destinationInfo.Height), - }; + _bitmap = new SKBitmap(destinationInfo.Width, destinationInfo.Height); SKColorType bitmapColorType; if (!_bitmap.CanCopyTo(destinationInfo.ColorType)) @@ -210,6 +194,8 @@ public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebuffer throw new Exception( $"Unable to create pixel format shim surface for conversion from {bitmapColorType} to {destinationInfo.ColorType}"); } + + SurfaceCopyHandler = Disposable.Create(CopySurface); } /// @@ -220,9 +206,7 @@ public PixelFormatConversionShim(SKImageInfo destinationInfo, IntPtr framebuffer /// /// Handler to start conversion via surface copy. /// - public IDisposable SurfaceCopyHandler { get => Disposable.Create(CopySurface); } - - public SurfaceOrientation Orientation => _orientation; + public IDisposable SurfaceCopyHandler { get; } /// public void Dispose() @@ -230,6 +214,7 @@ public void Dispose() Surface.Dispose(); _bitmap.Dispose(); } + /// /// Convert and copy surface to a framebuffer. /// @@ -237,56 +222,8 @@ private void CopySurface() { using (var snapshot = Surface.Snapshot()) { - if (Orientation != SurfaceOrientation.Rotation0) - { - // rotation or flipping required - int width; - int height; - - if (Orientation == SurfaceOrientation.Rotation180) - { - width = snapshot.Width; - height = snapshot.Height; - } - else - { - width = snapshot.Height; - height = snapshot.Width; - } - - // Create a new surface with swapped width and height - using var rotatedSurface = SKSurface.Create(new SKImageInfo(width, height)); - var rotatedCanvas = rotatedSurface.Canvas; - - // Apply transformation - rotatedCanvas.RotateDegrees(Orientation switch - { - SurfaceOrientation.Rotation90 => 90, - SurfaceOrientation.Rotation180 => 180, - SurfaceOrientation.Rotation270 => -90, - _ => 0 - }); - rotatedCanvas.Translate(Orientation switch - { - SurfaceOrientation.Rotation90 => new SKPoint(0, -width), - SurfaceOrientation.Rotation180 => new SKPoint(-width, -height), - SurfaceOrientation.Rotation270 => new SKPoint(-height, 0), - _ => new SKPoint(0, 0) - }); - - // Draw the original image onto the rotated canvas - rotatedCanvas.DrawImage(snapshot, 0, 0); - - // Return the rotated image - using var rotateSnapshot = rotatedSurface.Snapshot(); - rotateSnapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, - SKImageCachingHint.Disallow); - } - else - { - snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, - SKImageCachingHint.Disallow); - } + snapshot.ReadPixels(_destinationInfo, _framebufferAddress, _destinationInfo.RowBytes, 0, 0, + SKImageCachingHint.Disallow); } } } From e33101d5d5a0ab081c242f869d8ac80d05be10ba Mon Sep 17 00:00:00 2001 From: Thad House Date: Wed, 26 Nov 2025 12:41:17 -0800 Subject: [PATCH 6/8] Better method of rotating and transforming the canvas --- .../FramebufferToplevelImpl.cs | 14 +++---- .../Input/IScreenInfoProvider.cs | 3 ++ .../Input/LibInput/LibInputBackend.cs | 34 +++++++--------- .../Output/DrmOutput.cs | 2 +- .../Output/FbdevOutput.cs | 2 + .../Output/IOutputBackend.cs | 3 ++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 6 +-- .../Gpu/OpenGl/GlRenderTarget.cs | 26 ++----------- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 39 ++++++++++++++++++- 9 files changed, 71 insertions(+), 58 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 27ee1ddc1bf..4fb1683d14c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -11,21 +11,18 @@ namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider, ISurfaceOrientation + class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider { private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; private readonly RawEventGrouper _inputQueue; - private readonly SurfaceOrientation _orientation = SurfaceOrientation.Rotation0; + private readonly SurfaceOrientation _orientation; public IInputRoot? InputRoot { get; private set; } public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { - if (outputBackend is ISurfaceOrientation surfaceOrientation) - { - _orientation = surfaceOrientation.Orientation; - } + _orientation = outputBackend.Orientation; _outputBackend = outputBackend; _inputBackend = inputBackend; _inputQueue = new RawEventGrouper(groupedInput => Input?.Invoke(groupedInput), @@ -92,9 +89,8 @@ public void SetTransparencyLevelHint(IReadOnlyList tran public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); - public object? TryGetFeature(Type featureType) => null; - - SurfaceOrientation ISurfaceOrientation.Orientation => _orientation; + + SurfaceOrientation IScreenInfoProvider.Orientation => _orientation; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs index cb0e51862a0..95ed6e127e4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs @@ -1,7 +1,10 @@ +using Avalonia.Platform; + namespace Avalonia.LinuxFramebuffer.Input { public interface IScreenInfoProvider { Size ScaledSize { get; } + SurfaceOrientation Orientation { get; } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index ee43af0a64d..0cc0bda448b 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -33,30 +33,24 @@ private unsafe void InputThread(IntPtr ctx, LibInputBackendOptions options) { var fd = libinput_get_fd(ctx); IntPtr[] devices = [.. options.Events!.Select(f => libinput_path_add_device(ctx, f)).Where(d => d != IntPtr.Zero)]; - SurfaceOrientation screenOrientation = SurfaceOrientation.Rotation0; + var screenOrientation = _screen?.Orientation ?? SurfaceOrientation.Rotation0; + + float[] matrix = screenOrientation switch + { + SurfaceOrientation.Rotation90 => [0, 1, 0, -1, 0, 1], + SurfaceOrientation.Rotation180 => [-1, 0, 1, 0, -1, 1], + SurfaceOrientation.Rotation270 => [0, -1, 1, 1, 0, 0], + _ => [1, 0, 0, 0, 1, 0], // Normal + }; + + foreach (var device in devices) + { + libinput_device_config_calibration_set_matrix(device, matrix); + } while (true) { IntPtr ev; - - if (_screen is ISurfaceOrientation surfaceOrientationProvider && surfaceOrientationProvider.Orientation != screenOrientation) - { - screenOrientation = surfaceOrientationProvider.Orientation; - - float[] matrix = screenOrientation switch - { - SurfaceOrientation.Rotation90 => [0, 1, 0, -1, 0, 1], - SurfaceOrientation.Rotation180 => [-1, 0, 1, 0, -1, 1], - SurfaceOrientation.Rotation270 => [0, -1, 1, 1, 0, 0], - _ => [1, 0, 0, 0, 1, 0], // Normal - }; - - foreach (var device in devices) - { - libinput_device_config_calibration_set_matrix(device, matrix); - } - } - libinput_dispatch(ctx); while ((ev = libinput_get_event(ctx)) != IntPtr.Zero) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index a7e0c51c363..5b60cf3cabc 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -13,7 +13,7 @@ namespace Avalonia.LinuxFramebuffer.Output { - public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface, ISurfaceOrientation + public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface { private DrmOutputOptions _outputOptions = new(); private DrmCard _card; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 08a99d7c078..0dc7229c59a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -162,6 +162,8 @@ public PixelSize PixelSize } } + public SurfaceOrientation Orientation => SurfaceOrientation.Rotation0; + public ILockedFramebuffer Lock() => Lock(out _); private ILockedFramebuffer Lock(out FramebufferLockProperties properties) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs index 17a39b0219c..871841be49a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -1,8 +1,11 @@ +using Avalonia.Platform; + namespace Avalonia.LinuxFramebuffer.Output { public interface IOutputBackend { PixelSize PixelSize { get; } double Scaling { get; set; } + SurfaceOrientation Orientation { get; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 99e876f4c71..e122c212420 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -30,7 +30,6 @@ internal partial class DrawingContextImpl : IDrawingContextImpl, private readonly Matrix? _postTransform; private double _currentOpacity = 1.0f; private readonly bool _disableSubpixelTextRendering; - private Matrix _baseTransform; // default canvas rotation private Matrix? _currentTransform; private bool _disposed; private GRContext? _grContext; @@ -188,7 +187,6 @@ public DrawingContextImpl(CreateInfo createInfo, params IDisposable?[]? disposab Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); - _baseTransform = Canvas.TotalMatrix44.ToAvaloniaMatrix(); _intermediateSurfaceDpi = createInfo.Dpi; _disposables = disposables; _disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering; @@ -850,7 +848,7 @@ public Matrix Transform { // There is a Canvas.TotalMatrix (non 4x4 overload), but internally it still uses 4x4 matrix. // We want to avoid SKMatrix4x4 -> SKMatrix -> Matrix conversion by directly going SKMatrix4x4 -> Matrix. - get { return _currentTransform ??= _baseTransform.Invert() * Canvas.TotalMatrix44.ToAvaloniaMatrix(); } + get { return _currentTransform ??= Canvas.TotalMatrix44.ToAvaloniaMatrix(); } set { CheckLease(); @@ -868,7 +866,7 @@ public Matrix Transform // Canvas.SetMatrix internally uses 4x4 matrix, even with SKMatrix(3x3) overload. // We want to avoid Matrix -> SKMatrix -> SKMatrix4x4 conversion by directly going Matrix -> SKMatrix4x4. - Canvas.SetMatrix((_baseTransform * transform).ToSKMatrix44()); + Canvas.SetMatrix(transform.ToSKMatrix44()); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 17e2708381f..fbb80a121fd 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -23,7 +23,7 @@ public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurf public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true; - class GlGpuSession : ISkiaGpuRenderSession + class GlGpuSession : ISkiaGpuRenderSession, ISurfaceOrientation { private readonly GRBackendRenderTarget _backendRenderTarget; private readonly SKSurface _surface; @@ -55,6 +55,8 @@ public void Dispose() public GRContext GrContext { get; } public SKSurface SkSurface => _surface; public double ScaleFactor => _glSession.Scaling; + + public SurfaceOrientation Orientation => _glSession is ISurfaceOrientation orientation ? orientation.Orientation : SurfaceOrientation.Rotation0; } public ISkiaGpuRenderSession BeginRenderingSession(PixelSize size) => BeginRenderingSessionCore(size); @@ -99,28 +101,6 @@ ISkiaGpuRenderSession BeginRenderingSessionCore(PixelSize? expectedSize) glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, colorType, _surfaceProperties); - // Apply rotation to the canvas if supported bt the backend and it's not the native hardware orientation - if (glSession is ISurfaceOrientation orientation && orientation.Orientation != SurfaceOrientation.Rotation0) - { - var canvas = surface.Canvas; - var width = size.Width; - var height = size.Height; - canvas.RotateDegrees(orientation.Orientation switch - { - SurfaceOrientation.Rotation90 => 90, - SurfaceOrientation.Rotation180 => 180, - SurfaceOrientation.Rotation270 => -90, - _ => 0 - }); - canvas.Translate(orientation.Orientation switch - { - SurfaceOrientation.Rotation90 => new SKPoint(0, -width), - SurfaceOrientation.Rotation180 => new SKPoint(-width, -height), - SurfaceOrientation.Rotation270 => new SKPoint(-height, 0), - _ => new SKPoint() - }); - } - success = true; return new GlGpuSession(_grContext, renderTarget, surface, glSession); diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 41380fdbd13..47e20802bb9 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -38,12 +38,19 @@ public void Dispose() } } + private readonly ISurfaceOrientation? _orientation; + /// /// Create new surface render target. /// /// Create info. public SurfaceRenderTarget(CreateInfo createInfo) - { + { + if (createInfo.Session is ISurfaceOrientation orientation) + { + _orientation = orientation; + } + PixelSize = new PixelSize(createInfo.Width, createInfo.Height); Dpi = createInfo.Dpi; @@ -141,6 +148,34 @@ public void Save(Stream stream, int? quality = null) ImageSavingHelper.SaveImage(image, stream, quality); } } + + private SKMatrix ResetMatrix(SKCanvas canvas) + { + var matrix = canvas.TotalMatrix; + var width = PixelSize.Width; + var height = PixelSize.Height; + + canvas.ResetMatrix(); + var orientation = _orientation?.Orientation ?? SurfaceOrientation.Rotation0; + if (orientation == SurfaceOrientation.Rotation0) { + return matrix; + } + canvas.RotateDegrees(orientation switch + { + SurfaceOrientation.Rotation90 => 90, + SurfaceOrientation.Rotation180 => 180, + SurfaceOrientation.Rotation270 => -90, + _ => 0 + }); + canvas.Translate(orientation switch + { + SurfaceOrientation.Rotation90 => new SKPoint(0, -height), + SurfaceOrientation.Rotation180 => new SKPoint(-width, -height), + SurfaceOrientation.Rotation270 => new SKPoint(-width, 0), + _ => new SKPoint() + }); + return matrix; + } public void Blit(IDrawingContextImpl contextImpl) { @@ -153,7 +188,9 @@ public void Blit(IDrawingContextImpl contextImpl) } else { + var oldMatrix = ResetMatrix(context.Canvas); _surface.Surface.Draw(context.Canvas, 0, 0, null); + context.Canvas.SetMatrix(oldMatrix); } } From 0ce63e5dd1ba44cb4256f8e760c2c99a749e7926 Mon Sep 17 00:00:00 2001 From: Thad House Date: Wed, 26 Nov 2025 17:14:52 -0800 Subject: [PATCH 7/8] Remove breaking changes --- .../Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs | 6 +++--- .../Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs | 3 --- .../Input/LibInput/LibInputBackend.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs | 2 -- .../Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs | 3 --- 6 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 4fb1683d14c..933b79b2c96 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider + class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider, ISurfaceOrientation { private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; @@ -22,7 +22,7 @@ class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { - _orientation = outputBackend.Orientation; + _orientation = outputBackend is ISurfaceOrientation surfaceOrientation ? surfaceOrientation.Orientation : SurfaceOrientation.Rotation0; _outputBackend = outputBackend; _inputBackend = inputBackend; _inputQueue = new RawEventGrouper(groupedInput => Input?.Invoke(groupedInput), @@ -91,6 +91,6 @@ public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); public object? TryGetFeature(Type featureType) => null; - SurfaceOrientation IScreenInfoProvider.Orientation => _orientation; + SurfaceOrientation ISurfaceOrientation.Orientation => _orientation; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs index 95ed6e127e4..cb0e51862a0 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs @@ -1,10 +1,7 @@ -using Avalonia.Platform; - namespace Avalonia.LinuxFramebuffer.Input { public interface IScreenInfoProvider { Size ScaledSize { get; } - SurfaceOrientation Orientation { get; } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 0cc0bda448b..671508a9438 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -33,7 +33,7 @@ private unsafe void InputThread(IntPtr ctx, LibInputBackendOptions options) { var fd = libinput_get_fd(ctx); IntPtr[] devices = [.. options.Events!.Select(f => libinput_path_add_device(ctx, f)).Where(d => d != IntPtr.Zero)]; - var screenOrientation = _screen?.Orientation ?? SurfaceOrientation.Rotation0; + var screenOrientation = _screen is ISurfaceOrientation surfaceOrientation ? surfaceOrientation.Orientation : SurfaceOrientation.Rotation0; float[] matrix = screenOrientation switch { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index 5b60cf3cabc..a7e0c51c363 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -13,7 +13,7 @@ namespace Avalonia.LinuxFramebuffer.Output { - public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface + public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface, ISurfaceOrientation { private DrmOutputOptions _outputOptions = new(); private DrmCard _card; diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 0dc7229c59a..08a99d7c078 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -162,8 +162,6 @@ public PixelSize PixelSize } } - public SurfaceOrientation Orientation => SurfaceOrientation.Rotation0; - public ILockedFramebuffer Lock() => Lock(out _); private ILockedFramebuffer Lock(out FramebufferLockProperties properties) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs index 871841be49a..17a39b0219c 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -1,11 +1,8 @@ -using Avalonia.Platform; - namespace Avalonia.LinuxFramebuffer.Output { public interface IOutputBackend { PixelSize PixelSize { get; } double Scaling { get; set; } - SurfaceOrientation Orientation { get; } } } From 70ec0777c1c3e5c03668dfafe4e78e8ed31945e7 Mon Sep 17 00:00:00 2001 From: Thad House Date: Wed, 26 Nov 2025 18:11:49 -0800 Subject: [PATCH 8/8] Switch to using a 2nd frame buffer in the rotation case --- src/Avalonia.OpenGL/GlConsts.cs | 2 +- src/Avalonia.OpenGL/GlInterface.cs | 2 + .../FramebufferToplevelImpl.cs | 13 +- .../Output/DrmOutput.cs | 206 +++++++++++++++++- .../Gpu/OpenGl/GlRenderTarget.cs | 6 +- src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 40 +--- 6 files changed, 209 insertions(+), 60 deletions(-) diff --git a/src/Avalonia.OpenGL/GlConsts.cs b/src/Avalonia.OpenGL/GlConsts.cs index 55be3da5f02..c1e1df06394 100644 --- a/src/Avalonia.OpenGL/GlConsts.cs +++ b/src/Avalonia.OpenGL/GlConsts.cs @@ -20,7 +20,7 @@ public static class GlConsts // public const int GL_LINE_STRIP = 0x0003; public const int GL_TRIANGLES = 0x0004; // public const int GL_TRIANGLE_STRIP = 0x0005; -// public const int GL_TRIANGLE_FAN = 0x0006; + public const int GL_TRIANGLE_FAN = 0x0006; // public const int GL_QUADS = 0x0007; // public const int GL_QUAD_STRIP = 0x0008; // public const int GL_POLYGON = 0x0009; diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 12a5ef733e3..63d265c41e8 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -343,6 +343,8 @@ public int GetUniformLocationString(int program, string name) [GetProcAddress("glUniform1f")] public partial void Uniform1f(int location, float falue); + [GetProcAddress("glUniform1i")] + public partial void Uniform1i(int location, int value); [GetProcAddress("glUniformMatrix4fv")] public partial void UniformMatrix4fv(int location, int count, bool transpose, void* value); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 933b79b2c96..edd0b1ef72f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -16,13 +16,11 @@ class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider, ISurfaceOrie private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; private readonly RawEventGrouper _inputQueue; - private readonly SurfaceOrientation _orientation; public IInputRoot? InputRoot { get; private set; } public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { - _orientation = outputBackend is ISurfaceOrientation surfaceOrientation ? surfaceOrientation.Orientation : SurfaceOrientation.Rotation0; _outputBackend = outputBackend; _inputBackend = inputBackend; _inputQueue = new RawEventGrouper(groupedInput => Input?.Invoke(groupedInput), @@ -73,14 +71,7 @@ public void SetCursor(ICursorImpl? cursor) public Action? Closed { get; set; } public Action? LostFocus { get; set; } - public PixelSize RotatedSize => _orientation switch - { - SurfaceOrientation.Rotation90 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), - SurfaceOrientation.Rotation270 => new PixelSize(_outputBackend.PixelSize.Height, _outputBackend.PixelSize.Width), - _ => _outputBackend.PixelSize, - }; - - public Size ScaledSize => RotatedSize.ToSize(RenderScaling); + public Size ScaledSize => _outputBackend.PixelSize.ToSize(RenderScaling); public void SetTransparencyLevelHint(IReadOnlyList transparencyLevel) { } @@ -91,6 +82,6 @@ public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); public object? TryGetFeature(Type featureType) => null; - SurfaceOrientation ISurfaceOrientation.Orientation => _orientation; + SurfaceOrientation ISurfaceOrientation.Orientation => _outputBackend is ISurfaceOrientation surfaceOrientation ? surfaceOrientation.Orientation : SurfaceOrientation.Rotation0; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index a7e0c51c363..d0d0a42a2c4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -17,7 +17,9 @@ public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface, ISurfaceOr { private DrmOutputOptions _outputOptions = new(); private DrmCard _card; - public PixelSize PixelSize => _mode.Resolution; + public PixelSize PixelSize => Orientation == SurfaceOrientation.Rotation0 || Orientation == SurfaceOrientation.Rotation180 + ? new PixelSize(_mode.Resolution.Width, _mode.Resolution.Height) + : new PixelSize(_mode.Resolution.Height, _mode.Resolution.Width); public double Scaling { @@ -119,6 +121,12 @@ public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, D private IntPtr _currentBo; private IntPtr _gbmTargetSurface; private uint _crtcId; + private int _rotationFbo; + private int _rotationTexture; + private PixelSize _rotatedSize; + private int _rotationProgram; + private int _rotationVbo; + private int _rotationVao; void FbDestroyCallback(IntPtr bo, IntPtr userData) { @@ -163,7 +171,6 @@ uint GetFbIdForBo(IntPtr bo) return fbHandle; } - [MemberNotNull(nameof(_card))] [MemberNotNull(nameof(PlatformGraphics))] [MemberNotNull(nameof(FbDestroyDelegate))] @@ -242,6 +249,143 @@ uint GetCrtc() _mode = mode; _currentBo = bo; + + // Initialize FBO for rotation if needed + var needsRotation = _outputOptions.Orientation != SurfaceOrientation.Rotation0; + if (needsRotation) + { + // For 90/270 rotation, swap width and height + _rotatedSize = (_outputOptions.Orientation == SurfaceOrientation.Rotation90 || + _outputOptions.Orientation == SurfaceOrientation.Rotation270) + ? new PixelSize(modeInfo.Resolution.Height, modeInfo.Resolution.Width) + : modeInfo.Resolution; + + using (_deferredContext.MakeCurrent(_eglSurface)) + { + var gl = _deferredContext.GlInterface; + _rotationFbo = gl.GenFramebuffer(); + + unsafe + { + int tex = 0; + gl.GenTextures(1, &tex); + _rotationTexture = tex; + } + + gl.BindTexture(GlConsts.GL_TEXTURE_2D, _rotationTexture); + gl.TexImage2D(GlConsts.GL_TEXTURE_2D, 0, GlConsts.GL_RGBA, _rotatedSize.Width, _rotatedSize.Height, 0, + GlConsts.GL_RGBA, GlConsts.GL_UNSIGNED_BYTE, IntPtr.Zero); + gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MIN_FILTER, GlConsts.GL_LINEAR); + gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MAG_FILTER, GlConsts.GL_LINEAR); + + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _rotationFbo); + gl.FramebufferTexture2D(GlConsts.GL_FRAMEBUFFER, GlConsts.GL_COLOR_ATTACHMENT0, + GlConsts.GL_TEXTURE_2D, _rotationTexture, 0); + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + + // Create shader program for textured quad + const string vertexShader = @" + attribute vec2 aPos; + attribute vec2 aTexCoord; + varying vec2 vTexCoord; + void main() { + gl_Position = vec4(aPos, 0.0, 1.0); + vTexCoord = aTexCoord; + }"; + + const string fragmentShader = @" + precision mediump float; + varying vec2 vTexCoord; + uniform sampler2D uTexture; + void main() { + gl_FragColor = texture2D(uTexture, vTexCoord); + }"; + + var vs = gl.CreateShader(GlConsts.GL_VERTEX_SHADER); + gl.ShaderSourceString(vs, vertexShader); + gl.CompileShader(vs); + + var fs = gl.CreateShader(GlConsts.GL_FRAGMENT_SHADER); + gl.ShaderSourceString(fs, fragmentShader); + gl.CompileShader(fs); + + _rotationProgram = gl.CreateProgram(); + gl.AttachShader(_rotationProgram, vs); + gl.AttachShader(_rotationProgram, fs); + gl.LinkProgram(_rotationProgram); + gl.DeleteShader(vs); + gl.DeleteShader(fs); + + // Create VBO with quad vertices - texture coords depend on rotation + // Format: x, y, u, v + float[] vertices = _outputOptions.Orientation switch + { + SurfaceOrientation.Rotation90 => new float[] { + // 90° clockwise rotation + -1.0f, -1.0f, 1.0f, 0.0f, // Bottom-left -> Bottom-right of texture + 1.0f, -1.0f, 1.0f, 1.0f, // Bottom-right -> Top-right of texture + 1.0f, 1.0f, 0.0f, 1.0f, // Top-right -> Top-left of texture + -1.0f, 1.0f, 0.0f, 0.0f // Top-left -> Bottom-left of texture + }, + SurfaceOrientation.Rotation180 => new float[] { + // 180° rotation + -1.0f, -1.0f, 1.0f, 1.0f, // Bottom-left -> Top-right of texture + 1.0f, -1.0f, 0.0f, 1.0f, // Bottom-right -> Top-left of texture + 1.0f, 1.0f, 0.0f, 0.0f, // Top-right -> Bottom-left of texture + -1.0f, 1.0f, 1.0f, 0.0f // Top-left -> Bottom-right of texture + }, + SurfaceOrientation.Rotation270 => new float[] { + // 270° clockwise (90° counter-clockwise) rotation + -1.0f, -1.0f, 0.0f, 1.0f, // Bottom-left -> Top-left of texture + 1.0f, -1.0f, 0.0f, 0.0f, // Bottom-right -> Bottom-left of texture + 1.0f, 1.0f, 1.0f, 0.0f, // Top-right -> Bottom-right of texture + -1.0f, 1.0f, 1.0f, 1.0f // Top-left -> Top-right of texture + }, + _ => new float[] { + // No rotation (shouldn't reach here but fallback) + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f + } + }; + + unsafe + { + int vbo = 0; + gl.GenBuffers(1, &vbo); + _rotationVbo = vbo; + + int vao = 0; + gl.GenVertexArrays(1, &vao); + _rotationVao = vao; + } + + gl.BindVertexArray(_rotationVao); + gl.BindBuffer(GlConsts.GL_ARRAY_BUFFER, _rotationVbo); + + fixed (float* ptr = vertices) + { + gl.BufferData(GlConsts.GL_ARRAY_BUFFER, new IntPtr(vertices.Length * sizeof(float)), + new IntPtr(ptr), GlConsts.GL_STATIC_DRAW); + } + + var posAttrib = gl.GetAttribLocationString(_rotationProgram, "aPos"); + gl.EnableVertexAttribArray(posAttrib); + gl.VertexAttribPointer(posAttrib, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), IntPtr.Zero); + + var texAttrib = gl.GetAttribLocationString(_rotationProgram, "aTexCoord"); + gl.EnableVertexAttribArray(texAttrib); + gl.VertexAttribPointer(texAttrib, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), new IntPtr(2 * sizeof(float))); + + gl.BindVertexArray(0); + } + } + else + { + // No rotation needed + _rotatedSize = modeInfo.Resolution; + } if (_outputOptions.EnableInitialBufferSwapping) { @@ -281,7 +425,7 @@ public void Dispose() // We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim } - class RenderSession : IGlPlatformSurfaceRenderingSession, ISurfaceOrientation + class RenderSession : IGlPlatformSurfaceRenderingSession { private readonly DrmOutput _parent; private readonly IDisposable _clearContext; @@ -294,7 +438,39 @@ public RenderSession(DrmOutput parent, IDisposable clearContext) public void Dispose() { - _parent._deferredContext.GlInterface.Flush(); + var gl = _parent._deferredContext.GlInterface; + + if (_parent._outputOptions.Orientation != SurfaceOrientation.Rotation0) + { + // Rotation enabled - blit from FBO to screen + // Unbind FBO to render to default framebuffer + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + gl.Viewport(0, 0, _parent._mode.Resolution.Width, _parent._mode.Resolution.Height); + + // Clear the screen + gl.ClearColor(0, 0, 0, 1); + gl.Clear(GlConsts.GL_COLOR_BUFFER_BIT); + + // Use the shader program + gl.UseProgram(_parent._rotationProgram); + + // Bind the FBO texture + gl.ActiveTexture(GlConsts.GL_TEXTURE0); + gl.BindTexture(GlConsts.GL_TEXTURE_2D, _parent._rotationTexture); + + // Set texture uniform (texture unit 0) + var texLoc = gl.GetUniformLocationString(_parent._rotationProgram, "uTexture"); + gl.Uniform1i(texLoc, 0); + + // Draw the rotated quad + gl.BindVertexArray(_parent._rotationVao); + gl.DrawArrays(GlConsts.GL_TRIANGLE_FAN, 0, 4); + gl.BindVertexArray(0); + + gl.UseProgram(0); + } + + gl.Flush(); _parent._eglSurface.SwapBuffers(); var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface); @@ -339,18 +515,32 @@ public void Dispose() public IGlContext Context => _parent._deferredContext; - public PixelSize Size => _parent._mode.Resolution; + public PixelSize Size => _parent._rotatedSize; public double Scaling => _parent.Scaling; public bool IsYFlipped => false; - - public SurfaceOrientation Orientation { get => _parent.Orientation; set => _parent.Orientation = value; } } public IGlPlatformSurfaceRenderingSession BeginDraw() { - return new RenderSession(_parent, _parent._deferredContext.MakeCurrent(_parent._eglSurface)); + var clearContext = _parent._deferredContext.MakeCurrent(_parent._eglSurface); + var gl = _parent._deferredContext.GlInterface; + + if (_parent._outputOptions.Orientation != SurfaceOrientation.Rotation0) + { + // Bind FBO for rendering when rotation is enabled + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, _parent._rotationFbo); + gl.Viewport(0, 0, _parent._rotatedSize.Width, _parent._rotatedSize.Height); + } + else + { + // Render directly to screen when no rotation + gl.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + gl.Viewport(0, 0, _parent._mode.Resolution.Width, _parent._mode.Resolution.Height); + } + + return new RenderSession(_parent, clearContext); } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index fbb80a121fd..56ce4a71945 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -1,7 +1,9 @@ using System; +using Avalonia.Reactive; using Avalonia.OpenGL; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; +using Avalonia.Rendering; using SkiaSharp; using static Avalonia.OpenGL.GlConsts; @@ -23,7 +25,7 @@ public GlRenderTarget(GRContext grContext, IGlContext glContext, IGlPlatformSurf public bool IsCorrupted => (_surface as IGlPlatformSurfaceRenderTargetWithCorruptionInfo)?.IsCorrupted == true; - class GlGpuSession : ISkiaGpuRenderSession, ISurfaceOrientation + class GlGpuSession : ISkiaGpuRenderSession { private readonly GRBackendRenderTarget _backendRenderTarget; private readonly SKSurface _surface; @@ -55,8 +57,6 @@ public void Dispose() public GRContext GrContext { get; } public SKSurface SkSurface => _surface; public double ScaleFactor => _glSession.Scaling; - - public SurfaceOrientation Orientation => _glSession is ISurfaceOrientation orientation ? orientation.Orientation : SurfaceOrientation.Rotation0; } public ISkiaGpuRenderSession BeginRenderingSession(PixelSize size) => BeginRenderingSessionCore(size); diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 47e20802bb9..87aebc2df14 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -38,19 +38,12 @@ public void Dispose() } } - private readonly ISurfaceOrientation? _orientation; - /// /// Create new surface render target. /// /// Create info. public SurfaceRenderTarget(CreateInfo createInfo) - { - if (createInfo.Session is ISurfaceOrientation orientation) - { - _orientation = orientation; - } - + { PixelSize = new PixelSize(createInfo.Width, createInfo.Height); Dpi = createInfo.Dpi; @@ -148,34 +141,6 @@ public void Save(Stream stream, int? quality = null) ImageSavingHelper.SaveImage(image, stream, quality); } } - - private SKMatrix ResetMatrix(SKCanvas canvas) - { - var matrix = canvas.TotalMatrix; - var width = PixelSize.Width; - var height = PixelSize.Height; - - canvas.ResetMatrix(); - var orientation = _orientation?.Orientation ?? SurfaceOrientation.Rotation0; - if (orientation == SurfaceOrientation.Rotation0) { - return matrix; - } - canvas.RotateDegrees(orientation switch - { - SurfaceOrientation.Rotation90 => 90, - SurfaceOrientation.Rotation180 => 180, - SurfaceOrientation.Rotation270 => -90, - _ => 0 - }); - canvas.Translate(orientation switch - { - SurfaceOrientation.Rotation90 => new SKPoint(0, -height), - SurfaceOrientation.Rotation180 => new SKPoint(-width, -height), - SurfaceOrientation.Rotation270 => new SKPoint(-width, 0), - _ => new SKPoint() - }); - return matrix; - } public void Blit(IDrawingContextImpl contextImpl) { @@ -188,7 +153,8 @@ public void Blit(IDrawingContextImpl contextImpl) } else { - var oldMatrix = ResetMatrix(context.Canvas); + var oldMatrix = context.Canvas.TotalMatrix; + context.Canvas.ResetMatrix(); _surface.Surface.Draw(context.Canvas, 0, 0, null); context.Canvas.SetMatrix(oldMatrix); }