diff --git a/GSS2.Core/Hardware/CameraService.cs b/GSS2.Core/Hardware/CameraService.cs
index 7ced96f..7780fc9 100644
--- a/GSS2.Core/Hardware/CameraService.cs
+++ b/GSS2.Core/Hardware/CameraService.cs
@@ -357,6 +357,11 @@ public class CameraService : IDisposable
_logger.LogDebug("Request complited: \n\tResult: {} \n\tStatus: {}\n\tCookie: {}\n\tSequence: {}\n\tHasPendingBuffers: {}", result, request.Status, request.Cookie, request.Sequence, request.HasPendingBuffers);
_logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp);
+ _logger.LogDebug("Request buffer planes: ");
+ foreach (var (i, plane) in buffer.Metadata.Planes.Enumerate())
+ _logger.LogDebug("Plane[{}]: bytes used: 0x{:X}", i, plane.BytesUsed);
+ foreach (var (i, plane) in buffer.Planes.Enumerate())
+ _logger.LogDebug("Plane[{}]: \n\tFd: 0x{:X} \n\tOffset: 0x{:X} \n\tLength: 0x{:X} \n\tKInvalidOffset: 0x{:X}", i, plane.Fd.Get(), plane.Offset, plane.Length, plane.KInvalidOffset);
}
}
private void ImageCaptureFrameBuffersInitialize(CancellationToken cancellationToken)
@@ -401,6 +406,10 @@ public class CameraService : IDisposable
_logger.LogDebug("Request complited: \n\tResult: {} \n\tStatus: {}\n\tCookie: {}\n\tSequence: {}\n\tHasPendingBuffers: {}", result, request.Status, request.Cookie, request.Sequence, request.HasPendingBuffers);
_logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp);
+ foreach (var (i, plane) in buffer.Metadata.Planes.Enumerate())
+ _logger.LogDebug("Plane[{}]: bytes used: 0x{:X}", i, plane.BytesUsed);
+ foreach (var (i, plane) in buffer.Planes.Enumerate())
+ _logger.LogDebug("Plane[{}]: \n\tFd: 0x{:X} \n\tOffset: 0x{:X} \n\tLength: 0x{:X} \n\tKInvalidOffset: 0x{:X}", i, plane.Fd.Get(), plane.Offset, plane.Length, plane.KInvalidOffset);
}
}
private void ImageCaptureFrameBuffersMap(CancellationToken cancellationToken)
@@ -649,6 +658,91 @@ public class CameraService : IDisposable
return image;
}
+ public async Task<(FrameBuffer, StreamConfiguration)?> CaptureViewFinderBuffer(CancellationToken cancellationToken)
+ {
+ if (_camera is null)
+ throw new InvalidOperationException("Camera not initialized");
+ if (_viewFinderStream is null)
+ throw new InvalidOperationException("ViewFinderStream not initialized");
+ if (_viewFinderStreamConfiguration is null)
+ throw new InvalidOperationException("ViewFinderStreamConfiguration not initialized");
+ if (_viewFinderFrameBufferAllocator is null)
+ throw new InvalidOperationException("ViewFinderFrameBufferAllocator not initialized");
+
+ var effectiveCancellationToken = CancellationTokenSource
+ .CreateLinkedTokenSource(_disposeCts.Token, cancellationToken)
+ .Token;
+
+ var buffers = _viewFinderFrameBufferAllocator.Buffers(_viewFinderStream);
+ var buffer = buffers.First();
+
+ var request = _camera.CreateRequest(checked((ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()));
+ var result = request.AddBuffer(_viewFinderStream, buffer);
+ if (result < 0)
+ throw new Exception("Error while request create");
+ _logger.LogDebug("Request(s) created");
+
+ effectiveCancellationToken.ThrowIfCancellationRequested();
+
+ var cts = new TaskCompletionSource();
+ void RequestCompleted(object? sender, Camera.RequestCompletedEventArgs e)
+ {
+ if (request.Handle != e.Request.Handle)
+ return;
+
+ var buffer = e.Request.FindBuffer(_viewFinderStream);
+ if (buffer.Metadata.Status == FrameMetadata.StatusEnum.FrameError ||
+ buffer.Metadata.Status == FrameMetadata.StatusEnum.FrameStartup)
+ {
+ _logger.LogDebug("Request buffers status: {}", buffer.Metadata.Status);
+ e.Request.Reuse(Request.ReuseFlagEnum.ReuseBuffers);
+ var result = _camera.QueueRequest(e.Request);
+ if (result >= 0)
+ return;
+ }
+
+ _logger.LogDebug("Request complited: \n\tStatus: {}\n\tCookie: {}\n\tSequence: {}\n\tHasPendingBuffers: {}", e.Request.Status, e.Request.Cookie, e.Request.Sequence, e.Request.HasPendingBuffers);
+ _logger.LogDebug("Request buffer: \n\tStatus: {} \n\tCookie: {} \n\tSequence: {} \n\tTimestamp: {}", buffer.Metadata.Status, buffer.Cookie, buffer.Metadata.Sequence, buffer.Metadata.Timestamp);
+ _logger.LogDebug("Request buffer planes: ");
+ foreach (var (i, plane) in buffer.Metadata.Planes.Enumerate())
+ _logger.LogDebug("Plane[{}]: bytes used: 0x{:X}", i, plane.BytesUsed);
+ foreach (var (i, plane) in buffer.Planes.Enumerate())
+ _logger.LogDebug("Plane[{}]: \n\tFd: 0x{:X} \n\tOffset: 0x{:X} \n\tLength: 0x{:X} \n\tKInvalidOffset: 0x{:X}", i, plane.Fd.Get(), plane.Offset, plane.Length, plane.KInvalidOffset);
+
+ effectiveCancellationToken.ThrowIfCancellationRequested();
+
+ cts.SetResult();
+ }
+
+ _camera.RequestCompleted += RequestCompleted;
+ result = _camera.QueueRequest(request);
+ if (result < 0)
+ {
+ _camera.RequestCompleted -= RequestCompleted;
+ throw new Exception("Error while request queue");
+ }
+ _logger.LogDebug("Request queued");
+ await cts.Task.WaitAsync(effectiveCancellationToken);
+ _camera.RequestCompleted -= RequestCompleted;
+ _logger.LogDebug("Request complited");
+
+ return new (buffer, _viewFinderStreamConfiguration);
+ }
+
+ public void Test()
+ {
+ var config = _viewFinderStreamConfiguration!;
+ var size = config.Size;
+ var fourcc = config.PixelFormat.Fourcc;
+ var modifier = config.PixelFormat.Modifier;
+ var primariesEnum = config.ColorSpace?.Primaries;
+ var transferFunctionEnum = config.ColorSpace?.TransferFunction;
+ var ycbcrEncodingEnum = config.ColorSpace?.YcbcrEncoding;
+ var rangeEnum = config.ColorSpace?.Range;
+ var stride = config.Stride;
+ _logger.LogInformation($"Cannot find decoder for fourcc=0x{fourcc:X}({new string(BitConverter.GetBytes(fourcc).Select(b => (char)b).ToArray())}) modifier=0x{modifier:X} primariesEnum={primariesEnum} transferFunctionEnum={transferFunctionEnum} ycbcrEncodingEnum={ycbcrEncodingEnum} rangeEnum={rangeEnum}");
+ }
+
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
diff --git a/GSS2.Test/App.axaml b/GSS2.Test/App.axaml
index a972279..9a7233e 100644
--- a/GSS2.Test/App.axaml
+++ b/GSS2.Test/App.axaml
@@ -1,8 +1,12 @@
+
+
+
\ No newline at end of file
diff --git a/GSS2.Test/App.axaml.cs b/GSS2.Test/App.axaml.cs
index 27ecadd..c03b8ea 100644
--- a/GSS2.Test/App.axaml.cs
+++ b/GSS2.Test/App.axaml.cs
@@ -1,83 +1,25 @@
-using System.Drawing;
-
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
-using GSS2.Core;
-using GSS2.Core.Analysis.AmineContent.Database;
-using GSS2.Core.Hardware;
-
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging.Console;
-
namespace GSS2.Test;
public partial class App : Application
{
- private readonly IHost _host = BuildHostApp();
-
public override void Initialize()
{
- _host.Start();
AvaloniaXamlLoader.Load(this);
}
- private static IHost BuildHostApp()
- {
- var builder = Host.CreateApplicationBuilder();
-
- builder.Logging.ClearProviders();
- builder.Logging.AddConsole(options => options.FormatterName = nameof(ConsoleFormatter));
- builder.Logging.AddConsoleFormatter();
- builder.Logging.AddProvider(new FileLoggerProvider("app.log"));
- builder.Services.AddSingleton();
-
- builder.Services.AddAmineContentCalibrationContext(builder.Configuration);
- builder.Services.AddAmineContentResultsContext(builder.Configuration);
-
- builder.Services.AddLightsService("Hardware:Lights");
- builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
- builder.Services.AddIlluminatorService("Hardware:Illuminator");
- builder.Services.AddCameraService("Hardware:Camera", true);
-
- var host = builder.Build();
- host.Services.GetRequiredService();
-
- using (var scope = host.Services.CreateScope())
- {
- var context = scope.ServiceProvider.GetRequiredService();
- context.Database.Migrate();
- }
- using (var scope = host.Services.CreateScope())
- {
- var context = scope.ServiceProvider.GetRequiredService();
- context.Database.Migrate();
- }
-
- return host;
- }
-
public override void OnFrameworkInitializationCompleted()
{
+ var mainWindowViewModel = Program.ApplicationHost.Services.GetRequiredService();
+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
- {
- var lightsService = _host.Services.GetRequiredService();
- _ = lightsService.Run(CancellationToken.None);
- _ = lightsService.SnowfallAsync(0.3, 1, Color.Aqua);
-
- var cameraService = _host.Services.GetRequiredService();
- var illuminatorService = _host.Services.GetRequiredService();
-
- desktop.MainWindow = new MainWindow(cameraService, illuminatorService, lightsService);
- desktop.MainWindow.Closed += async (_, _) =>
+ desktop.MainWindow = new MainWindow()
{
- await lightsService.DisconnectAsync();
- await _host.StopAsync();
- _host.Dispose();
+ DataContext = mainWindowViewModel
};
- }
-
base.OnFrameworkInitializationCompleted();
}
}
\ No newline at end of file
diff --git a/GSS2.Test/CameraView.axaml b/GSS2.Test/CameraView.axaml
new file mode 100644
index 0000000..75d4976
--- /dev/null
+++ b/GSS2.Test/CameraView.axaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/GSS2.Test/CameraView.axaml.cs b/GSS2.Test/CameraView.axaml.cs
new file mode 100644
index 0000000..e8f5400
--- /dev/null
+++ b/GSS2.Test/CameraView.axaml.cs
@@ -0,0 +1,380 @@
+using System.Security.Cryptography;
+
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
+using Avalonia.Threading;
+
+using GSS2.Vulkan;
+
+using Iot.Device.FtCommon;
+
+using LibCameraSharp;
+
+using Microsoft.Extensions.Logging.Abstractions;
+
+using Silk.NET.Core;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.KHR;
+
+namespace GSS2.Test;
+
+interface IImage
+{
+ public (IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties, Silk.NET.Vulkan.Semaphore semaphore) Export();
+}
+interface IRenderer
+{
+ public abstract IImage ImportOrUpdate(int fd, int width, int height);
+}
+
+class VkImage
+{
+ public int Fd { get; set; }
+ public int Width { get; set; }
+ public int Height { get; set; }
+ public ulong MemorySize { get; set; }
+ public Silk.NET.Vulkan.Semaphore Semaphore { get; set; }
+
+ public (IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties, Silk.NET.Vulkan.Semaphore semaphore) Export()
+ {
+ return (
+ new PlatformHandle(new IntPtr(Fd), KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor),
+ new PlatformGraphicsExternalImageProperties
+ {
+ Width = Width,
+ Height = Height,
+ Format = PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm,
+ MemorySize = MemorySize
+ },
+ Semaphore
+ );
+ }
+}
+class VkRenderer
+{
+ readonly VulkanContext context = new VulkanContext("GSS2.Test", "CameraView");
+
+ public IPlatformHandle ExportSemaphore(bool renderFinished)
+ {
+ int fd = context.ExportSemaphoreFd(renderFinished ? context.RenderFinishedSemaphore : context.ImageAvailableSemaphore);
+ return new PlatformHandle(new IntPtr(fd), KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor);
+ }
+
+ unsafe public VkImage ImportOrUpdate(int fd, int width, int height)
+ {
+ try
+ {
+ var extImageInfo = new ExternalMemoryImageCreateInfo
+ {
+ SType = StructureType.ExternalMemoryImageCreateInfo,
+ HandleTypes = ExternalMemoryHandleTypeFlags.OpaqueFDBit
+ };
+
+ var imageInfo = new ImageCreateInfo
+ {
+ SType = StructureType.ImageCreateInfo,
+ PNext = &extImageInfo,
+ ImageType = ImageType.Type2D,
+ Format = Format.R8G8B8A8Unorm,
+ Extent = new Extent3D
+ {
+ Width = checked((uint)width),
+ Height = checked((uint)height),
+ Depth = 1
+ },
+ MipLevels = 1,
+ ArrayLayers = 1,
+ Samples = SampleCountFlags.Count1Bit,
+ Tiling = ImageTiling.Optimal,
+ Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
+ SharingMode = SharingMode.Exclusive,
+ InitialLayout = ImageLayout.Undefined
+ };
+
+ context.Api.CreateImage(context.Device, &imageInfo, null, out var image).ThrowOnError();
+ context.Api.GetImageMemoryRequirements(context.Device, image, out var memReq);
+
+ var exportAllocInfo = new ExportMemoryAllocateInfo
+ {
+ SType = StructureType.ExportMemoryAllocateInfo,
+ HandleTypes = ExternalMemoryHandleTypeFlags.OpaqueFDBit
+ };
+
+ var allocInfo = new MemoryAllocateInfo
+ {
+ SType = StructureType.MemoryAllocateInfo,
+ PNext = &exportAllocInfo,
+ AllocationSize = memReq.Size,
+ MemoryTypeIndex = checked((uint)FindMemoryType(memReq.MemoryTypeBits, MemoryPropertyFlags.DeviceLocalBit))
+ };
+
+ context.Api.AllocateMemory(context.Device, &allocInfo, null, out var memory).ThrowOnError();
+ context.Api.BindImageMemory(context.Device, image, memory, 0).ThrowOnError();
+
+ var poolInfo = new CommandPoolCreateInfo
+ {
+ SType = StructureType.CommandPoolCreateInfo,
+ Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
+ QueueFamilyIndex = context.QueueFamilyIndex
+ };
+ context.Api.CreateCommandPool(context.Device, &poolInfo, null, out var commandPool).ThrowOnError();
+
+ var cmdBufferAllocInfo = new CommandBufferAllocateInfo
+ {
+ SType = StructureType.CommandBufferAllocateInfo,
+ CommandPool = commandPool,
+ Level = CommandBufferLevel.Primary,
+ CommandBufferCount = 1
+ };
+
+ context.Api.AllocateCommandBuffers(context.Device, &cmdBufferAllocInfo, out var cmd).ThrowOnError();
+
+ var clearColor = new ClearColorValue((float)Random.Shared.NextDouble(), (float)Random.Shared.NextDouble(), (float)Random.Shared.NextDouble(), (float)Random.Shared.NextDouble());
+ var range = new ImageSubresourceRange
+ {
+ AspectMask = ImageAspectFlags.ColorBit,
+ BaseMipLevel = 0,
+ LevelCount = 1,
+ BaseArrayLayer = 0,
+ LayerCount = 1
+ };
+
+ var beginInfo = new CommandBufferBeginInfo
+ {
+ SType = StructureType.CommandBufferBeginInfo,
+ Flags = CommandBufferUsageFlags.OneTimeSubmitBit
+ };
+ context.Api.BeginCommandBuffer(cmd, &beginInfo).ThrowOnError();
+
+ TransitionImageLayout(cmd, image, ImageLayout.Undefined, ImageLayout.TransferDstOptimal);
+
+ context.Api.CmdClearColorImage(cmd, image, ImageLayout.TransferDstOptimal, &clearColor, 1, &range);
+
+ TransitionImageLayout(cmd, image, ImageLayout.TransferDstOptimal, ImageLayout.General);
+
+ context.Api.EndCommandBuffer(cmd).ThrowOnError();
+
+ var renderFinished = context.RenderFinishedSemaphore;
+
+ var submitInfo = new SubmitInfo
+ {
+ SType = StructureType.SubmitInfo,
+ CommandBufferCount = 1,
+ PCommandBuffers = &cmd,
+ PSignalSemaphores = &renderFinished,
+ SignalSemaphoreCount = 1
+ };
+
+ context.Api.QueueSubmit(context.Queue, 1, &submitInfo, default).ThrowOnError();
+
+ var getFdInfo = new MemoryGetFdInfoKHR
+ {
+ SType = StructureType.MemoryGetFDInfoKhr,
+ Memory = memory,
+ HandleType = ExternalMemoryHandleTypeFlags.OpaqueFDBit
+ };
+ if (context.Api.TryGetDeviceExtension(context.Instance, context.Device, out var khrExternalMemoryFd))
+ Console.WriteLine("KhrExternalMemoryFd import success");
+ else
+ throw new Exception("KhrExternalMemoryFd import error");
+ khrExternalMemoryFd.GetMemoryF(context.Device, &getFdInfo, out var fd1).ThrowOnError();
+ Console.WriteLine($"Image export success {fd1}");
+
+ return new VkImage()
+ {
+ Width = width,
+ Height = height,
+ Fd = fd1,
+ MemorySize = memReq.Size
+ };
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.Message);
+ Console.WriteLine(e.StackTrace);
+ throw;
+ }
+ }
+
+ private int FindMemoryType(uint typeFilter, MemoryPropertyFlags properties)
+ {
+ context!.Api.GetPhysicalDeviceMemoryProperties(context.PhysicalDevice, out var memProps);
+ for (int i = 0; i < memProps.MemoryTypeCount; i++)
+ if ((typeFilter & (1 << i)) != 0 && (memProps.MemoryTypes[i].PropertyFlags & properties) == properties)
+ return i;
+ return -1;
+ }
+ unsafe private void TransitionImageLayout(CommandBuffer cmd, Silk.NET.Vulkan.Image image, ImageLayout oldLayout, ImageLayout newLayout)
+ {
+ var barrier = new ImageMemoryBarrier
+ {
+ SType = StructureType.ImageMemoryBarrier,
+ OldLayout = oldLayout,
+ NewLayout = newLayout,
+ SrcQueueFamilyIndex = 0,
+ DstQueueFamilyIndex = 0,
+ Image = image,
+ SubresourceRange = new ImageSubresourceRange
+ {
+ AspectMask = ImageAspectFlags.ColorBit,
+ BaseMipLevel = 0,
+ LevelCount = 1,
+ BaseArrayLayer = 0,
+ LayerCount = 1
+ }
+ };
+
+ PipelineStageFlags srcStage;
+ PipelineStageFlags dstStage;
+
+ if (oldLayout == ImageLayout.Undefined && newLayout == ImageLayout.TransferDstOptimal)
+ {
+ barrier.SrcAccessMask = 0;
+ barrier.DstAccessMask = AccessFlags.TransferWriteBit;
+
+ srcStage = PipelineStageFlags.TopOfPipeBit;
+ dstStage = PipelineStageFlags.TransferBit;
+
+ }
+ else if (oldLayout == ImageLayout.TransferDstOptimal && newLayout == ImageLayout.ShaderReadOnlyOptimal)
+ {
+
+ barrier.SrcAccessMask = AccessFlags.TransferWriteBit;
+ barrier.DstAccessMask = AccessFlags.ShaderReadBit;
+
+ srcStage = PipelineStageFlags.TransferBit;
+ dstStage = PipelineStageFlags.FragmentShaderBit;
+
+ }
+ else
+ {
+ srcStage = PipelineStageFlags.AllCommandsBit;
+ dstStage = PipelineStageFlags.AllCommandsBit;
+ }
+
+ context!.Api.CmdPipelineBarrier(cmd, srcStage, dstStage, 0, 0, null, 0, null, 1, &barrier);
+ }
+
+}
+
+public partial class CameraView : Control
+{
+ private bool _initialized = false;
+ private Compositor? _compositor;
+ private CompositionSurfaceVisual? _visual;
+ private ICompositionGpuInterop? _gpuInterop;
+ private CompositionDrawingSurface? Surface { get; set; }
+ private bool _compositionRequested;
+ private VkRenderer? _renerer;
+ private VkImage? _latestImage;
+
+ public CameraView()
+ { }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ Initialize();
+ }
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ if (_initialized)
+ Surface?.Dispose();
+
+ _initialized = false;
+ base.OnDetachedFromLogicalTree(e);
+ }
+ private async void Initialize()
+ {
+ try
+ {
+ var selfVisual = ElementComposition.GetElementVisual(this)!;
+ _compositor = selfVisual.Compositor;
+
+ Surface = _compositor.CreateDrawingSurface();
+ _visual = _compositor.CreateSurfaceVisual();
+ _visual.Surface = Surface;
+ ElementComposition.SetElementChildVisual(this, _visual);
+ _gpuInterop = await _compositor.TryGetCompositionGpuInterop();
+ _renerer = new VkRenderer();
+ _initialized = true;
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"Error while initialization: \n{e.Message}\n{e.StackTrace}");
+ }
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ if (_visual is not null && change.Property == BoundsProperty)
+ {
+ _visual.Size = new Vector(Bounds.Width, Bounds.Height);
+ Update();
+ }
+ if (change.Property == DataContextProperty)
+ {
+ (DataContext as CameraViewModel)?.Update += (_, ea) => Update(ea.FrameBuffer, ea.StreamConfiguration);
+ }
+ base.OnPropertyChanged(change);
+ }
+
+ private void RenderToSurface(VkImage image)
+ {
+ var (handle, properties, semaphore) = image.Export();
+
+ var imageRenderedSemaphore = _gpuInterop!.ImportSemaphore(_renerer!.ExportSemaphore(false));
+ var renderCompletedSemaphore = _gpuInterop!.ImportSemaphore(_renerer!.ExportSemaphore(true));
+
+ var importedImage = _gpuInterop!.ImportImage(handle, properties);
+
+ Dispatcher.UIThread.Invoke(async () =>
+ {
+ await importedImage.ImportCompleted;
+ await Surface!.UpdateWithSemaphoresAsync(importedImage, renderCompletedSemaphore, imageRenderedSemaphore)
+ .ContinueWith((t) =>
+ {
+ if (t.Exception is not null)
+ {
+ Console.WriteLine($"Error while surface update: \n{t.Exception.Message}\n{t.Exception.StackTrace} ");
+ foreach (var e in t.Exception.InnerExceptions)
+ {
+ Console.WriteLine(e.Message);
+ Console.WriteLine(e.StackTrace);
+ }
+ }
+ });
+ }
+ );
+ }
+ private void OnComposition()
+ {
+ _compositionRequested = false;
+
+ var image = _latestImage;
+ if (image is null)
+ return;
+
+ RenderToSurface(image);
+ }
+ private void Update(FrameBuffer? frameBuffer = null, StreamConfiguration? streamConfiguration = null)
+ {
+ if (!_initialized || _compositor is null)
+ return;
+
+ if (frameBuffer is not null && streamConfiguration is not null && _renerer is not null)
+ {
+ var image = _renerer.ImportOrUpdate(frameBuffer.Planes.First().Fd.Get(), checked((int)streamConfiguration.Size.Width), checked((int)streamConfiguration.Size.Height));
+ _latestImage = image;
+ }
+
+ if (_compositionRequested)
+ return;
+
+ _compositionRequested = true;
+ _compositor?.RequestCompositionUpdate(OnComposition);
+ }
+}
\ No newline at end of file
diff --git a/GSS2.Test/CameraViewModel.cs b/GSS2.Test/CameraViewModel.cs
new file mode 100644
index 0000000..493506c
--- /dev/null
+++ b/GSS2.Test/CameraViewModel.cs
@@ -0,0 +1,32 @@
+using LibCameraSharp;
+
+namespace GSS2.Test;
+
+public class CameraViewModel : ViewModelBase
+{
+ public class UpdateEventArgs : EventArgs
+ {
+ public FrameBuffer? FrameBuffer { get; }
+ public StreamConfiguration? StreamConfiguration { get; }
+
+ public UpdateEventArgs(FrameBuffer? frameBuffer = null, StreamConfiguration? streamConfiguration = null)
+ {
+ FrameBuffer = frameBuffer;
+ StreamConfiguration = streamConfiguration;
+ }
+ }
+ private readonly ILogger _logger;
+
+ public event EventHandler? Update;
+
+ public CameraViewModel(ILogger logger)
+ {
+ _logger = logger;
+ _logger.LogInformation("Initialized");
+ }
+
+ public void CallUpdate(FrameBuffer? frameBuffer = null, StreamConfiguration? streamConfiguration = null)
+ {
+ Update?.Invoke(this, new UpdateEventArgs(frameBuffer, streamConfiguration));
+ }
+}
\ No newline at end of file
diff --git a/GSS2.Test/DependencyInjectionHelper.cs b/GSS2.Test/DependencyInjectionHelper.cs
new file mode 100644
index 0000000..9eba52c
--- /dev/null
+++ b/GSS2.Test/DependencyInjectionHelper.cs
@@ -0,0 +1,19 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.IdentityModel.Protocols.Configuration;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+using GSS2.Core.Hardware;
+using GSS2.Core.Analysis.AmineContent.Database;
+
+namespace GSS2.Test;
+
+public static class DependencyInjectionHelper
+{
+ public static IServiceCollection AddUi(this IServiceCollection services) =>
+ services
+ .AddTransient()
+ .AddTransient()
+ .AddTransient();
+}
\ No newline at end of file
diff --git a/GSS2.Test/DependencyInjectionViewLocator.cs b/GSS2.Test/DependencyInjectionViewLocator.cs
new file mode 100644
index 0000000..a9c285a
--- /dev/null
+++ b/GSS2.Test/DependencyInjectionViewLocator.cs
@@ -0,0 +1,29 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Templates;
+
+namespace GSS2.Test;
+
+public class DependencyInjectionViewLocator : IDataTemplate
+{
+ public Control? Build(object? data)
+ {
+ if (data is null)
+ return null;
+
+ var name = data.GetType().FullName?.Replace("ViewModel", "View");
+ if (name is null)
+ return new TextBlock { Text = $"Not found: {data}" };
+ var type = Type.GetType(name);
+ if (type is null)
+ return new TextBlock { Text = $"Not found type: {name} for {data}" };
+
+ var control = Program.ApplicationHost.Services.GetRequiredService(type) as Control;
+ if (control is null)
+ return new TextBlock { Text = $"Not found in DI: {type} for {data}" };
+
+ control.DataContext = data;
+ return control;
+ }
+
+ public bool Match(object? data) => data is ViewModelBase;
+}
\ No newline at end of file
diff --git a/GSS2.Test/FileLogger.cs b/GSS2.Test/FileLogger.cs
index d15ae60..02a777a 100644
--- a/GSS2.Test/FileLogger.cs
+++ b/GSS2.Test/FileLogger.cs
@@ -38,7 +38,6 @@ public sealed class FileLogger : ILogger
lock (_lock)
{
File.AppendAllText(_filePath, line + Environment.NewLine);
-
if (exception != null)
File.AppendAllText(_filePath, exception + Environment.NewLine);
}
diff --git a/GSS2.Test/FileLoggerProvider.cs b/GSS2.Test/FileLoggerProvider.cs
index 2c367e4..59d5e36 100644
--- a/GSS2.Test/FileLoggerProvider.cs
+++ b/GSS2.Test/FileLoggerProvider.cs
@@ -6,7 +6,12 @@ public sealed class FileLoggerProvider : ILoggerProvider
{
private readonly string _filePath;
- public FileLoggerProvider(string filePath) => _filePath = filePath;
+ public FileLoggerProvider(string filePath, bool append = true)
+ {
+ _filePath = filePath;
+ if (!append && File.Exists(filePath))
+ File.Create(filePath);
+ }
public ILogger CreateLogger(string categoryName) => new FileLogger(categoryName, _filePath);
diff --git a/GSS2.Test/GSS2.Test.csproj b/GSS2.Test/GSS2.Test.csproj
index b91816d..e09d129 100644
--- a/GSS2.Test/GSS2.Test.csproj
+++ b/GSS2.Test/GSS2.Test.csproj
@@ -8,23 +8,31 @@
dotnet-GSS2.Test-191ece0a-855d-46ec-9ecb-44378fc3db83
app.manifest
true
+ true
-
-
-
-
+
+
+
+
-
+
None
All
+
+
+
+
+
+
+
diff --git a/GSS2.Test/MainWindow.axaml b/GSS2.Test/MainWindow.axaml
index 64b4513..6457f9a 100644
--- a/GSS2.Test/MainWindow.axaml
+++ b/GSS2.Test/MainWindow.axaml
@@ -2,8 +2,15 @@
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"
- mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+ xmlns:local="using:GSS2.Test"
x:Class="GSS2.Test.MainWindow"
+ x:DataType="local:MainWindowViewModel"
Title="GSS2.Test">
-
+
+
+
+
+
+
+
diff --git a/GSS2.Test/MainWindow.axaml.cs b/GSS2.Test/MainWindow.axaml.cs
index 914970f..bbdb8cc 100644
--- a/GSS2.Test/MainWindow.axaml.cs
+++ b/GSS2.Test/MainWindow.axaml.cs
@@ -1,60 +1,26 @@
using Avalonia.Controls;
-using Avalonia.Media.Imaging;
-
-using GSS2.Core.Hardware;
namespace GSS2.Test;
public partial class MainWindow : Window
{
- private readonly CameraService _cameraService;
- private readonly IlluminatorService _illuminatorService;
- private readonly LightsService _lightsService;
-
- public MainWindow(CameraService cameraService, IlluminatorService illuminatorService, LightsService lightsService)
+ public MainWindow()
{
- _cameraService = cameraService;
- _illuminatorService = illuminatorService;
- _lightsService = lightsService;
-
InitializeComponent();
PointerPressed += async (_, _) =>
{
- _lightsService.CpuLoad(0.5, 1, System.Drawing.Color.Aqua);
+ var viewModel = DataContext as MainWindowViewModel;
+ if (viewModel is null)
+ return;
- try
+ var result = await viewModel.CaptureImage();
+ if (result is not null)
{
- _illuminatorService.SetIntensity(0.01, 0, 0);
- _illuminatorService.TurnOn();
- using var img = await _cameraService.CaptureImage(CancellationToken.None, 5);
-
- if (img is not null)
- {
- var bytes = img.ToBytes(".png");
- using var stream = new MemoryStream(bytes);
- image.Source = new Bitmap(stream);
- }
-
- _lightsService.Flash(0.5, 2, System.Drawing.Color.Green);
+ var (frameBuffer, streamConfiguration) = result.Value;
+ // cameraView.Update(frameBuffer, streamConfiguration);
+ (cameraView.Content as CameraViewModel)?.CallUpdate(frameBuffer, streamConfiguration);
}
- catch
- {
- _lightsService.Flash(0.5, 2, System.Drawing.Color.Red);
- }
- finally
- {
- _illuminatorService.TurnOff();
- }
-
- var snowfallTimer = new System.Timers.Timer(3000);
- snowfallTimer.Elapsed += (_, _) =>
- {
- lightsService.Snowfall(0.3, 1, System.Drawing.Color.Aqua);
- snowfallTimer.Dispose();
- };
- snowfallTimer.Start();
-
};
}
}
\ No newline at end of file
diff --git a/GSS2.Test/MainWindowViewModel.cs b/GSS2.Test/MainWindowViewModel.cs
new file mode 100644
index 0000000..5f20440
--- /dev/null
+++ b/GSS2.Test/MainWindowViewModel.cs
@@ -0,0 +1,66 @@
+using Avalonia.Input;
+
+using CommunityToolkit.Mvvm.ComponentModel;
+
+using GSS2.Core.Hardware;
+
+namespace GSS2.Test;
+
+public partial class MainWindowViewModel : ViewModelBase
+{
+ private readonly CameraService _cameraService;
+ private readonly IlluminatorService _illuminatorService;
+ private readonly LightsService _lightsService;
+ private readonly ILogger _logger;
+ [ObservableProperty] private CameraViewModel _cameraViewModel;
+
+ public MainWindowViewModel(
+ CameraService cameraService,
+ IlluminatorService illuminatorService,
+ LightsService lightsService,
+ ILogger logger,
+ CameraViewModel cameraViewModel
+ )
+ {
+ _cameraService = cameraService;
+ _illuminatorService = illuminatorService;
+ _lightsService = lightsService;
+ _logger = logger;
+ CameraViewModel = cameraViewModel;
+ _logger.LogInformation("Initialized");
+ }
+
+ public async Task<(LibCameraSharp.FrameBuffer, LibCameraSharp.StreamConfiguration)?> CaptureImage()
+ {
+ _logger.LogInformation("Capture image");
+ _lightsService.CpuLoad(0.5, 1, System.Drawing.Color.Aqua);
+
+ (LibCameraSharp.FrameBuffer, LibCameraSharp.StreamConfiguration)? result = null;
+ try
+ {
+ _illuminatorService.SetIntensity(0.01, 0, 0);
+ _illuminatorService.TurnOn();
+ result = await _cameraService.CaptureViewFinderBuffer(CancellationToken.None);
+ _lightsService.Flash(0.5, 2, System.Drawing.Color.Green);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while image capture");
+ _lightsService.Flash(0.5, 2, System.Drawing.Color.Red);
+ }
+ finally
+ {
+ _illuminatorService.TurnOff();
+ }
+
+ var snowfallTimer = new System.Timers.Timer(3000);
+ snowfallTimer.Elapsed += (_, _) =>
+ {
+ _lightsService.Snowfall(0.3, 1, System.Drawing.Color.Aqua);
+ snowfallTimer.Dispose();
+ };
+ snowfallTimer.Start();
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/GSS2.Test/Program.cs b/GSS2.Test/Program.cs
index 59a916c..6572a0c 100644
--- a/GSS2.Test/Program.cs
+++ b/GSS2.Test/Program.cs
@@ -1,25 +1,82 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Console;
+
using GSS2.Core;
using GSS2.Core.Analysis.AmineContent.Database;
using Avalonia;
+using Avalonia.Vulkan;
namespace GSS2.Test;
public class Program
{
+ public static IHost ApplicationHost { get; }
+
+ static Program()
+ {
+ ApplicationHost = BuildHostApp();
+ }
+
[STAThread]
public static void Main(string[] args)
{
+ ApplicationHost.RunAsync();
BuildAvaloniaApp()
- .UseStandardRuntimePlatformSubsystem()
.StartWithClassicDesktopLifetime(args);
}
- public static AppBuilder BuildAvaloniaApp()
+ private static IHost BuildHostApp()
+ {
+ var builder = Host.CreateApplicationBuilder();
+
+ builder.Logging.ClearProviders();
+ builder.Logging.AddConsole(options => options.FormatterName = nameof(ConsoleFormatter));
+ builder.Logging.AddConsoleFormatter();
+ builder.Logging.AddProvider(new FileLoggerProvider("full.log", true));
+ builder.Logging.AddProvider(new FileLoggerProvider("last_run.log", false));
+ builder.Services.AddSingleton();
+
+ builder.Services.AddAmineContentCalibrationContext(builder.Configuration);
+ builder.Services.AddAmineContentResultsContext(builder.Configuration);
+
+ builder.Services.AddLightsService("Hardware:Lights");
+ builder.Services.AddTemperatureHumidityService("Hardware:TemperatureHumidity");
+ builder.Services.AddIlluminatorService("Hardware:Illuminator");
+ builder.Services.AddCameraService("Hardware:Camera", true);
+
+ builder.Services.AddUi();
+
+ var host = builder.Build();
+ host.Services.GetRequiredService();
+
+ using (var scope = host.Services.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService();
+ context.Database.Migrate();
+ }
+ using (var scope = host.Services.CreateScope())
+ {
+ var context = scope.ServiceProvider.GetRequiredService();
+ context.Database.Migrate();
+ }
+
+ return host;
+ }
+
+ private static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure()
.UsePlatformDetect()
- .WithInterFont()
+ .With(new X11PlatformOptions
+ {
+ RenderingMode = [X11RenderingMode.Vulkan]
+ })
+ .With(new VulkanOptions
+ {
+ VulkanInstanceCreationOptions = new VulkanInstanceCreationOptions
+ {
+ UseDebug = true
+ }
+ })
.LogToTrace();
}
diff --git a/GSS2.Test/ViewModelBase.cs b/GSS2.Test/ViewModelBase.cs
new file mode 100644
index 0000000..4cf40d9
--- /dev/null
+++ b/GSS2.Test/ViewModelBase.cs
@@ -0,0 +1,6 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace GSS2.Test;
+
+public abstract class ViewModelBase : ObservableObject
+{ }
\ No newline at end of file
diff --git a/GSS2.Test/Worker.cs b/GSS2.Test/Worker.cs
index f2ab8db..cc7de70 100644
--- a/GSS2.Test/Worker.cs
+++ b/GSS2.Test/Worker.cs
@@ -1,4 +1,5 @@
using System.Drawing;
+
using GSS2.Core.Analysis.AmineContent.Database;
using GSS2.Core.Hardware;
@@ -24,24 +25,24 @@ public class Worker(
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
- await _cameraService.InitializeAsync(cancellationToken);
+ _cameraService.Test();
- _ = _lightsService.Run(cancellationToken);
- _ = _lightsService.SnowfallAsync(0.3, 1, Color.Aqua, cancellationToken);
+ // _ = _lightsService.Run(cancellationToken);
+ // _ = _lightsService.SnowfallAsync(0.3, 1, Color.Aqua, cancellationToken);
- _illuminatorService.SetIntensity(0.01, 0, 0);
- _illuminatorService.TurnOn();
+ // _illuminatorService.SetIntensity(0.01, 0, 0);
+ // _illuminatorService.TurnOn();
- await Task.Delay(1000);
+ // await Task.Delay(1000);
- var img = await _cameraService.CaptureImage(cancellationToken, 50);
- img?.SaveImage("IMAGE.png");
+ // var img = await _cameraService.CaptureImage(cancellationToken, 50);
+ // // img?.SaveImage("IMAGE.png");
- await Task.Delay(1000);
+ // await Task.Delay(1000);
- _illuminatorService.TurnOff();
+ // _illuminatorService.TurnOff();
- await _lightsService.DisconnectAsync(cancellationToken);
+ // await _lightsService.DisconnectAsync(cancellationToken);
applicationLifetime.StopApplication();
}
diff --git a/GSS2.Test/appsettings.json b/GSS2.Test/appsettings.json
index d85a5d0..fb61e82 100644
--- a/GSS2.Test/appsettings.json
+++ b/GSS2.Test/appsettings.json
@@ -7,7 +7,7 @@
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
"GSS2.Core.Hardware.LightsService": "Information",
"GSS2.Core.Hardware.IlluminatorService": "Information",
- "GSS2.Core.Hardware.CameraService": "Debug",
+ "GSS2.Core.Hardware.CameraService": "Information",
"LibCamera": "Information",
"LibCamera-Camera": "Information",
"LibCamera-RPI": "Information",
@@ -36,11 +36,11 @@
},
"Camera": {
"CameraId": "",
- "ViewFinderFrameBufferCount": 10,
+ "ViewFinderFrameBufferCount": 2,
"ViewFinderWidth": 1920,
"ViewFinderHeight": 1080,
"ViewFinderFps": 30,
- "ImageCaptureFrameBufferCount": 5,
+ "ImageCaptureFrameBufferCount": 2,
"ImageCaptureWidth": 4056,
"ImageCaptureHeight": 3040
},
diff --git a/GSS2.Vulkan/ByteString.cs b/GSS2.Vulkan/ByteString.cs
new file mode 100644
index 0000000..9b86da4
--- /dev/null
+++ b/GSS2.Vulkan/ByteString.cs
@@ -0,0 +1,68 @@
+// Основан на примере GpuInterop Avalonia
+// https://github.com/AvaloniaUI/Avalonia/blob/5f3dbae22244e830b464fc680c6c28ca124be41a/samples/GpuInterop/VulkanDemo/ByteString.cs
+
+using System.Runtime.InteropServices;
+
+namespace GSS2.Vulkan;
+
+unsafe sealed class ByteString : IDisposable
+{
+ private bool _disposedValue;
+ private readonly byte* _pointer;
+
+ public ByteString(string s)
+ {
+ _pointer = (byte*)Marshal.StringToHGlobalAnsi(s);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ Marshal.FreeHGlobal(new IntPtr(_pointer));
+ _disposedValue = true;
+ }
+ }
+ ~ByteString() => Dispose(disposing: false);
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ public static implicit operator byte*(ByteString obj) => obj._pointer;
+}
+
+unsafe sealed class ByteStringList : IDisposable
+{
+ private bool _disposedValue;
+ private readonly List _list;
+ private readonly byte** _pointer;
+
+ public int Count => _list.Count;
+
+ public ByteStringList(IEnumerable list)
+ {
+ _list = list.Select(x => new ByteString(x)).ToList();
+ _pointer = (byte**)Marshal.AllocHGlobal(IntPtr.Size * _list.Count + 1);
+ for (var c = 0; c < _list.Count; c++)
+ _pointer[c] = (byte*)_list[c];
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ Marshal.FreeHGlobal(new IntPtr(_pointer));
+ _disposedValue = true;
+ }
+ }
+ ~ByteStringList() => Dispose(disposing: false);
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ public static implicit operator byte**(ByteStringList obj) => obj._pointer;
+}
diff --git a/GSS2.Vulkan/GSS2.Vulkan.csproj b/GSS2.Vulkan/GSS2.Vulkan.csproj
new file mode 100644
index 0000000..20fd5f1
--- /dev/null
+++ b/GSS2.Vulkan/GSS2.Vulkan.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+ None
+ All
+
+
+
+
+
+
+
+
+
diff --git a/GSS2.Vulkan/VulkanContext.cs b/GSS2.Vulkan/VulkanContext.cs
new file mode 100644
index 0000000..f4196c7
--- /dev/null
+++ b/GSS2.Vulkan/VulkanContext.cs
@@ -0,0 +1,209 @@
+// Основан на примере GpuInterop Avalonia
+// https://github.com/AvaloniaUI/Avalonia/blob/5f3dbae22244e830b464fc680c6c28ca124be41a/samples/GpuInterop/VulkanDemo/VulkanContext.cs
+
+using Silk.NET.Core;
+using Silk.NET.Vulkan;
+using Silk.NET.Vulkan.Extensions.KHR;
+
+using Semaphore = Silk.NET.Vulkan.Semaphore;
+
+namespace GSS2.Vulkan;
+
+unsafe public class VulkanContext : IDisposable
+{
+ private bool _disposedValue;
+
+ public Vk Api { get; init; }
+ public Instance Instance { get; init; }
+ public PhysicalDevice PhysicalDevice { get; init; }
+ public Device Device { get; init; }
+ public uint QueueFamilyIndex { get; init; }
+ public Queue Queue { get; init; }
+ public Semaphore ImageAvailableSemaphore { get; init; }
+ public Semaphore RenderFinishedSemaphore { get; init; }
+
+ public VulkanContext(string applicationName, string engineName)
+ {
+ Api = Vk.GetApi();
+
+ List instanceExtensions =
+ [
+ "VK_KHR_get_physical_device_properties2",
+ "VK_KHR_external_memory_capabilities",
+ "VK_KHR_external_semaphore_capabilities",
+ "VK_EXT_debug_utils"
+ ];
+ List instanceLayers =
+ [
+
+ ];
+ List deviceExtensions =
+ [
+ "VK_KHR_external_memory",
+ "VK_KHR_external_memory_fd",
+ "VK_KHR_external_semaphore",
+ "VK_KHR_external_semaphore_fd"
+ ];
+
+ Instance = CreateInstance(Api, applicationName, engineName, instanceExtensions, instanceLayers);
+ PhysicalDevice = FindPhysicalDevice(Api, Instance, deviceExtensions);
+ (Device, QueueFamilyIndex) = CreateDevice(Api, Instance, PhysicalDevice, deviceExtensions);
+ Queue = CreateQueue(Api, Device, QueueFamilyIndex);
+ ImageAvailableSemaphore = CreateSemaphore(Api, Device);
+ RenderFinishedSemaphore = CreateSemaphore(Api, Device);
+ }
+
+ public int ExportSemaphoreFd(Semaphore semaphore)
+ {
+ if (!Api.TryGetDeviceExtension(Instance, Device, out var extension))
+ throw new InvalidOperationException($"No {KhrExternalSemaphoreFd.ExtensionName} device extension");
+
+ var info = new SemaphoreGetFdInfoKHR()
+ {
+ SType = StructureType.SemaphoreGetFDInfoKhr,
+ Semaphore = semaphore,
+ HandleType = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit
+ };
+ extension.GetSemaphoreF(Device, in info, out var fd).ThrowOnError();
+ return fd;
+ }
+
+ private static Instance CreateInstance(Vk api, string applicationName, string engineName, List extentions, List layers)
+ {
+ using var pApplicationName = new ByteString(applicationName);
+ using var pEngineName = new ByteString(engineName);
+ var applicationInfo = new ApplicationInfo
+ {
+ SType = StructureType.ApplicationInfo,
+ PApplicationName = pApplicationName,
+ PEngineName = pEngineName,
+ ApiVersion = new Version32(1, 1, 0),
+ EngineVersion = new Version32(1, 0, 0),
+ ApplicationVersion = new Version32(1, 0, 0)
+ };
+
+ using var pExtensions = new ByteStringList(extentions);
+ using var pLayers = new ByteStringList(layers);
+ var instanceCreateInfo = new InstanceCreateInfo
+ {
+ SType = StructureType.InstanceCreateInfo,
+ PApplicationInfo = &applicationInfo,
+ PpEnabledExtensionNames = pExtensions,
+ EnabledExtensionCount = checked((uint)pExtensions.Count),
+ PpEnabledLayerNames = pLayers,
+ EnabledLayerCount = checked((uint)pLayers.Count),
+ Flags = default
+ };
+
+ api.CreateInstance(in instanceCreateInfo, null, out var Instance).ThrowOnError();
+
+ return Instance;
+ }
+ private static PhysicalDevice FindPhysicalDevice(Vk api, Instance instance, List extensions)
+ {
+ uint count = 0;
+ api.EnumeratePhysicalDevices(instance, ref count, null).ThrowOnError();
+ var physicalDevices = stackalloc PhysicalDevice[(int)count];
+ api.EnumeratePhysicalDevices(instance, ref count, physicalDevices).ThrowOnError();
+
+ for (uint c = 0; c < count; c++)
+ {
+ if (extensions.Any(e => !api.IsDeviceExtensionPresent(physicalDevices[c], e)))
+ continue;
+ return physicalDevices[c];
+ }
+
+ throw new Exception("Suшtable Device not found");
+ }
+ private static (Device device, uint queueFamilyIndex) CreateDevice(Vk api, Instance instance, PhysicalDevice physicalDevice, List extensions)
+ {
+ uint queueFamilyCount = 0;
+ api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, null);
+ var queueFamilyProperties = new QueueFamilyProperties[(int)queueFamilyCount];
+ fixed (QueueFamilyProperties* pQueueFamilyProperties = queueFamilyProperties)
+ api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, pQueueFamilyProperties);
+
+ for (uint i = 0; i < queueFamilyCount; i++)
+ {
+ var queueFamily = queueFamilyProperties[i];
+ if (!queueFamily.QueueFlags.HasFlag(QueueFlags.GraphicsBit))
+ continue;
+
+ var queuePriorities = new float[(int)queueFamily.QueueCount];
+
+ for (var j = 0; j < queueFamily.QueueCount; j++)
+ queuePriorities[j] = 1f;
+
+ var features = new PhysicalDeviceFeatures();
+
+ Device device;
+ fixed (float* pQueuePriorities = queuePriorities)
+ {
+ var queueCreateInfo = new DeviceQueueCreateInfo
+ {
+ SType = StructureType.DeviceQueueCreateInfo,
+ QueueFamilyIndex = i,
+ QueueCount = queueFamily.QueueCount,
+ PQueuePriorities = pQueuePriorities
+ };
+
+ using var pEnabledDeviceExtensions = new ByteStringList(extensions);
+ var deviceCreateInfo = new DeviceCreateInfo
+ {
+ SType = StructureType.DeviceCreateInfo,
+ QueueCreateInfoCount = 1,
+ PQueueCreateInfos = &queueCreateInfo,
+ PpEnabledExtensionNames = pEnabledDeviceExtensions,
+ EnabledExtensionCount = checked((uint)pEnabledDeviceExtensions.Count),
+ PEnabledFeatures = &features
+ };
+
+ api.CreateDevice(physicalDevice, in deviceCreateInfo, null, out device).ThrowOnError();
+
+ return (device, i);
+ }
+ }
+ throw new Exception("Cannot create device");
+ }
+ private static Queue CreateQueue(Vk api, Device device, uint queueFamilyIndex)
+ {
+ api.GetDeviceQueue(device, queueFamilyIndex, 0, out var queue);
+ return queue;
+ }
+ private static Semaphore CreateSemaphore(Vk api, Device device)
+ {
+ var semaphoreExportInfo = new ExportSemaphoreCreateInfo
+ {
+ SType = StructureType.ExportSemaphoreCreateInfo,
+ HandleTypes = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit
+ };
+ var semaphoreCreateInfo = new SemaphoreCreateInfo
+ {
+ SType = StructureType.SemaphoreCreateInfo,
+ PNext = &semaphoreExportInfo
+ };
+
+ api.CreateSemaphore(device, in semaphoreCreateInfo, null, out var semaphore).ThrowOnError();
+ return semaphore;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ if (Api is null)
+ return;
+ Api.DestroySemaphore(Device, ImageAvailableSemaphore, null);
+ Api.DestroySemaphore(Device, RenderFinishedSemaphore, null);
+ }
+ _disposedValue = true;
+ }
+ }
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/GSS2.Vulkan/VulkanExtensions.cs b/GSS2.Vulkan/VulkanExtensions.cs
new file mode 100644
index 0000000..3a699ac
--- /dev/null
+++ b/GSS2.Vulkan/VulkanExtensions.cs
@@ -0,0 +1,15 @@
+// Основан на примере GpuInterop Avalonia
+// https://github.com/AvaloniaUI/Avalonia/blob/5f3dbae22244e830b464fc680c6c28ca124be41a/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs
+
+using Silk.NET.Vulkan;
+
+namespace GSS2.Vulkan;
+
+public static class VulkanExtensions
+{
+ public static void ThrowOnError(this Result result)
+ {
+ if (result != Result.Success)
+ throw new Exception($"Unexpected API error \"{result}\"");
+ }
+}
\ No newline at end of file
diff --git a/GranuSightSoftware2.sln b/GranuSightSoftware2.sln
index 24bf55b..4abd035 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.Vulkan", "GSS2.Vulkan\GSS2.Vulkan.csproj", "{CA315A5C-A9FA-44F2-A09C-08F3504ADD62}"
+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
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Debug|x64.Build.0 = Debug|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Debug|x86.Build.0 = Debug|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Release|x64.ActiveCfg = Release|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Release|x64.Build.0 = Release|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Release|x86.ActiveCfg = Release|Any CPU
+ {CA315A5C-A9FA-44F2-A09C-08F3504ADD62}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE