Skip to content

Commit

Permalink
Add support for zero-copy SDL3 clipboard callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
Susko3 committed Apr 19, 2024
1 parent af8bbd2 commit 15d7d54
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 0 deletions.
6 changes: 6 additions & 0 deletions osu.Framework/Allocation/ObjectHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ public bool GetTarget(out T target)
return false;
}

public void FreeUnsafe()
{
if (handle.IsAllocated)
handle.Free();
}

#region IDisposable Support

public void Dispose()
Expand Down
138 changes: 138 additions & 0 deletions osu.Framework/Platform/SDL/SDL3Clipboard.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using SDL;
using SixLabors.ImageSharp;

Expand All @@ -24,5 +32,135 @@ public override bool SetImage(Image image)
{
return false;
}

/// <summary>
/// Decodes data from a native memory span. Return null or throw an exception if the data couldn't be decoded.
/// </summary>
/// <typeparam name="T">Type of decoded data.</typeparam>
private delegate T? SpanDecoder<out T>(ReadOnlySpan<byte> span);

private static unsafe bool tryGetData<T>(string mimeType, SpanDecoder<T> decoder, out T? data)
{
if (SDL3.SDL_HasClipboardData(mimeType) == SDL_bool.SDL_FALSE)
{
data = default;
return false;
}

UIntPtr nativeSize;
IntPtr pointer = SDL3.SDL_GetClipboardData(mimeType, &nativeSize);

if (pointer == IntPtr.Zero)
{
Logger.Log($"Failed to get SDL clipboard data for {mimeType}. SDL error: {SDL3.SDL_GetError()}");
data = default;
return false;
}

try
{
var nativeMemory = new ReadOnlySpan<byte>((void*)pointer, (int)nativeSize);
data = decoder(nativeMemory);
return data != null;
}
catch (Exception e)
{
Logger.Error(e, $"Failed to decode clipboard data for {mimeType}.");
data = default;
return false;
}
finally
{
SDL3.SDL_free(pointer);
}
}

private static unsafe bool trySetData(string mimeType, Func<ReadOnlyMemory<byte>> dataProvider)
{
var callbackContext = new ClipboardCallbackContext(mimeType, dataProvider);
var objectHandle = new ObjectHandle<ClipboardCallbackContext>(callbackContext, GCHandleType.Normal);

// TODO: support multiple mime types in a single callback
fixed (byte* ptr = Encoding.UTF8.GetBytes(mimeType + '\0'))
{
int ret = SDL3.SDL_SetClipboardData(&dataCallback, &cleanupCallback, objectHandle.Handle, &ptr, 1);

if (ret < 0)
{
objectHandle.Dispose();
Logger.Log($"Failed to set clipboard data callback. SDL error: {SDL3.SDL_GetError()}");
}

return ret == 0;
}
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static unsafe IntPtr dataCallback(IntPtr userdata, byte* mimeType, UIntPtr* length)
{
var objectHandle = new ObjectHandle<ClipboardCallbackContext>(userdata);

if (!objectHandle.GetTarget(out var context))
{
*length = 0;
return IntPtr.Zero;
}

Debug.Assert(context.MimeType == SDL3.PtrToStringUTF8(mimeType));

var memory = context.GetAndPinData();
*length = (UIntPtr)memory.Length;
return context.Address;
}

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static void cleanupCallback(IntPtr userdata)
{
var objectHandle = new ObjectHandle<ClipboardCallbackContext>(userdata);

if (objectHandle.GetTarget(out var context))
{
context.Dispose();
objectHandle.FreeUnsafe();
}
}

private class ClipboardCallbackContext : IDisposable
{
public readonly string MimeType;

/// <summary>
/// Provider of data suitable for the <see cref="MimeType"/>.
/// </summary>
/// <remarks>Called when another application requests that mime type from the OS clipboard.</remarks>
private Func<ReadOnlyMemory<byte>> dataProvider;

private MemoryHandle memoryHandle;

/// <summary>
/// Address of the <see cref="ReadOnlyMemory{T}"/> returned by the <see cref="dataProvider"/>.
/// </summary>
/// <remarks>Pinned and suitable for passing to unmanaged code.</remarks>
public unsafe IntPtr Address => (IntPtr)memoryHandle.Pointer;

public ClipboardCallbackContext(string mimeType, Func<ReadOnlyMemory<byte>> dataProvider)
{
MimeType = mimeType;
this.dataProvider = dataProvider;
}

public ReadOnlyMemory<byte> GetAndPinData()
{
var data = dataProvider();
dataProvider = null!;
memoryHandle = data.Pin();
return data;
}

public void Dispose()
{
memoryHandle.Dispose();
}
}
}
}

0 comments on commit 15d7d54

Please sign in to comment.