Browse Source

Add screen orientation support for Linux DRM (#20154)

* 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.

* Fix review comments, add ability to test

* Some formatting

* Should be working now

* Remove fbdev changes

Theres really no point to do this in fbdev.

* Better method of rotating and transforming the canvas

* Remove breaking changes

* Switch to using a 2nd frame buffer in the rotation case

* Fix review comments

* Incorrect variable

* Fix sample

---------

Co-authored-by: davidw <davidw@icselectronics.co.uk>
pull/20593/head
Thad House 3 days ago
committed by GitHub
parent
commit
babab4f9cb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 32
      samples/ControlCatalog.Desktop/Program.cs
  2. 19
      src/Android/Avalonia.Android/Platform/AndroidScreens.cs
  3. 6
      src/Avalonia.Base/Platform/ISurfaceOrientation.cs
  4. 9
      src/Avalonia.Base/Platform/SurfaceOrientation.cs
  5. 2
      src/Avalonia.OpenGL/GlConsts.cs
  6. 2
      src/Avalonia.OpenGL/GlInterface.cs
  7. 9
      src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs
  8. 4
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  9. 19
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  10. 3
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs
  11. 5
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  12. 196
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs

32
samples/ControlCatalog.Desktop/Program.cs

@ -8,8 +8,10 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
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;
@ -19,16 +21,9 @@ namespace ControlCatalog.Desktop
{
static class Program
{
private static bool s_useFramebuffer;
[STAThread]
static int Main(string[] args)
{
if (args.Contains("--fbdev"))
{
s_useFramebuffer = true;
}
if (args.Contains("--wait-for-attach"))
{
Console.WriteLine("Attach debugger and use 'Set next statement'");
@ -50,7 +45,22 @@ namespace ControlCatalog.Desktop
return scaling;
return 1;
}
if (s_useFramebuffer)
SurfaceOrientation GetOrientation()
{
var idx = Array.IndexOf(args, "--orientation");
if (idx >= 0 && args.Length > idx + 1 &&
Enum.TryParse<SurfaceOrientation>(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 (args.Contains("--fbdev"))
{
SilenceConsole();
return builder.StartLinuxFbDev(args, new FbDevOutputOptions()
@ -108,7 +118,11 @@ namespace ControlCatalog.Desktop
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"))
{

19
src/Android/Avalonia.Android/Platform/AndroidScreens.cs

@ -10,6 +10,7 @@ using AndroidX.Window.Layout;
using Avalonia.Android.Platform.SkiaPlatform;
using Avalonia.Platform;
using AndroidOrientation = global::Android.Content.Res.Orientation;
using AndroidRotation = global::Android.Views.SurfaceOrientation;
namespace Avalonia.Android.Platform;
@ -53,7 +54,7 @@ internal class AndroidScreen(Display display) : PlatformScreen(new PlatformHandl
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 AndroidRotation.Rotation0 or AndroidRotation.Rotation180)
naturalOrientation = orientation == AndroidOrientation.Landscape ?
ScreenOrientation.Landscape :
ScreenOrientation.Portrait;
@ -73,14 +74,14 @@ internal class AndroidScreen(Display display) : PlatformScreen(new PlatformHandl
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,
(AndroidRotation.Rotation0, ScreenOrientation.Landscape) => ScreenOrientation.Landscape,
(AndroidRotation.Rotation90, ScreenOrientation.Landscape) => ScreenOrientation.Portrait,
(AndroidRotation.Rotation180, ScreenOrientation.Landscape) => ScreenOrientation.LandscapeFlipped,
(AndroidRotation.Rotation270, ScreenOrientation.Landscape) => ScreenOrientation.PortraitFlipped,
(AndroidRotation.Rotation0, _) => ScreenOrientation.Portrait,
(AndroidRotation.Rotation90, _) => ScreenOrientation.Landscape,
(AndroidRotation.Rotation180, _) => ScreenOrientation.PortraitFlipped,
(AndroidRotation.Rotation270, _) => ScreenOrientation.LandscapeFlipped,
_ => ScreenOrientation.Portrait
};
}

6
src/Avalonia.Base/Platform/ISurfaceOrientation.cs

@ -0,0 +1,6 @@
namespace Avalonia.Platform;
internal interface ISurfaceOrientation
{
SurfaceOrientation Orientation { get; }
}

9
src/Avalonia.Base/Platform/SurfaceOrientation.cs

@ -0,0 +1,9 @@
namespace Avalonia.Platform;
public enum SurfaceOrientation
{
Rotation0,
Rotation90,
Rotation180,
Rotation270,
}

2
src/Avalonia.OpenGL/GlConsts.cs

@ -20,7 +20,7 @@ namespace Avalonia.OpenGL
// 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;

2
src/Avalonia.OpenGL/GlInterface.cs

@ -343,6 +343,8 @@ namespace Avalonia.OpenGL
[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);

9
src/Linux/Avalonia.LinuxFramebuffer/DrmOutputOptions.cs

@ -1,4 +1,5 @@
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.LinuxFramebuffer
{
@ -12,7 +13,13 @@ namespace Avalonia.LinuxFramebuffer
/// Default: 1.0
/// </summary>
public double Scaling { get; set; } = 1.0;
/// <summary>
/// The orientation of the screen relative to the frame buffer memory orientation
/// Default: Normal
/// </summary>
public SurfaceOrientation Orientation { get; set; } = SurfaceOrientation.Rotation0;
/// <summary>
/// If true an two cycle buffer swapping is processed at init.
/// Default: True

4
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@ -11,7 +11,7 @@ using Avalonia.Rendering.Composition;
namespace Avalonia.LinuxFramebuffer
{
class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider
class FramebufferToplevelImpl : ITopLevelImpl, IScreenInfoProvider, ISurfaceOrientation
{
private readonly IOutputBackend _outputBackend;
private readonly IInputBackend _inputBackend;
@ -81,5 +81,7 @@ using Avalonia.Rendering.Composition;
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1);
public object? TryGetFeature(Type featureType) => null;
SurfaceOrientation ISurfaceOrientation.Orientation => _outputBackend is ISurfaceOrientation surfaceOrientation ? surfaceOrientation.Orientation : SurfaceOrientation.Rotation0;
}
}

19
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.Platform;
using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods;
namespace Avalonia.LinuxFramebuffer.Input.LibInput
{
@ -30,9 +32,22 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
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 is ISurfaceOrientation surfaceOrientation ? surfaceOrientation.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);
}
foreach (var f in options.Events!)
libinput_path_add_device(ctx, f);
while (true)
{
IntPtr ev;

3
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs

@ -55,6 +55,9 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput
[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);

5
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -142,7 +142,10 @@ namespace Avalonia.LinuxFramebuffer
{
get
{
EnsureTopLevel();
if (_topLevel == null)
{
EnsureTopLevel();
}
return _topLevel;
}
}

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

@ -13,11 +13,13 @@ using static Avalonia.LinuxFramebuffer.Output.LibDrm;
namespace Avalonia.LinuxFramebuffer.Output
{
public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface
public unsafe class DrmOutput : IGlOutputBackend, IGlPlatformSurface, ISurfaceOrientation
{
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
{
@ -25,6 +27,12 @@ namespace Avalonia.LinuxFramebuffer.Output
set => _outputOptions.Scaling = value;
}
public SurfaceOrientation Orientation
{
get => _outputOptions.Orientation;
set => _outputOptions.Orientation = value;
}
class SharedContextGraphics : IPlatformGraphics
{
private readonly IPlatformGraphicsContext _context;
@ -113,6 +121,12 @@ namespace Avalonia.LinuxFramebuffer.Output
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)
{
@ -157,7 +171,6 @@ namespace Avalonia.LinuxFramebuffer.Output
return fbHandle;
}
[MemberNotNull(nameof(_card))]
[MemberNotNull(nameof(PlatformGraphics))]
[MemberNotNull(nameof(FbDestroyDelegate))]
@ -236,6 +249,129 @@ namespace Avalonia.LinuxFramebuffer.Output
_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();
_rotationTexture = gl.GenTexture();
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
}
};
_rotationVbo = gl.GenBuffer();
_rotationVao = gl.GenVertexArray();
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)
{
@ -288,7 +424,39 @@ namespace Avalonia.LinuxFramebuffer.Output
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);
@ -333,7 +501,7 @@ namespace Avalonia.LinuxFramebuffer.Output
public IGlContext Context => _parent._deferredContext;
public PixelSize Size => _parent._mode.Resolution;
public PixelSize Size => _parent._rotatedSize;
public double Scaling => _parent.Scaling;
@ -342,7 +510,23 @@ namespace Avalonia.LinuxFramebuffer.Output
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);
}
}

Loading…
Cancel
Save