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