From 5af1c508f54db874a0c39e1eea3e41cb93214913 Mon Sep 17 00:00:00 2001 From: Alek-ban Date: Thu, 22 Jan 2026 08:04:04 +0300 Subject: [PATCH] feat: GSS2.Test - UI+DI, Vulkan render test --- GSS2.Core/Hardware/CameraService.cs | 94 +++++ GSS2.Test/App.axaml | 4 + GSS2.Test/App.axaml.cs | 66 +--- GSS2.Test/CameraView.axaml | 12 + GSS2.Test/CameraView.axaml.cs | 380 ++++++++++++++++++++ GSS2.Test/CameraViewModel.cs | 32 ++ GSS2.Test/DependencyInjectionHelper.cs | 19 + GSS2.Test/DependencyInjectionViewLocator.cs | 29 ++ GSS2.Test/FileLogger.cs | 1 - GSS2.Test/FileLoggerProvider.cs | 7 +- GSS2.Test/GSS2.Test.csproj | 18 +- GSS2.Test/MainWindow.axaml | 11 +- GSS2.Test/MainWindow.axaml.cs | 52 +-- GSS2.Test/MainWindowViewModel.cs | 66 ++++ GSS2.Test/Program.cs | 63 +++- GSS2.Test/ViewModelBase.cs | 6 + GSS2.Test/Worker.cs | 23 +- GSS2.Test/appsettings.json | 6 +- GSS2.Vulkan/ByteString.cs | 68 ++++ GSS2.Vulkan/GSS2.Vulkan.csproj | 29 ++ GSS2.Vulkan/VulkanContext.cs | 209 +++++++++++ GSS2.Vulkan/VulkanExtensions.cs | 15 + GranuSightSoftware2.sln | 14 + 23 files changed, 1093 insertions(+), 131 deletions(-) create mode 100644 GSS2.Test/CameraView.axaml create mode 100644 GSS2.Test/CameraView.axaml.cs create mode 100644 GSS2.Test/CameraViewModel.cs create mode 100644 GSS2.Test/DependencyInjectionHelper.cs create mode 100644 GSS2.Test/DependencyInjectionViewLocator.cs create mode 100644 GSS2.Test/MainWindowViewModel.cs create mode 100644 GSS2.Test/ViewModelBase.cs create mode 100644 GSS2.Vulkan/ByteString.cs create mode 100644 GSS2.Vulkan/GSS2.Vulkan.csproj create mode 100644 GSS2.Vulkan/VulkanContext.cs create mode 100644 GSS2.Vulkan/VulkanExtensions.cs 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