diff --git a/GSS2.Test/DependencyInjectionHelper.cs b/GSS2.Test/DependencyInjectionHelper.cs index 91827dd..ef8260d 100644 --- a/GSS2.Test/DependencyInjectionHelper.cs +++ b/GSS2.Test/DependencyInjectionHelper.cs @@ -1,3 +1,5 @@ +using GSS2.UI.Core.Views; +using GSS2.UI.Core.ViewModels; using GSS2.Test.Views; using GSS2.Test.ViewModels; diff --git a/GSS2.Test/DependencyInjectionViewLocator.cs b/GSS2.Test/DependencyInjectionViewLocator.cs index aa5a620..5e72dab 100644 --- a/GSS2.Test/DependencyInjectionViewLocator.cs +++ b/GSS2.Test/DependencyInjectionViewLocator.cs @@ -1,7 +1,7 @@ using Avalonia.Controls; using Avalonia.Controls.Templates; -using GSS2.Test.ViewModels; +using GSS2.UI.Core.ViewModels; namespace GSS2.Test; @@ -12,7 +12,7 @@ public class DependencyInjectionViewLocator : IDataTemplate if (data is null) return null; - var name = data.GetType().FullName? + var name = data.GetType().AssemblyQualifiedName? .Replace("ViewModels", "Views") .Replace("ViewModel", "View"); if (name is null) diff --git a/GSS2.Test/GSS2.Test.csproj b/GSS2.Test/GSS2.Test.csproj index 64b64e5..e3979a0 100644 --- a/GSS2.Test/GSS2.Test.csproj +++ b/GSS2.Test/GSS2.Test.csproj @@ -8,7 +8,6 @@ dotnet-GSS2.Test-191ece0a-855d-46ec-9ecb-44378fc3db83 app.manifest true - true @@ -20,20 +19,15 @@ - None All - - - - - + diff --git a/GSS2.Test/ViewModels/CameraViewModel.cs b/GSS2.Test/ViewModels/CameraViewModel.cs deleted file mode 100644 index ba82f63..0000000 --- a/GSS2.Test/ViewModels/CameraViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using GSS2.Core.Hardware; - -using LibCameraSharp; - -namespace GSS2.Test.ViewModels; - -public class CameraViewModel : ViewModelBase -{ - private readonly CameraService _cameraService; - private readonly ILogger _logger; - - public CameraViewModel(CameraService cameraService, ILogger logger) - { - _cameraService = cameraService; - _logger = logger; - _logger.LogInformation("{} initialized", nameof(CameraViewModel)); - } - - public void StartViewFinder() => _cameraService.StartViewFinder(); - public void StopViewFinder() => _cameraService.StopViewFinder(); - public (FrameBuffer, StreamConfiguration)? GrabViewFinderFrame() => _cameraService.GrabViewFinderFrame(); - public void ReturnViewFinderFrame(FrameBuffer frameBuffer) => _cameraService.ReturnViewFinderFrame(frameBuffer); -} \ No newline at end of file diff --git a/GSS2.Test/ViewModels/MainViewModel.cs b/GSS2.Test/ViewModels/MainViewModel.cs index 9e3caad..5a0823e 100644 --- a/GSS2.Test/ViewModels/MainViewModel.cs +++ b/GSS2.Test/ViewModels/MainViewModel.cs @@ -1,5 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; +using GSS2.UI.Core.ViewModels; + namespace GSS2.Test.ViewModels; public partial class MainViewModel : ViewModelBase diff --git a/GSS2.Test/ViewModels/MainWindowViewModel.cs b/GSS2.Test/ViewModels/MainWindowViewModel.cs index 6b5964e..9e15df5 100644 --- a/GSS2.Test/ViewModels/MainWindowViewModel.cs +++ b/GSS2.Test/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; +using GSS2.UI.Core.ViewModels; + namespace GSS2.Test.ViewModels; public partial class MainWindowViewModel : ViewModelBase diff --git a/GSS2.Test/ViewModels/ResourcesViewModel.cs b/GSS2.Test/ViewModels/ResourcesViewModel.cs index dda898c..cd8ff5f 100644 --- a/GSS2.Test/ViewModels/ResourcesViewModel.cs +++ b/GSS2.Test/ViewModels/ResourcesViewModel.cs @@ -4,15 +4,15 @@ using CommunityToolkit.Mvvm.ComponentModel; using Avalonia.Threading; +using SkiaSharp; + using LiveChartsCore; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Painting; - -using SkiaSharp; +using LiveChartsCore.Defaults; using GSS2.Core.Hardware; -using LiveChartsCore.SkiaSharpView.Avalonia; -using LiveChartsCore.Defaults; +using GSS2.UI.Core.ViewModels; namespace GSS2.Test.ViewModels; diff --git a/GSS2.Test/Views/CameraView.axaml.cs b/GSS2.Test/Views/CameraView.axaml.cs deleted file mode 100644 index 4cd0df4..0000000 --- a/GSS2.Test/Views/CameraView.axaml.cs +++ /dev/null @@ -1,372 +0,0 @@ -using System.Runtime.InteropServices; -using Avalonia; -using Avalonia.LogicalTree; -using Avalonia.Media; -using Avalonia.OpenGL.Egl; -using Avalonia.OpenGL.Controls; -using Avalonia.OpenGL; -using Avalonia.Threading; -using GSS2.Test.ViewModels; - -namespace GSS2.Test.Views; - -public unsafe partial class CameraView : OpenGlControlBase -{ - private readonly ILogger _logger; - private bool _isEglInitialized = false; - private IntPtr? _eglDisplay = null; - private int? _texture = null; - private int? _program = null; - private int? _uTexLocation = null; - private int? _vao = null; - private int? _vbo = null; - private eglCreateImageKHR_t? _eglCreateImageKHR = null; - private eglDestroyImageKHR_t? _eglDestroyImageKHR = null; - private glEGLImageTargetTexture2DOES_t? _glEGLImageTargetTexture2DOES = null; - private System.Timers.Timer? _renderTimer = null; - - public CameraView(ILogger logger) - { - _logger = logger; - _logger.LogInformation("{} initialized", nameof(CameraView)); - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - (DataContext as CameraViewModel)?.StartViewFinder(); - base.OnAttachedToVisualTree(e); - _renderTimer = new System.Timers.Timer(33) - { - AutoReset = true - }; - _renderTimer.Elapsed += OnRendering; - _renderTimer.Start(); - } - protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - (DataContext as CameraViewModel)?.StopViewFinder(); - base.OnDetachedFromLogicalTree(e); - } - - private void OnRendering(object? sender, EventArgs e) - { - Dispatcher.UIThread.Invoke(() => - { - RequestNextFrameRendering(); - }); - } - - [DllImport("libEGL.so.1")] private static extern IntPtr eglQueryString(IntPtr dpy, int name); - [DllImport("libEGL.so.1")] private static extern IntPtr eglGetCurrentDisplay(); - [DllImport("libEGL.so.1")] static extern IntPtr eglGetProcAddress(string procname); - [DllImport("libGL.so.1")] static extern void glUniform1i(int location, int falue); - - delegate IntPtr glEGLImageTargetTexture2DOES_t(int target, IntPtr image); - delegate IntPtr eglCreateImageKHR_t(IntPtr dpy, IntPtr ctx, int target, IntPtr buffer, int[] attribs); - delegate bool eglDestroyImageKHR_t(IntPtr dpy, IntPtr image); - - private const uint DRM_FORMAT_BGRX8888 = 0x34325258; // XR24 XRGB8888 - private const int EGL_LINUX_DMA_BUF_EXT = 0x3270; - private const int EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272; - private const int EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273; - private const int EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274; - private const int EGL_LINUX_DRM_FOURCC_EXT = 0x3271; - private const int GL_CLAMP_TO_EDGE = 0x812F; - private const int GL_TEXTURE_WRAP_S = 0x2802; - private const int GL_TEXTURE_WRAP_T = 0x2803; - private const int GL_TRIANGLE_STRIP = 0x0005; - - private const string VertexShader = @" - #version 300 es - layout (location = 0) in vec2 aPos; - layout (location = 1) in vec2 aUv; - - out vec2 vUv; - - void main() - { - vUv = aUv; - gl_Position = vec4(aPos, 0.0, 1.0); - } - "; - private const string FragmentShader = @" - #version 300 es - precision mediump float; - - in vec2 vUv; - uniform sampler2D uTex; - - out vec4 fragColor; - - void main() - { - fragColor = texture(uTex, vUv); - } - "; - private readonly float[] Vertices = - [ - -1f, -1f, 0f, 1f, - 1f, -1f, 1f, 1f, - -1f, 1f, 0f, 0f, - 1f, 1f, 1f, 0f, - ]; - - protected override void OnOpenGlInit(GlInterface gl) - { - _logger.LogInformation("OpenGL initialized"); - _logger.LogInformation("GL version: {}", gl.GetString(GlConsts.GL_VERSION)); - _logger.LogInformation("GL vendor: {}", gl.GetString(GlConsts.GL_VENDOR)); - _logger.LogInformation("GL renderer: {}", gl.GetString(GlConsts.GL_RENDERER)); - - var glExtensions = gl.GetString(GlConsts.GL_EXTENSIONS); - if (glExtensions is null) - { - _logger.LogCritical("Cannot use EGL, GL extentions query returned nullptr"); - base.OnOpenGlInit(gl); - return; - } - _logger.LogInformation("GL extentions: [\n\t{}\n]", glExtensions.Trim().Replace(" ", "\n\t")); - if (!glExtensions.Contains("GL_OES_EGL_image")) - { - _logger.LogWarning("Required GL extention \"GL_OES_EGL_image\" not found"); - if (!glExtensions.Contains("GL_OES_EGL_image_external")) - { - _logger.LogCritical("Required GL extention \"GL_OES_EGL_image_external\" also not found"); - base.OnOpenGlInit(gl); - return; - } - } - - _eglDisplay = eglGetCurrentDisplay(); - if (_eglDisplay == IntPtr.Zero) - { - _logger.LogCritical("Cannot use EGL, current EGL display is nullptr"); - base.OnOpenGlInit(gl); - return; - } - _logger.LogInformation("EGL display: 0x{:X}", _eglDisplay); - - var eglExtensions = Marshal.PtrToStringAnsi(eglQueryString(_eglDisplay.Value, EglConsts.EGL_EXTENSIONS)); - if (eglExtensions is null) - { - _logger.LogCritical("Cannot use EGL, EGL extentions query returned nullptr"); - base.OnOpenGlInit(gl); - return; - } - _logger.LogInformation("EGL extensions: [\n\t{}\n]", eglExtensions.Trim().Replace(" ", "\n\t")); - - if (!eglExtensions.Contains("EGL_EXT_image_dma_buf_import")) - { - _logger.LogCritical("Cannot use EGL, required EGL extention \"EGL_EXT_image_dma_buf_import\" not found"); - base.OnOpenGlInit(gl); - return; - } - - var pEglCreateImageKHR = eglGetProcAddress("eglCreateImageKHR"); - if (pEglCreateImageKHR == IntPtr.Zero) - { - _logger.LogCritical("Cannot use EGL, required EGL method \"eglCreateImageKHR\" not found"); - base.OnOpenGlInit(gl); - return; - } - _eglCreateImageKHR = Marshal.GetDelegateForFunctionPointer(pEglCreateImageKHR); - - var pEglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR"); - if (pEglDestroyImageKHR == IntPtr.Zero) - { - _logger.LogCritical("Cannot use EGL, required EGL method \"eglDestroyImageKHR\" not found"); - base.OnOpenGlInit(gl); - return; - } - _eglDestroyImageKHR = Marshal.GetDelegateForFunctionPointer(pEglDestroyImageKHR); - - var pGlEGLImageTargetTexture2DOES = eglGetProcAddress("glEGLImageTargetTexture2DOES"); - if (pGlEGLImageTargetTexture2DOES == IntPtr.Zero) - { - _logger.LogCritical("Cannot use EGL, required EGL method \"glEGLImageTargetTexture2DOES\" not found"); - base.OnOpenGlInit(gl); - return; - } - _glEGLImageTargetTexture2DOES = Marshal.GetDelegateForFunctionPointer(pGlEGLImageTargetTexture2DOES); - - _texture = gl.GenTexture(); - gl.BindTexture(GlConsts.GL_TEXTURE_2D, _texture.Value); - gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - 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); - - int vs = gl.CreateShader(GlConsts.GL_VERTEX_SHADER); - gl.ShaderSourceString(vs, VertexShader); - gl.CompileShader(vs); - - int fs = gl.CreateShader(GlConsts.GL_FRAGMENT_SHADER); - gl.ShaderSourceString(fs, FragmentShader); - gl.CompileShader(fs); - - _program = gl.CreateProgram(); - gl.AttachShader(_program.Value, vs); - gl.AttachShader(_program.Value, fs); - gl.LinkProgram(_program.Value); - - gl.DeleteShader(vs); - gl.DeleteShader(fs); - - _uTexLocation = gl.GetUniformLocationString(_program.Value, "uTex"); - - _vao = gl.GenVertexArray(); - _vbo = gl.GenBuffer(); - - gl.BindVertexArray(_vao.Value); - - gl.BindBuffer(GlConsts.GL_ARRAY_BUFFER, _vbo.Value); - fixed (float* pVertices = Vertices) - gl.BufferData(GlConsts.GL_ARRAY_BUFFER, Vertices.Length * sizeof(float), new IntPtr(pVertices), GlConsts.GL_STATIC_DRAW); - - // position - gl.EnableVertexAttribArray(0); - gl.VertexAttribPointer(0, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), IntPtr.Zero); - - // uv - gl.EnableVertexAttribArray(1); - gl.VertexAttribPointer(1, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), 2 * sizeof(float)); - - gl.BindVertexArray(0); - - _isEglInitialized = true; - - } - - protected override void OnOpenGlRender(GlInterface gl, int fb) - { - if (!_isEglInitialized || - _eglDisplay is null || - _eglDisplay.Value == IntPtr.Zero || - _texture is null || - _program is null || - _uTexLocation is null || - _vao is null || - _vbo is null) - { - _logger.LogError("OpenGL render called with uninitialized EGL"); - return; - } - try - { - (LibCameraSharp.FrameBuffer, LibCameraSharp.StreamConfiguration)? frame = null; - do - { - frame = (DataContext as CameraViewModel)?.GrabViewFinderFrame(); - } while (frame is null); - if (frame is null) - return; - var (frameBuffer, streamConfiguration) = frame.Value; - - // 0. Проверка что полученый фрейм ожидаемый праильный формат - var width = streamConfiguration.Size.Width; - if (width <= 0) - { - _logger.LogError("Cannot render frame, stream configuration vaildation error, expected frame width >= 0, got {}", streamConfiguration.Size.Width); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - var height = streamConfiguration.Size.Height; - if (height <= 0) - { - _logger.LogError("Cannot render frame, stream configuration vaildation error, expected frame height >= 0, got {}", streamConfiguration.Size.Height); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - if (streamConfiguration.PixelFormat.Fourcc != DRM_FORMAT_BGRX8888) - { - _logger.LogError("Cannot render frame, stream configuration vaildation error, expected forcc: 0x{:X}, got 0x{:X}", DRM_FORMAT_BGRX8888, streamConfiguration.PixelFormat.Fourcc); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - if (streamConfiguration.PixelFormat.Modifier != 0) - { - _logger.LogError("Cannot render frame, stream configuration vaildation error, expected DRM modifier: 0x{:X}, got 0x{:X}", 0, streamConfiguration.PixelFormat.Modifier); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - if (frameBuffer.Planes.Count() != 1) - { - _logger.LogError("Cannot render frame, planes vaildation error, expected planes count: 0, got {}", frameBuffer.Planes.Count()); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - var plane = frameBuffer.Planes.First(); - if (plane.Length != width * height * 4) - { - _logger.LogError("Cannot render frame, plane validation error, expected plane size 0x{:X}, got 0x{:X}", width * height * 4, plane.Length); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - var fd = plane.Fd.Get(); - if (fd <= 0) - { - _logger.LogError("Cannot render frame, FD vaildation error, FD >= 0, got 0x{:X}", fd); - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - return; - } - - // 1. Очистка экрана - gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); - gl.ClearColor( - Colors.CornflowerBlue.R / 255f, - Colors.CornflowerBlue.G / 255f, - Colors.CornflowerBlue.B / 255f, - Colors.CornflowerBlue.A / 255f - ); - gl.Clear(GlConsts.GL_COLOR_BUFFER_BIT); - - // 2. Создание EGLImage из dma-buf - var eglImage = CreateEglImageFromDmabuf(_eglDisplay.Value, fd, (int)width, (int)height, (int)(width * 4)); - - // 3. Биндинг текстуры - gl.ActiveTexture(GlConsts.GL_TEXTURE0); - gl.BindTexture(GlConsts.GL_TEXTURE_2D, _texture.Value); - - // 4. Привязка EGLImage к текстуре - _glEGLImageTargetTexture2DOES!(GlConsts.GL_TEXTURE_2D, eglImage); - - // 5. Запуск шейдера - gl.UseProgram(_program.Value); - glUniform1i(_uTexLocation.Value, 0); - - // 6. Отрисовка - gl.BindVertexArray(_vao.Value); - gl.DrawArrays(GL_TRIANGLE_STRIP, 0, 4); - - // 7. Уничтожение EGLImage - _eglDestroyImageKHR!(_eglDisplay.Value, eglImage); - - (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception whiel OpenGL render"); - } - } - private IntPtr CreateEglImageFromDmabuf(IntPtr eglDisplay, int fd, int width, int height, int stride) - { - var attribs = new[] - { - EglConsts.EGL_WIDTH, width, - EglConsts.EGL_HEIGHT, height, - EGL_LINUX_DRM_FOURCC_EXT, unchecked((int)DRM_FORMAT_BGRX8888), - EGL_DMA_BUF_PLANE0_FD_EXT, fd, - EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, - EGL_DMA_BUF_PLANE0_PITCH_EXT, stride, - EglConsts.EGL_NONE - }; - - var image = _eglCreateImageKHR?.Invoke(eglDisplay, IntPtr.Zero, EGL_LINUX_DMA_BUF_EXT, IntPtr.Zero, attribs); - - if (image is null || image == IntPtr.Zero) - throw new Exception("eglCreateImageKHR failed"); - - return image.Value; - } -} \ No newline at end of file diff --git a/GSS2.Test/Views/MainView.axaml b/GSS2.Test/Views/MainView.axaml index be1af8b..1e56832 100644 --- a/GSS2.Test/Views/MainView.axaml +++ b/GSS2.Test/Views/MainView.axaml @@ -1,9 +1,9 @@ diff --git a/GSS2.Test/appsettings.json b/GSS2.Test/appsettings.json index abd35d0..8287db5 100644 --- a/GSS2.Test/appsettings.json +++ b/GSS2.Test/appsettings.json @@ -38,9 +38,9 @@ }, "Camera": { "CameraId": "", - "ViewFinderFrameBufferCount": 10, - "ViewFinderWidth": 800, - "ViewFinderHeight": 640, + "ViewFinderFrameBufferCount": 5, + "ViewFinderWidth": 1920, + "ViewFinderHeight": 1080, "ViewFinderFps": 30, "ImageCaptureFrameBufferCount": 2, "ImageCaptureWidth": 4056, diff --git a/GSS2.UI.Core/GSS2.UI.Core.csproj b/GSS2.UI.Core/GSS2.UI.Core.csproj new file mode 100644 index 0000000..f4778a4 --- /dev/null +++ b/GSS2.UI.Core/GSS2.UI.Core.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + true + + + + + + + + + + + + + None + All + + + + + + + + + diff --git a/GSS2.UI.Core/ViewModels/CameraViewModel.cs b/GSS2.UI.Core/ViewModels/CameraViewModel.cs new file mode 100644 index 0000000..514a42f --- /dev/null +++ b/GSS2.UI.Core/ViewModels/CameraViewModel.cs @@ -0,0 +1,67 @@ +using Microsoft.Extensions.Logging; + +using LibCameraSharp; + +using GSS2.Core.Hardware; + +using CommunityToolkit.Mvvm.ComponentModel; + +using Avalonia.Media; + +using LiveChartsCore.Measure; + +namespace GSS2.UI.Core.ViewModels; + +public partial class CameraViewModel : ViewModelBase, IDisposable +{ + private bool _disposedValue; + private readonly CameraService _cameraService; + private readonly ILogger _logger; + private readonly System.Timers.Timer _renderTimer; + + public event EventHandler? RenderReqested; + + [ObservableProperty] private Color _backgroundColor = Colors.Transparent; + [ObservableProperty] private Stretch _stretchMode = Stretch.Uniform; + [ObservableProperty] private TileMode _tileMode = TileMode.None; + + public CameraViewModel(CameraService cameraService, ILogger logger) + { + _cameraService = cameraService; + _logger = logger; + _renderTimer = new System.Timers.Timer(33); + _renderTimer.Elapsed += (_, _) => RenderReqested?.Invoke(this, new EventArgs()); + _logger.LogInformation("{} initialized", nameof(CameraViewModel)); + } + + public void StartViewFinder() + { + _cameraService.StartViewFinder(); + _renderTimer.Start(); + } + public void StopViewFinder() + { + _renderTimer.Stop(); + _cameraService.StopViewFinder(); + } + public (FrameBuffer, StreamConfiguration)? GrabViewFinderFrame() => _cameraService.GrabViewFinderFrame(); + public void ReturnViewFinderFrame(FrameBuffer frameBuffer) => _cameraService.ReturnViewFinderFrame(frameBuffer); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _renderTimer.Stop(); + _renderTimer.Dispose(); + } + _disposedValue = true; + } + } + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/GSS2.Test/ViewModels/ViewModelBase.cs b/GSS2.UI.Core/ViewModels/ViewModelBase.cs similarity index 74% rename from GSS2.Test/ViewModels/ViewModelBase.cs rename to GSS2.UI.Core/ViewModels/ViewModelBase.cs index 56c194a..36943b5 100644 --- a/GSS2.Test/ViewModels/ViewModelBase.cs +++ b/GSS2.UI.Core/ViewModels/ViewModelBase.cs @@ -1,6 +1,6 @@ using CommunityToolkit.Mvvm.ComponentModel; -namespace GSS2.Test.ViewModels; +namespace GSS2.UI.Core.ViewModels; public abstract class ViewModelBase : ObservableObject { } \ No newline at end of file diff --git a/GSS2.Test/Views/CameraView.axaml b/GSS2.UI.Core/Views/CameraView.axaml similarity index 62% rename from GSS2.Test/Views/CameraView.axaml rename to GSS2.UI.Core/Views/CameraView.axaml index 2de6355..43798c4 100644 --- a/GSS2.Test/Views/CameraView.axaml +++ b/GSS2.UI.Core/Views/CameraView.axaml @@ -2,7 +2,9 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:vm="using:GSS2.Test.ViewModels" + xmlns:vm="using:GSS2.UI.Core.ViewModels" x:DataType="vm:CameraViewModel" - x:Class="GSS2.Test.Views.CameraView"> + x:Class="GSS2.UI.Core.Views.CameraView" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch"> diff --git a/GSS2.UI.Core/Views/CameraView.axaml.cs b/GSS2.UI.Core/Views/CameraView.axaml.cs new file mode 100644 index 0000000..6fe2b2e --- /dev/null +++ b/GSS2.UI.Core/Views/CameraView.axaml.cs @@ -0,0 +1,614 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +using Microsoft.Extensions.Logging; + +using Avalonia; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.OpenGL.Egl; +using Avalonia.OpenGL.Controls; +using Avalonia.OpenGL; +using Avalonia.Threading; +using Avalonia.Input; + +using GSS2.UI.Core.ViewModels; +using System.ComponentModel; + +namespace GSS2.UI.Core.Views; + +public partial class CameraView : OpenGlControlBase +{ + private class OpenGLContext + { + [DllImport("libEGL.so.1")] public static extern IntPtr eglQueryString(IntPtr dpy, int name); + [DllImport("libEGL.so.1")] public static extern IntPtr eglGetCurrentDisplay(); + [DllImport("libEGL.so.1")] public static extern IntPtr eglGetProcAddress(string procname); + [DllImport("libGL.so.1")] public static extern void glUniform1i(int location, int falue); + [DllImport("libGL.so.1")] public static extern void glUniformMatrix3x2fv(int location, int count, bool transpose, IntPtr value); + [DllImport("libGL.so.1")] public static extern void glUniformMatrix3fv(int location, int count, bool transpose, IntPtr value); + + public const uint DRM_FORMAT_BGRX8888 = 0x34325258; // XR24 XRGB8888 + public const int GL_TRIANGLE_STRIP = 0x0005; + public const int GL_TEXTURE_WRAP_S = 0x2802; + public const int GL_TEXTURE_WRAP_T = 0x2803; + public const int GL_CLAMP_TO_EDGE = 0x812F; + public const int EGL_LINUX_DMA_BUF_EXT = 0x3270; + public const int EGL_LINUX_DRM_FOURCC_EXT = 0x3271; + public const int EGL_DMA_BUF_PLANE0_FD_EXT = 0x3272; + public const int EGL_DMA_BUF_PLANE0_OFFSET_EXT = 0x3273; + public const int EGL_DMA_BUF_PLANE0_PITCH_EXT = 0x3274; + + public delegate IntPtr GlEGLImageTargetTexture2DOESDeligate(int target, IntPtr image); + public delegate IntPtr EglCreateImageKHRDeligate(IntPtr dpy, IntPtr ctx, int target, IntPtr buffer, int[] attribs); + public delegate bool EglDestroyImageKHRDeligate(IntPtr dpy, IntPtr image); + + private const string VERTEX_SHADER = @" + #version 300 es + layout (location = 0) in vec2 aPos; + layout (location = 1) in vec2 aUv; + + uniform mat4 uMvp; + uniform mat3 uUv; + + out vec2 vUv; + + void main() + { + vec3 uv = uUv * vec3(aUv, 1.0); + vUv = uv.xy; + gl_Position = uMvp * vec4(aPos, 0.0, 1.0); + } + "; + private const string FRAGMENT_SHADER = @" + #version 300 es + precision mediump float; + + in vec2 vUv; + uniform sampler2D uTex; + + out vec4 fragColor; + + void main() + { + fragColor = texture(uTex, vUv); + } + "; + private readonly float[] Vertices = + [ + -1f, -1f, 0f, 1f, + 1f, -1f, 1f, 1f, + -1f, 1f, 0f, 0f, + 1f, 1f, 1f, 0f, + ]; + + public IntPtr EglDisplay { get; } + public int Texture { get; } + public int Program { get; } + public int UTexLocation { get; } + public int UMvpLocation { get; } + public int UUvLocation { get; } + public int VAO { get; } + public int VBO { get; } + public EglCreateImageKHRDeligate EglCreateImageKHR { get; } + public EglDestroyImageKHRDeligate EglDestroyImageKHR { get; } + public GlEGLImageTargetTexture2DOESDeligate GlEGLImageTargetTexture2DOES { get; } + + public OpenGLContext(GlInterface gl) + { + var glExtensions = gl.GetString(GlConsts.GL_EXTENSIONS); + if (glExtensions is null) + throw new Exception("GL extentions query returned null string"); + + if (!glExtensions.Contains("GL_OES_EGL_image") && + !glExtensions.Contains("GL_OES_EGL_image_external")) + throw new Exception("Required GL extentions \"GL_OES_EGL_image_external\" or \"GL_OES_EGL_image\" not found"); + + EglDisplay = eglGetCurrentDisplay(); + if (EglDisplay == IntPtr.Zero) + throw new Exception("EGL display is nullptr"); + + var eglExtensions = Marshal.PtrToStringAnsi(eglQueryString(EglDisplay, EglConsts.EGL_EXTENSIONS)); + if (eglExtensions is null) + throw new Exception("EGL extentions query returned null"); + + if (!eglExtensions.Contains("EGL_EXT_image_dma_buf_import")) + throw new Exception("Required EGL extention \"EGL_EXT_image_dma_buf_import\" not found"); + + var pEglCreateImageKHR = eglGetProcAddress("eglCreateImageKHR"); + if (pEglCreateImageKHR == IntPtr.Zero) + throw new Exception("Required EGL method \"eglCreateImageKHR\" not found"); + EglCreateImageKHR = Marshal.GetDelegateForFunctionPointer(pEglCreateImageKHR); + + var pEglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR"); + if (pEglDestroyImageKHR == IntPtr.Zero) + throw new Exception("Required EGL method \"eglDestroyImageKHR\" not found"); + EglDestroyImageKHR = Marshal.GetDelegateForFunctionPointer(pEglDestroyImageKHR); + + var pGlEGLImageTargetTexture2DOES = eglGetProcAddress("glEGLImageTargetTexture2DOES"); + if (pGlEGLImageTargetTexture2DOES == IntPtr.Zero) + throw new Exception("Required EGL method \"glEGLImageTargetTexture2DOES\" not found"); + GlEGLImageTargetTexture2DOES = Marshal.GetDelegateForFunctionPointer(pGlEGLImageTargetTexture2DOES); + + Texture = gl.GenTexture(); ThrowIfOpenGLError(gl); + gl.BindTexture(GlConsts.GL_TEXTURE_2D, Texture); ThrowIfOpenGLError(gl); + gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl); + gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); ThrowIfOpenGLError(gl); + + gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MIN_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl); + gl.TexParameteri(GlConsts.GL_TEXTURE_2D, GlConsts.GL_TEXTURE_MAG_FILTER, GlConsts.GL_LINEAR); ThrowIfOpenGLError(gl); + + int vertexShader = gl.CreateShader(GlConsts.GL_VERTEX_SHADER); ThrowIfOpenGLError(gl); + gl.ShaderSourceString(vertexShader, VERTEX_SHADER); ThrowIfOpenGLError(gl); + gl.CompileShader(vertexShader); + + int fragmentShader = gl.CreateShader(GlConsts.GL_FRAGMENT_SHADER); ThrowIfOpenGLError(gl); + gl.ShaderSourceString(fragmentShader, FRAGMENT_SHADER); ThrowIfOpenGLError(gl); + gl.CompileShader(fragmentShader); ThrowIfOpenGLError(gl); + + Program = gl.CreateProgram(); ThrowIfOpenGLError(gl); + gl.AttachShader(Program, vertexShader); ThrowIfOpenGLError(gl); + gl.AttachShader(Program, fragmentShader); ThrowIfOpenGLError(gl); + gl.LinkProgram(Program); ThrowIfOpenGLError(gl); + + gl.DeleteShader(vertexShader); ThrowIfOpenGLError(gl); + gl.DeleteShader(fragmentShader); ThrowIfOpenGLError(gl); + + UTexLocation = gl.GetUniformLocationString(Program, "uTex"); ThrowIfOpenGLError(gl); + UMvpLocation = gl.GetUniformLocationString(Program, "uMvp"); ThrowIfOpenGLError(gl); + UUvLocation = gl.GetUniformLocationString(Program, "uUv"); ThrowIfOpenGLError(gl); + + VAO = gl.GenVertexArray(); ThrowIfOpenGLError(gl); + VBO = gl.GenBuffer(); ThrowIfOpenGLError(gl); + + gl.BindVertexArray(VAO); ThrowIfOpenGLError(gl); + + gl.BindBuffer(GlConsts.GL_ARRAY_BUFFER, VAO); ThrowIfOpenGLError(gl); + unsafe + { + fixed (float* pVertices = Vertices) + gl.BufferData(GlConsts.GL_ARRAY_BUFFER, Vertices.Length * sizeof(float), new IntPtr(pVertices), GlConsts.GL_STATIC_DRAW); ThrowIfOpenGLError(gl); + } + + gl.EnableVertexAttribArray(0); ThrowIfOpenGLError(gl); + gl.VertexAttribPointer(0, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), IntPtr.Zero); ThrowIfOpenGLError(gl); + + gl.EnableVertexAttribArray(1); ThrowIfOpenGLError(gl); + gl.VertexAttribPointer(1, 2, GlConsts.GL_FLOAT, 0, 4 * sizeof(float), 2 * sizeof(float)); ThrowIfOpenGLError(gl); + + gl.BindVertexArray(0); ThrowIfOpenGLError(gl); + } + + private void ThrowIfOpenGLError(GlInterface gl) + { + var error = gl.GetError(); + if (error != 0) + throw new Exception($"OpenGL context initialization error: {error}"); + } + } + + private readonly ILogger _logger; + private readonly Dictionary _importedDmaBufers = new Dictionary(); + private bool _isViewFinderStarted = false; + private OpenGLContext? _openGLContext = null; + private Point? _lastPosition = null; + private float _zoom = 1; + private float _panX = 0; + private float _panY = 0; + + public CameraView(ILogger logger) + { + _logger = logger; + _logger.LogInformation("{} initialized", nameof(CameraView)); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs ea) + { + if (DataContext is CameraViewModel viewModel) + { + try + { + viewModel.StartViewFinder(); + viewModel.RenderReqested += RenderReqested; + _isViewFinderStarted = true; + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Exception while view finder start"); + } + } + else + _logger.LogCritical("Cannot start view finder, data context is null or not camera view model"); + + base.OnAttachedToVisualTree(ea); + } + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs ea) + { + if (_isViewFinderStarted && DataContext is CameraViewModel viewModel) + { + try + { + viewModel.StopViewFinder(); + foreach (var (fd, image) in _importedDmaBufers) + _openGLContext?.EglDestroyImageKHR!(_openGLContext.EglDisplay, image); + _isViewFinderStarted = false; + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Exception while view finder stop"); + } + viewModel.RenderReqested -= RenderReqested; + } + else if (_isViewFinderStarted) + _logger.LogCritical("Cannot stop view finder, data context is null or not camera view model"); + + _isViewFinderStarted = false; + base.OnDetachedFromLogicalTree(ea); + } + + protected override void OnPointerWheelChanged(PointerWheelEventArgs ea) + { + if (ea.Pointer.Type == PointerType.Mouse) + { + var position = ea.GetPosition(this); + + // Рассчитываем позицию в координатах изображения (до масштабирования) + var imageX = (position.X - Bounds.Width / 2 - _panX) / _zoom; + var imageY = (-position.Y + Bounds.Height / 2 - _panY) / _zoom; // Ось Y инвертированна + + // Применяем масштаб + float zoomFactor = ea.Delta.Y > 0 ? 1.1f : 1 / 1.1f; + _zoom *= zoomFactor; + _zoom = Math.Clamp(_zoom, 1f, 10f); + _zoom = (float)Math.Round(_zoom, 2); + + if (_zoom != 1f) + { + // Пересчитываем смещение так, чтобы указатель остался на месте + _panX = (float)(position.X - Bounds.Width / 2 - imageX * _zoom); + _panY = (float)(-position.Y + Bounds.Height / 2 - imageY * _zoom); // Ось Y инвертированна + } + else + { + // Если зум == 1, то возвращаем изображение к исходному смещению + _panX = 0f; + _panY = 0f; + } + } + else if (ea.Pointer.Type == PointerType.Touch) + { + _logger.LogInformation("PointerWheelChanged Type Touch {} {}", ea.GetPosition(this), ea.Delta); + } + base.OnPointerWheelChanged(ea); + } + protected override void OnPointerPressed(PointerPressedEventArgs ea) + { + if (ea.Pointer.Type == PointerType.Mouse) + { + if (ea.Properties.IsLeftButtonPressed) + { + if (ea.ClickCount == 2) // По двойному клику возвращаем изображение к смещение и зум к стандартным значениям + { + _zoom = 1f; + _panX = 0f; + _panY = 0f; + } + else + _lastPosition = ea.GetPosition(this); + } + } + else if (ea.Pointer.Type == PointerType.Touch) + { + _logger.LogInformation("PointerPressed Type Touch {}", ea.GetPosition(this)); + } + base.OnPointerPressed(ea); + } + protected override void OnPointerReleased(PointerReleasedEventArgs ea) + { + if (ea.Pointer.Type == PointerType.Mouse) + { + if (!ea.Properties.IsLeftButtonPressed) + _lastPosition = null; + } + else if (ea.Pointer.Type == PointerType.Touch) + { + _logger.LogInformation("PointerReleased Type Touch {}", ea.GetPosition(this)); + } + base.OnPointerReleased(ea); + } + protected override void OnPointerMoved(PointerEventArgs ea) + { + if (ea.Pointer.Type == PointerType.Mouse) + { + if (_lastPosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем + { + base.OnPointerMoved(ea); + return; + } + var position = ea.GetPosition(this); + _panX -= (float)(_lastPosition.Value.X - position.X); + _panY += (float)(_lastPosition.Value.Y - position.Y); // Ось Y инвертированна + _lastPosition = position; + base.OnPointerMoved(ea); + } + else if (ea.Pointer.Type == PointerType.Touch) + { + _logger.LogInformation("PointerMoved Type Touch {}", ea.GetPosition(this)); + } + } + + public override void Render(DrawingContext context) + { + var backgroundColor = (DataContext as CameraViewModel)?.BackgroundColor ?? Colors.Transparent; + context.FillRectangle(new SolidColorBrush(backgroundColor), new Rect(0, 0, Bounds.Width, Bounds.Height)); + base.Render(context); + } + private void RenderReqested(object? sender, EventArgs ea) => Dispatcher.UIThread.Invoke(() => RequestNextFrameRendering()); + + protected override void OnOpenGlInit(GlInterface gl) + { + _logger.LogInformation("OpenGL initialized"); + _logger.LogInformation("GL version: {}", gl.GetString(GlConsts.GL_VERSION)); + _logger.LogInformation("GL vendor: {}", gl.GetString(GlConsts.GL_VENDOR)); + _logger.LogInformation("GL renderer: {}", gl.GetString(GlConsts.GL_RENDERER)); + try + { + _openGLContext = new OpenGLContext(gl); + _logger.LogInformation("OpenGL context initialized"); + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Error while OpenGL initialization"); + } + base.OnOpenGlInit(gl); + } + + protected override void OnOpenGlRender(GlInterface gl, int fb) + { + if (_openGLContext is null) + { + _logger.LogError("OpenGL render called with uninitialized EGL"); + return; + } + try + { + (LibCameraSharp.FrameBuffer, LibCameraSharp.StreamConfiguration)? frame = null; + while (frame is null) + frame = (DataContext as CameraViewModel)?.GrabViewFinderFrame(); + var (frameBuffer, streamConfiguration) = frame.Value; + + // 0. Проверяем что полученый фрейм ожидаемый праильный формат + var width = streamConfiguration.Size.Width; + if (width <= 0) + { + _logger.LogError("Cannot render frame, stream configuration vaildation error, expected frame width >= 0, got {}", streamConfiguration.Size.Width); + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + return; + } + var height = streamConfiguration.Size.Height; + if (height <= 0) + { + _logger.LogError("Cannot render frame, stream configuration vaildation error, expected frame height >= 0, got {}", streamConfiguration.Size.Height); + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + return; + } + if (streamConfiguration.PixelFormat.Fourcc != OpenGLContext.DRM_FORMAT_BGRX8888) + { + _logger.LogError("Cannot render frame, stream configuration vaildation error, expected forcc: 0x{:X}, got 0x{:X}", OpenGLContext.DRM_FORMAT_BGRX8888, streamConfiguration.PixelFormat.Fourcc); + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + return; + } + if (streamConfiguration.PixelFormat.Modifier != 0) + { + _logger.LogError("Cannot render frame, stream configuration vaildation error, expected DRM modifier: 0x{:X}, got 0x{:X}", 0, streamConfiguration.PixelFormat.Modifier); + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + return; + } + if (frameBuffer.Planes.Count() != 1) + { + _logger.LogError("Cannot render frame, planes vaildation error, expected planes count: 0, got {}", frameBuffer.Planes.Count()); + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + return; + } + var plane = frameBuffer.Planes.First(); + if (plane.Length != width * height * 4) + { + _logger.LogError("Cannot render frame, plane validation error, expected plane size 0x{:X}, got 0x{:X}", width * height * 4, plane.Length); + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + return; + } + var fd = plane.Fd.Get(); + if (fd <= 0) + { + _logger.LogError("Cannot render frame, FD vaildation error, FD >= 0, got 0x{:X}", fd); + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + return; + } + + var stretchMode = (DataContext as CameraViewModel)?.StretchMode ?? Stretch.Uniform; + var tileMode = (DataContext as CameraViewModel)?.TileMode ?? TileMode.None; + + // 1. Очищаем экран + gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); + gl.ClearColor(0f, 0f, 0f, 0f); + gl.Clear(GlConsts.GL_COLOR_BUFFER_BIT); + + // 2. Создаём EGL изображение из dma-buf + IntPtr image; + if (_importedDmaBufers.ContainsKey(fd)) + { + // 2.1. Если EGL изображение для fd уже создано, то берём его из словаря + image = _importedDmaBufers[fd]; + } + else + { + // 2.2. Если EGL изображение для fd не создано, то создаём + var attribs = new[] + { + EglConsts.EGL_WIDTH, (int)width, + EglConsts.EGL_HEIGHT, (int)height, + OpenGLContext.EGL_LINUX_DRM_FOURCC_EXT, unchecked((int)OpenGLContext.DRM_FORMAT_BGRX8888), + OpenGLContext.EGL_DMA_BUF_PLANE0_FD_EXT, fd, + OpenGLContext.EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, + OpenGLContext.EGL_DMA_BUF_PLANE0_PITCH_EXT, (int)(width * 4), + EglConsts.EGL_NONE + }; + image = _openGLContext.EglCreateImageKHR.Invoke(_openGLContext.EglDisplay, IntPtr.Zero, OpenGLContext.EGL_LINUX_DMA_BUF_EXT, IntPtr.Zero, attribs); + _importedDmaBufers.Add(fd, image); + } + + // 3. Биндим текстуры текстуры + gl.ActiveTexture(GlConsts.GL_TEXTURE0); + gl.BindTexture(GlConsts.GL_TEXTURE_2D, _openGLContext.Texture); + + // 4. Привязываем EGL изображение к текстуре + _openGLContext.GlEGLImageTargetTexture2DOES!(GlConsts.GL_TEXTURE_2D, image); + + // 5. Вычисляем матрицы транформаций + var mvp = ComputeMvp(stretchMode, width, height); + var zoom = Matrix4x4.CreateScale(_zoom, _zoom, 1f); + var translate = Matrix4x4.CreateTranslation((float)(_panX / Bounds.Width * 2), (float)(_panY / Bounds.Height * 2), 0); + mvp *= zoom; + mvp *= translate; + var mvpArray = new[] + { + mvp.M11, mvp.M12, mvp.M13, mvp.M14, + mvp.M21, mvp.M22, mvp.M23, mvp.M24, + mvp.M31, mvp.M32, mvp.M33, mvp.M34, + mvp.M41, mvp.M42, mvp.M43, mvp.M44, + }; + unsafe + { + fixed (float* pMvp = mvpArray) + gl.UniformMatrix4fv(_openGLContext.UMvpLocation, 1, false, pMvp); + } + + var uv = ComputeUv(tileMode); + var uvArray = new[] + { + uv.M11, uv.M12, 0f, + uv.M21, uv.M22, 0f, + uv.M31, uv.M32, 1f + }; + unsafe + { + fixed (float* pUv = uvArray) + OpenGLContext.glUniformMatrix3fv(_openGLContext.UUvLocation, 1, false, new IntPtr(pUv)); + } + + // 6. Запускаем шейдер + gl.UseProgram(_openGLContext.Program); + OpenGLContext.glUniform1i(_openGLContext.UTexLocation, 0); + + // 7. Рисуем вершины на экране + gl.BindVertexArray(_openGLContext.VAO); + gl.DrawArrays(OpenGLContext.GL_TRIANGLE_STRIP, 0, 4); + + // 8. Уничтожаем EGL изображение + // _openGLContext.EglDestroyImageKHR!(_openGLContext.EglDisplay, image); + + (DataContext as CameraViewModel)?.ReturnViewFinderFrame(frameBuffer); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception whiel OpenGL render"); + } + } + private Matrix4x4 ComputeMvp(Stretch stretch, float imageWidth, float imageHeight) + { + float viewWidth = (float)Bounds.Width; + float viewHeight = (float)Bounds.Height; + + float sx = 1f; + float sy = 1f; + + float viewAspect = viewWidth / viewHeight; + float imgAspect = imageWidth / imageHeight; + + switch (stretch) + { + case Stretch.None: + sx = imageWidth / viewWidth; + sy = imageHeight / viewHeight; + break; + + case Stretch.Fill: + break; + + case Stretch.Uniform: + if (imgAspect > viewAspect) + sy = viewAspect / imgAspect; + else + sx = imgAspect / viewAspect; + break; + + case Stretch.UniformToFill: + if (imgAspect > viewAspect) + sx = imgAspect / viewAspect; + else + sy = viewAspect / imgAspect; + break; + } + + return Matrix4x4.CreateScale(sx, sy, 1f); + } + private Matrix3x2 ComputeUv(TileMode mode) + { + float sx = 1f; + float sy = 1f; + float ox = 0f; + float oy = 0f; + + switch (mode) + { + case TileMode.Tile: + sx = sy = 2f; + break; + + case TileMode.FlipX: + sx = -1f; + ox = 1f; + break; + + case TileMode.FlipY: + sy = -1f; + oy = 1f; + break; + + case TileMode.FlipXY: + sx = sy = -1f; + ox = oy = 1f; + break; + } + + return new Matrix3x2( + sx, 0, + 0, sy, + ox, oy + ); + } + private (float sx, float sy) ComputeScale(Stretch mode, float imageWidth, float imageHeight) + { + float viewWidth = (float)Bounds.Width; + float viewHeight = (float)Bounds.Height; + + float scaleX = viewWidth / imageWidth; + float scaleY = viewHeight / imageHeight; + + switch (mode) + { + case Stretch.None: + return (imageWidth / viewWidth, imageHeight / viewHeight); + case Stretch.Fill: + return (1f, 1f); + case Stretch.Uniform: + var sMin = MathF.Min(scaleX, scaleY); + return (imageWidth * sMin / viewWidth, imageHeight * sMin / viewHeight); + case Stretch.UniformToFill: + var sMax = MathF.Max(scaleX, scaleY); + return (imageWidth * sMax / viewWidth, imageHeight * sMax / viewHeight); + default: + return (1f, 1f); + } + } +} \ No newline at end of file diff --git a/GranuSightSoftware2.sln b/GranuSightSoftware2.sln index 24bf55b..89dae76 100644 --- a/GranuSightSoftware2.sln +++ b/GranuSightSoftware2.sln @@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.LightsControl.CSharpCl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.FrameDecoders", "GSS2.FrameDecoders\GSS2.FrameDecoders.csproj", "{05085293-E710-41D6-8F61-701ADBA52FA9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.UI.Core", "GSS2.UI.Core\GSS2.UI.Core.csproj", "{52424CF4-88F4-440A-AA37-21F5A080AA16}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -73,6 +75,18 @@ Global {05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x64.Build.0 = Release|Any CPU {05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x86.ActiveCfg = Release|Any CPU {05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x86.Build.0 = Release|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Debug|x64.ActiveCfg = Debug|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Debug|x64.Build.0 = Debug|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Debug|x86.ActiveCfg = Debug|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Debug|x86.Build.0 = Debug|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|Any CPU.Build.0 = Release|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x64.ActiveCfg = Release|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x64.Build.0 = Release|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x86.ActiveCfg = Release|Any CPU + {52424CF4-88F4-440A-AA37-21F5A080AA16}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE