forked from amkovkov/GranuSightSoftware2
refactor: move ui core
move ui core classes into separate project refactor CameraView
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
using GSS2.UI.Core.Views;
|
||||||
|
using GSS2.UI.Core.ViewModels;
|
||||||
using GSS2.Test.Views;
|
using GSS2.Test.Views;
|
||||||
using GSS2.Test.ViewModels;
|
using GSS2.Test.ViewModels;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.Templates;
|
using Avalonia.Controls.Templates;
|
||||||
|
|
||||||
using GSS2.Test.ViewModels;
|
using GSS2.UI.Core.ViewModels;
|
||||||
|
|
||||||
namespace GSS2.Test;
|
namespace GSS2.Test;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ public class DependencyInjectionViewLocator : IDataTemplate
|
|||||||
if (data is null)
|
if (data is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var name = data.GetType().FullName?
|
var name = data.GetType().AssemblyQualifiedName?
|
||||||
.Replace("ViewModels", "Views")
|
.Replace("ViewModels", "Views")
|
||||||
.Replace("ViewModel", "View");
|
.Replace("ViewModel", "View");
|
||||||
if (name is null)
|
if (name is null)
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<UserSecretsId>dotnet-GSS2.Test-191ece0a-855d-46ec-9ecb-44378fc3db83</UserSecretsId>
|
<UserSecretsId>dotnet-GSS2.Test-191ece0a-855d-46ec-9ecb-44378fc3db83</UserSecretsId>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -20,20 +19,15 @@
|
|||||||
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.11" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.11" />
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11">
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11">
|
||||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||||
<PackageReference Include="Silk.NET.Direct3D11" Version="2.22.0" />
|
|
||||||
<PackageReference Include="Silk.NET.Direct3D.Compilers" Version="2.22.0" />
|
|
||||||
<PackageReference Include="Silk.NET.Vulkan" Version="2.22.0" />
|
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" />
|
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\GSS2.Core\GSS2.Core.csproj" />
|
<ProjectReference Include="..\GSS2.Core\GSS2.Core.csproj" />
|
||||||
|
<ProjectReference Include="..\GSS2.UI.Core\GSS2.UI.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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<CameraViewModel> _logger;
|
|
||||||
|
|
||||||
public CameraViewModel(CameraService cameraService, ILogger<CameraViewModel> 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);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
using GSS2.UI.Core.ViewModels;
|
||||||
|
|
||||||
namespace GSS2.Test.ViewModels;
|
namespace GSS2.Test.ViewModels;
|
||||||
|
|
||||||
public partial class MainViewModel : ViewModelBase
|
public partial class MainViewModel : ViewModelBase
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
using GSS2.UI.Core.ViewModels;
|
||||||
|
|
||||||
namespace GSS2.Test.ViewModels;
|
namespace GSS2.Test.ViewModels;
|
||||||
|
|
||||||
public partial class MainWindowViewModel : ViewModelBase
|
public partial class MainWindowViewModel : ViewModelBase
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
using LiveChartsCore;
|
using LiveChartsCore;
|
||||||
using LiveChartsCore.SkiaSharpView;
|
using LiveChartsCore.SkiaSharpView;
|
||||||
using LiveChartsCore.SkiaSharpView.Painting;
|
using LiveChartsCore.SkiaSharpView.Painting;
|
||||||
|
using LiveChartsCore.Defaults;
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
using GSS2.Core.Hardware;
|
using GSS2.Core.Hardware;
|
||||||
using LiveChartsCore.SkiaSharpView.Avalonia;
|
using GSS2.UI.Core.ViewModels;
|
||||||
using LiveChartsCore.Defaults;
|
|
||||||
|
|
||||||
namespace GSS2.Test.ViewModels;
|
namespace GSS2.Test.ViewModels;
|
||||||
|
|
||||||
|
|||||||
@@ -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<CameraView> _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<CameraView> 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<eglCreateImageKHR_t>(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<eglDestroyImageKHR_t>(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<glEGLImageTargetTexture2DOES_t>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<UserControl xmlns="https://github.com/avaloniaui"
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="using:GSS2.Test.ViewModels"
|
xmlns:local_vm="using:GSS2.Test.ViewModels"
|
||||||
xmlns:local="using:GSS2.Test.Views"
|
xmlns:core_vm="using:GSS2.UI.Core.ViewModels"
|
||||||
x:DataType="vm:MainViewModel"
|
x:DataType="local_vm:MainViewModel"
|
||||||
x:Class="GSS2.Test.Views.MainView">
|
x:Class="GSS2.Test.Views.MainView">
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
<ContentControl Content="{Binding CameraViewModel}" Width="200" Height="200"/>
|
<ContentControl Content="{Binding CameraViewModel}" Width="200" Height="200"/>
|
||||||
|
|||||||
@@ -38,9 +38,9 @@
|
|||||||
},
|
},
|
||||||
"Camera": {
|
"Camera": {
|
||||||
"CameraId": "",
|
"CameraId": "",
|
||||||
"ViewFinderFrameBufferCount": 10,
|
"ViewFinderFrameBufferCount": 5,
|
||||||
"ViewFinderWidth": 800,
|
"ViewFinderWidth": 1920,
|
||||||
"ViewFinderHeight": 640,
|
"ViewFinderHeight": 1080,
|
||||||
"ViewFinderFps": 30,
|
"ViewFinderFps": 30,
|
||||||
"ImageCaptureFrameBufferCount": 2,
|
"ImageCaptureFrameBufferCount": 2,
|
||||||
"ImageCaptureWidth": 4056,
|
"ImageCaptureWidth": 4056,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Avalonia.LinuxFramebuffer" Version="11.3.11" />
|
||||||
|
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-rc6.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Avalonia" Version="11.3.11" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.11" />
|
||||||
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.11" />
|
||||||
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.11" />
|
||||||
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.3.11">
|
||||||
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GSS2.Core\GSS2.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -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<CameraViewModel> _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<CameraViewModel> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
namespace GSS2.Test.ViewModels;
|
namespace GSS2.UI.Core.ViewModels;
|
||||||
|
|
||||||
public abstract class ViewModelBase : ObservableObject
|
public abstract class ViewModelBase : ObservableObject
|
||||||
{ }
|
{ }
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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:DataType="vm:CameraViewModel"
|
||||||
x:Class="GSS2.Test.Views.CameraView">
|
x:Class="GSS2.UI.Core.Views.CameraView"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch">
|
||||||
</Control>
|
</Control>
|
||||||
@@ -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<EglCreateImageKHRDeligate>(pEglCreateImageKHR);
|
||||||
|
|
||||||
|
var pEglDestroyImageKHR = eglGetProcAddress("eglDestroyImageKHR");
|
||||||
|
if (pEglDestroyImageKHR == IntPtr.Zero)
|
||||||
|
throw new Exception("Required EGL method \"eglDestroyImageKHR\" not found");
|
||||||
|
EglDestroyImageKHR = Marshal.GetDelegateForFunctionPointer<EglDestroyImageKHRDeligate>(pEglDestroyImageKHR);
|
||||||
|
|
||||||
|
var pGlEGLImageTargetTexture2DOES = eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||||
|
if (pGlEGLImageTargetTexture2DOES == IntPtr.Zero)
|
||||||
|
throw new Exception("Required EGL method \"glEGLImageTargetTexture2DOES\" not found");
|
||||||
|
GlEGLImageTargetTexture2DOES = Marshal.GetDelegateForFunctionPointer<GlEGLImageTargetTexture2DOESDeligate>(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<CameraView> _logger;
|
||||||
|
private readonly Dictionary<int, IntPtr> _importedDmaBufers = new Dictionary<int, nint>();
|
||||||
|
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<CameraView> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.LightsControl.CSharpCl
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.FrameDecoders", "GSS2.FrameDecoders\GSS2.FrameDecoders.csproj", "{05085293-E710-41D6-8F61-701ADBA52FA9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.FrameDecoders", "GSS2.FrameDecoders\GSS2.FrameDecoders.csproj", "{05085293-E710-41D6-8F61-701ADBA52FA9}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GSS2.UI.Core", "GSS2.UI.Core\GSS2.UI.Core.csproj", "{52424CF4-88F4-440A-AA37-21F5A080AA16}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{05085293-E710-41D6-8F61-701ADBA52FA9}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user