diff --git a/SightKeeper.Application.Linux/Natives/LibC.cs b/SightKeeper.Application.Linux/Natives/LibC.cs index 0e2341ba..34238541 100644 --- a/SightKeeper.Application.Linux/Natives/LibC.cs +++ b/SightKeeper.Application.Linux/Natives/LibC.cs @@ -6,6 +6,7 @@ internal static partial class LibC { public const int IPC_CREAT = 01000; public const int IPC_PRIVATE = 0; + public const int IPC_RMID = 0; private const string DllName = "libc"; [LibraryImport(DllName, SetLastError = true)] @@ -13,10 +14,10 @@ internal static partial class LibC [LibraryImport(DllName, SetLastError = true)] public static partial int shmget(int key, IntPtr size, int shmflg); - - [LibraryImport(DllName)] - public static partial int shmctl(int shmid, int cmd, int buf); - + + [LibraryImport(DllName)] + public static partial int shmctl(int shmid, int cmd, int buf); + [LibraryImport(DllName, SetLastError = true)] public static partial int shmdt(IntPtr shmaddr); } \ No newline at end of file diff --git a/SightKeeper.Application.Linux/Natives/LibXExt.cs b/SightKeeper.Application.Linux/Natives/LibXExt.cs index 8c48729f..f03c5d86 100644 --- a/SightKeeper.Application.Linux/Natives/LibXExt.cs +++ b/SightKeeper.Application.Linux/Natives/LibXExt.cs @@ -11,7 +11,8 @@ internal static unsafe partial class LibXExt public static partial bool XShmDetach(IntPtr display, XShmSegmentInfo* shminfo); [LibraryImport(DllName)] - public static partial int XShmGetImage(IntPtr display, UIntPtr drawable, XImage* image, int x, int y, UIntPtr plane_mask); + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool XShmGetImage(IntPtr display, UIntPtr drawable, XImage* image, int x, int y, UIntPtr plane_mask); [LibraryImport(DllName, SetLastError = true)] public static partial int XShmQueryExtension(IntPtr display); diff --git a/SightKeeper.Application.Linux/SharedImageMemorySegment.cs b/SightKeeper.Application.Linux/SharedImageMemorySegment.cs new file mode 100644 index 00000000..6d99ce9b --- /dev/null +++ b/SightKeeper.Application.Linux/SharedImageMemorySegment.cs @@ -0,0 +1,53 @@ +using System.Runtime.InteropServices; +using SightKeeper.Application.Linux.Natives; +using SightKeeper.Domain.Model; +using SixLabors.ImageSharp.PixelFormats; + +namespace SightKeeper.Application.Linux; + +internal sealed class SharedImageMemorySegment : IDisposable + where TPixel : unmanaged, IPixel +{ + public Vector2 Resolution { get; } + public unsafe ReadOnlySpan Data => new(_image.data, Resolution.X * Resolution.Y); + + public unsafe SharedImageMemorySegment(nint display, Vector2 resolution) + { + _display = display; + Resolution = resolution; + _image = new ShmImage(); + _handle = GCHandle.Alloc(this, GCHandleType.Pinned); + fixed (ShmImage* image = &_image) + XLibShm.CreateImageSharedMemorySegment(display, image, resolution.X, resolution.Y); + } + + public unsafe void FetchData(int screen, Vector2 offset) + { + const int allPlanes = ~0; + UIntPtr allPlanes2 = new(unchecked((uint)allPlanes)); + var drawable = (UIntPtr)LibX.XRootWindow(_display, screen); + LibXExt.XShmGetImage(_display, drawable, _image.ximage, offset.X, offset.Y, allPlanes2); + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private readonly IntPtr _display; + private readonly ShmImage _image; + private GCHandle _handle; + + private unsafe void ReleaseUnmanagedResources() + { + fixed (ShmImage* image = &_image) + XLibShm.DestroyImage(_display, image); + _handle.Free(); + } + + ~SharedImageMemorySegment() + { + ReleaseUnmanagedResources(); + } +} \ No newline at end of file diff --git a/SightKeeper.Application.Linux/X11ScreenCapture.cs b/SightKeeper.Application.Linux/X11ScreenCapture.cs index 3b05d839..b2b86266 100644 --- a/SightKeeper.Application.Linux/X11ScreenCapture.cs +++ b/SightKeeper.Application.Linux/X11ScreenCapture.cs @@ -1,4 +1,3 @@ -using CommunityToolkit.Diagnostics; using SightKeeper.Application.Linux.Natives; using SightKeeper.Domain.Model; using SixLabors.ImageSharp; @@ -16,7 +15,6 @@ public X11ScreenCapture() { _display = LibX.XOpenDisplay(null); _screen = LibX.XDefaultScreen(_display); - _window = LibX.XRootWindow(_display, _screen); if (LibXExt.XShmQueryExtension(_display) == 0) { LibX.XCloseDisplay(_display); @@ -26,44 +24,23 @@ public X11ScreenCapture() public unsafe Stream Capture(Vector2 resolution, Game? game) { - int AllPlanes = ~0; - UIntPtr AllPlanes2 = new UIntPtr((uint)AllPlanes); + if (_memorySegment?.Resolution != resolution) + { + _memorySegment?.Dispose(); + _memorySegment = new SharedImageMemorySegment(_display, resolution); + } + _memorySegment.FetchData(_screen, new Vector2()); MemoryStream stream = new(); ShmImage image = new(); - XLibShm.createimage(_display, &image, resolution.X, resolution.Y);; - LibXExt.XShmGetImage(_display, (UIntPtr)LibX.XRootWindow(_display, _screen), image.ximage, 0, 0, AllPlanes2); - ReadOnlySpan data = new(image.data, resolution.X * resolution.Y); - var array = data.ToArray(); - var sum = array.Sum(x => x.PackedValue); - Guard.IsGreaterThan(sum, 0); - Image.LoadPixelData(data, resolution.X, resolution.Y).Save(stream, Encoder); - XLibShm.destroyimage(_display, &image); + Image.LoadPixelData(_memorySegment.Data, resolution.X, resolution.Y).Save(stream, Encoder); + XLibShm.DestroyImage(_display, &image); stream.Position = 0; return stream; } - public void Dispose() - { - ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - private readonly nint _display; private readonly int _screen; - private readonly IntPtr _window; - - private unsafe XImage* GetXImage(Vector2 resolution, Vector2 offset) - { - return LibX.XGetImage( - _display, - _window, - offset.X, - offset.Y, - resolution.X, - resolution.Y, - (ulong)Planes.AllPlanes, - PixmapFormat.ZPixmap); - } + private SharedImageMemorySegment? _memorySegment; private void ReleaseUnmanagedResources() { @@ -71,7 +48,20 @@ private void ReleaseUnmanagedResources() } ~X11ScreenCapture() + { + Dispose(false); + } + + private void Dispose(bool disposing) { ReleaseUnmanagedResources(); + if (disposing) + _memorySegment?.Dispose(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/SightKeeper.Application.Linux/XLibShm.cs b/SightKeeper.Application.Linux/XLibShm.cs index d3615b2e..416e290d 100644 --- a/SightKeeper.Application.Linux/XLibShm.cs +++ b/SightKeeper.Application.Linux/XLibShm.cs @@ -1,60 +1,65 @@ +using System.Runtime.InteropServices; using SightKeeper.Application.Linux.Natives; -using SixLabors.ImageSharp.PixelFormats; namespace SightKeeper.Application.Linux; -// https://stackoverflow.com/questions/34176795/any-efficient-way-of-converting-ximage-data-to-pixel-map-e-g-array-of-rgb-quad internal static class XLibShm { - private const int IPC_RMID = 0; - - public static unsafe void createimage(IntPtr dsp, ShmImage* image, int width, int height) + public static unsafe void CreateImageSharedMemorySegment(IntPtr display, ShmImage* image, int width, int height) { - // Create a shared memory area - image->shminfo.shmid = LibC.shmget(LibC.IPC_PRIVATE, width * height * sizeof(Bgra32), LibC.IPC_CREAT | 0600); - if (image->shminfo.shmid == -1) - { - throw new Exception(); - } - - // Map the shared memory segment into the address space of this process - image->shminfo.shmaddr = (char*)LibC.shmat(image->shminfo.shmid, 0, 0); - if (image->shminfo.shmaddr == (char*)-1) - { - throw new Exception(); - } - - image->data = (uint*)image->shminfo.shmaddr; - image->shminfo.readOnly = false; - - // Mark the shared memory segment for removal - // It will be removed even if this program crashes - LibC.shmctl(image->shminfo.shmid, IPC_RMID, 0); + CreateSharedMemorySegment(ref image->shminfo, width * height * sizeof(uint)); + image->data = MapSharedMemorySegment(ref image->shminfo); + MarkSharedMemorySegmentForRemoval(image->shminfo); + AllocateImageMemory(display, image, width, height); + LibXExt.XShmAttach(display, &image->shminfo); + LibX.XSync(display, false); + } - // Allocate the memory needed for the XImage structure - image->ximage = (XImage*)LibXExt.XShmCreateImage(dsp, LibX.XDefaultVisual(dsp, LibX.XDefaultScreen(dsp)), - LibX.XDefaultDepth(dsp, LibX.XDefaultScreen(dsp)), (int)PixmapFormat.ZPixmap, 0, - &image->shminfo, 0, 0); + private static unsafe void AllocateImageMemory(IntPtr display, ShmImage* image, int width, int height) + { + var screen = LibX.XDefaultScreen(display); + var visual = LibX.XDefaultVisual(display, screen); + var depth = LibX.XDefaultDepth(display, screen); + image->ximage = (XImage*)LibXExt.XShmCreateImage( + display, visual, depth, (int)PixmapFormat.ZPixmap, 0, &image->shminfo, 0, 0); if (image->ximage is null) { - destroyimage(dsp, image); + DestroyImage(display, image); throw new Exception("could not allocate the XImage structure"); } - image->ximage->data = (IntPtr)image->data; image->ximage->width = width; image->ximage->height = height; + } - // Ask the X server to attach the shared memory segment and sync - LibXExt.XShmAttach(dsp, &image->shminfo); - LibX.XSync(dsp, false); + private static void MarkSharedMemorySegmentForRemoval(XShmSegmentInfo segmentInfo) + { + LibC.shmctl(segmentInfo.shmid, LibC.IPC_RMID, 0); } - public static unsafe void destroyimage(nint dsp, ShmImage* image) + private static unsafe uint* MapSharedMemorySegment(ref XShmSegmentInfo segmentInfo) { - if (image->ximage is null) + segmentInfo.shmaddr = (char*)LibC.shmat(segmentInfo.shmid, 0, 0); + if (segmentInfo.shmaddr == (char*)-1) + { + throw new Exception(); + } + segmentInfo.readOnly = false; + return (uint*)segmentInfo.shmaddr; + } + + private static void CreateSharedMemorySegment(ref XShmSegmentInfo segmentInfo, nint size) + { + segmentInfo.shmid = LibC.shmget(LibC.IPC_PRIVATE, size, LibC.IPC_CREAT | 0600); + if (segmentInfo.shmid == -1) + throw new Exception(Marshal.GetLastPInvokeErrorMessage()); + } + + public static unsafe void DestroyImage(nint display, ShmImage* image) + { + if (image->ximage is not null) { - LibXExt.XShmDetach(dsp, &image->shminfo); + LibXExt.XShmDetach(display, &image->shminfo); LibX.XDestroyImage((IntPtr)image->ximage); image->ximage = null; } diff --git a/SightKeeper.Avalonia/Program.cs b/SightKeeper.Avalonia/Program.cs index e218669f..bec12838 100644 --- a/SightKeeper.Avalonia/Program.cs +++ b/SightKeeper.Avalonia/Program.cs @@ -32,8 +32,6 @@ public static void Main(string[] args) const int samples = 1000; for (int i = 0; i < samples; i++) { - if (i % 100 == 0 && i != 0) - Console.WriteLine($"{i} Elapsed {(DateTime.UtcNow - start).TotalMilliseconds / i}ms per capture"); using var stream = screenCapture.Capture(new Vector2(640, 640), null); } var end = DateTime.UtcNow - start;