Skip to content

Commit

Permalink
Optimize x11 screen capturing
Browse files Browse the repository at this point in the history
  • Loading branch information
Neakita committed Sep 20, 2024
1 parent 11c0b02 commit 439314e
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 76 deletions.
9 changes: 5 additions & 4 deletions SightKeeper.Application.Linux/Natives/LibC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ 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)]
public static partial IntPtr shmat(int shmid, IntPtr shmaddr, int shmflg);

[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);
}
3 changes: 2 additions & 1 deletion SightKeeper.Application.Linux/Natives/LibXExt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
53 changes: 53 additions & 0 deletions SightKeeper.Application.Linux/SharedImageMemorySegment.cs
Original file line number Diff line number Diff line change
@@ -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<TPixel> : IDisposable
where TPixel : unmanaged, IPixel<TPixel>
{
public Vector2<ushort> Resolution { get; }
public unsafe ReadOnlySpan<TPixel> Data => new(_image.data, Resolution.X * Resolution.Y);

public unsafe SharedImageMemorySegment(nint display, Vector2<ushort> 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<ushort> 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();
}
}
54 changes: 22 additions & 32 deletions SightKeeper.Application.Linux/X11ScreenCapture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using CommunityToolkit.Diagnostics;
using SightKeeper.Application.Linux.Natives;
using SightKeeper.Domain.Model;
using SixLabors.ImageSharp;
Expand All @@ -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);
Expand All @@ -26,52 +24,44 @@ public X11ScreenCapture()

public unsafe Stream Capture(Vector2<ushort> resolution, Game? game)
{
int AllPlanes = ~0;
UIntPtr AllPlanes2 = new UIntPtr((uint)AllPlanes);
if (_memorySegment?.Resolution != resolution)
{
_memorySegment?.Dispose();
_memorySegment = new SharedImageMemorySegment<Bgra32>(_display, resolution);
}
_memorySegment.FetchData(_screen, new Vector2<ushort>());
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<Bgra32> 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<ushort> resolution, Vector2<ushort> offset)
{
return LibX.XGetImage(
_display,
_window,
offset.X,
offset.Y,
resolution.X,
resolution.Y,
(ulong)Planes.AllPlanes,
PixmapFormat.ZPixmap);
}
private SharedImageMemorySegment<Bgra32>? _memorySegment;

private void ReleaseUnmanagedResources()
{
LibX.XCloseDisplay(_display);
}

~X11ScreenCapture()
{
Dispose(false);
}

private void Dispose(bool disposing)
{
ReleaseUnmanagedResources();
if (disposing)
_memorySegment?.Dispose();
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
79 changes: 42 additions & 37 deletions SightKeeper.Application.Linux/XLibShm.cs
Original file line number Diff line number Diff line change
@@ -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;
}
Expand Down
2 changes: 0 additions & 2 deletions SightKeeper.Avalonia/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ushort>(640, 640), null);
}
var end = DateTime.UtcNow - start;
Expand Down

0 comments on commit 439314e

Please sign in to comment.