Skip to content

Commit

Permalink
Merge pull request #6254 from Susko3/sdl-clipboard-data
Browse files Browse the repository at this point in the history
Add support for deferred SDL3 clipboard callbacks (and use it for images)
  • Loading branch information
smoogipoo authored May 10, 2024
2 parents bf44358 + bbe54ce commit 9c6dee5
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 9 deletions.
15 changes: 8 additions & 7 deletions osu.Framework/Allocation/ObjectHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public struct ObjectHandle<T> : IDisposable

private GCHandle handle;

private readonly bool fromPointer;
private readonly bool canFree;

/// <summary>
/// Wraps the provided object with a <see cref="GCHandle" />, using the given <see cref="GCHandleType" />.
Expand All @@ -39,18 +39,19 @@ public struct ObjectHandle<T> : IDisposable
public ObjectHandle(T target, GCHandleType handleType)
{
handle = GCHandle.Alloc(target, handleType);
fromPointer = false;
canFree = true;
}

/// <summary>
/// Recreates an <see cref="ObjectHandle{T}" /> based on the passed <see cref="IntPtr" />.
/// Disposing this object will not free the handle, the original object must be disposed instead.
/// If <paramref name="ownsHandle"/> is <c>true</c>, disposing this object will free the handle.
/// </summary>
/// <param name="handle">Handle.</param>
public ObjectHandle(IntPtr handle)
/// <param name="handle"><see cref="Handle"/> from a previously constructed <see cref="ObjectHandle{T}(T, GCHandleType)"/>.</param>
/// <param name="ownsHandle">Whether this instance owns the underlying <see cref="GCHandle"/>.</param>
public ObjectHandle(IntPtr handle, bool ownsHandle = false)
{
this.handle = GCHandle.FromIntPtr(handle);
fromPointer = true;
canFree = ownsHandle;
}

/// <summary>
Expand Down Expand Up @@ -87,7 +88,7 @@ public bool GetTarget(out T target)

public void Dispose()
{
if (!fromPointer && handle.IsAllocated)
if (canFree && handle.IsAllocated)
handle.Free();
}

Expand Down
194 changes: 193 additions & 1 deletion osu.Framework/Platform/SDL/SDL3Clipboard.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,42 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using SDL;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;

namespace osu.Framework.Platform.SDL
{
public class SDL3Clipboard : Clipboard
{
/// <summary>
/// Supported formats for decoding images from the clipboard.
/// </summary>
// It's possible for a format to not have a registered decoder, but all default formats will have one:
// https://github.com/SixLabors/ImageSharp/discussions/1353#discussioncomment-9142056
private static IEnumerable<string> supportedImageMimeTypes => SixLabors.ImageSharp.Configuration.Default.ImageFormats.SelectMany(f => f.MimeTypes);

/// <summary>
/// Format used for encoding (saving) images to the clipboard.
/// </summary>
private readonly IImageFormat imageFormat;

public SDL3Clipboard(IImageFormat imageFormat)
{
this.imageFormat = imageFormat;
}

// SDL cannot differentiate between string.Empty and no text (eg. empty clipboard or an image)
// doesn't matter as text editors don't really allow copying empty strings.
// assume that empty text means no text.
Expand All @@ -17,12 +46,175 @@ public class SDL3Clipboard : Clipboard

public override Image<TPixel>? GetImage<TPixel>()
{
foreach (string mimeType in supportedImageMimeTypes)
{
if (tryGetData(mimeType, Image.Load<TPixel>, out var image))
{
Logger.Log($"Decoded {mimeType} from clipboard.");
return image;
}
}

return null;
}

public override bool SetImage(Image image)
{
return false;
ReadOnlyMemory<byte> memory;

// we can't save the image in the callback as the caller owns the image and might dispose it from under us.

using (var stream = new MemoryStream())
{
image.Save(stream, imageFormat);

// The buffer is allowed to escape the lifetime of the MemoryStream.
// https://learn.microsoft.com/en-us/dotnet/api/system.io.memorystream.getbuffer?view=net-8.0
// "This method works when the memory stream is closed."
memory = new ReadOnlyMemory<byte>(stream.GetBuffer(), 0, (int)stream.Length);
}

return trySetData(imageFormat.DefaultMimeType, () => memory);
}

/// <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)
{
using var objectHandle = new ObjectHandle<ClipboardCallbackContext>(userdata);

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

context.EnsureDataValid();
*length = context.DataLength;
return context.Address;
}

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

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

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;

/// <summary>
/// Length of the <see cref="ReadOnlyMemory{T}"/> returned by the <see cref="dataProvider"/>.
/// </summary>
public UIntPtr DataLength { get; private set; }

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

public void EnsureDataValid()
{
if (dataProvider == null)
{
Debug.Assert(Address != IntPtr.Zero);
Debug.Assert(DataLength != 0);
return;
}

var data = dataProvider();
dataProvider = null!;
DataLength = (UIntPtr)data.Length;
memoryHandle = data.Pin();
}

public void Dispose()
{
memoryHandle.Dispose();
DataLength = 0;
}
}
}
}
4 changes: 3 additions & 1 deletion osu.Framework/Platform/SDL3GameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Platform.SDL;
using SixLabors.ImageSharp.Formats.Png;

namespace osu.Framework.Platform
{
Expand All @@ -31,7 +32,8 @@ protected override TextInputSource CreateTextInput()
return base.CreateTextInput();
}

protected override Clipboard CreateClipboard() => new SDL3Clipboard();
// PNG works well on linux
protected override Clipboard CreateClipboard() => new SDL3Clipboard(PngFormat.Instance);

protected override IEnumerable<InputHandler> CreateAvailableInputHandlers() =>
new InputHandler[]
Expand Down

0 comments on commit 9c6dee5

Please sign in to comment.