Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for deferred SDL3 clipboard callbacks (and use it for images) #6254

Merged
merged 12 commits into from
May 10, 2024

Conversation

Susko3
Copy link
Member

@Susko3 Susko3 commented Apr 19, 2024

Improvements on previous PR

How it works

All the required data for the clipboard callback is saved to a ClipboardCallbackContext object. An ObjectHandle is allocated for that object to keep it alive when it's owned by SDL (ownership is transferred upon successfully calling SDL_SetClipboardData).

When another application (or we) request clipboard data, the clipboard content is generated and the resulting ReadOnlyMemory<byte> is pinned (so the GC doesn't move it) and its pointer is passed to SDL.

When new clipboard data is set, our cleanup callback gets called so we unpin the ReadOnlyMemory and free the ObjectHandle.

Tested on

  • Windows (with BMP format, still uses WindowsClipbard)
  • macOS (still uses MacOSClipboard)
  • X
  • Wayland

Please make sure that TestSceneClipboard passes and that copying and pasting images to/from external apps also works.

iOS and Android are not supported (clipboard text only)

@Speykious
Copy link
Contributor

The Debug.Assert fails on my Linux machine.
image

This might not be due to not supporting the image format though. Here, dataCallback wrongly assumes that it'll only get called once with the requested mimetype, but on Linux it gets called multiple times with a different mimetype each time, even though we pass an array of mimetypes with only one element in it as an argument to SDL_SetClipboardData.

I'd recommend integrating the logic I wrote in my latest commit to solve this issue.
Apparently the image could be dropped at any time since we don't own it, so the only solution I see for this is to copy the Image itself in SetImage and pass that to the callback context.

@FreezyLemon
Copy link
Contributor

FreezyLemon commented Apr 19, 2024

Seems to fail for BMP files on Wayland (PNG, JPEG work fine):

[runtime] 2024-04-19 20:01:01 [verbose]: SDL error log [debug]: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: Failed to get SDL clipboard data for image/png. SDL error: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: SDL error log [debug]: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: Failed to get SDL clipboard data for image/jpeg. SDL error: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: SDL error log [debug]: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: Failed to get SDL clipboard data for image/bmp. SDL error: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: SDL error log [debug]: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: Failed to get SDL clipboard data for image/tiff. SDL error: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: SDL error log [debug]: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: Failed to get SDL clipboard data for image/webp. SDL error: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: SDL error log [debug]: Pipe timeout
[runtime] 2024-04-19 20:01:01 [verbose]: Failed to get SDL clipboard data for image/qoi. SDL error: Pipe timeout
[runtime] 2024-04-19 20:01:01 [error]: Step "retrieve image from clipboard" SingleStepButton triggered an error
[runtime] 2024-04-19 20:01:01 [error]: System.NullReferenceException: Object reference not set to an instance of an object.
[runtime] 2024-04-19 20:01:01 [error]: at osu.Framework.Tests.Visual.Platform.TestSceneClipboard.<TestImage>b__15_5() in /home/jannik/Development/osu-framework/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs:line 73
[runtime] 2024-04-19 20:01:01 [error]: at osu.Framework.Testing.Drawables.Steps.SingleStepButton.<.ctor>b__1_0() in /home/jannik/Development/osu-framework/osu.Framework/Testing/Drawables/Steps/SingleStepButton.cs:line 19
[runtime] 2024-04-19 20:01:01 [error]: at osu.Framework.Testing.Drawables.Steps.StepButton.PerformStep(Boolean userTriggered) in /home/jannik/Development/osu-framework/osu.Framework/Testing/Drawables/Steps/StepButton.cs:line 124
[runtime] 2024-04-19 20:01:01 [error]: at osu.Framework.Testing.Drawables.Steps.StepButton.OnClick(ClickEvent e) in /home/jannik/Development/osu-framework/osu.Framework/Testing/Drawables/Steps/StepButton.cs:line 98
[runtime] 2024-04-19 20:01:01 [debug]: ClickEvent(Left) handled by "retrieve image from clipboard" SingleStepButton.
[runtime] 2024-04-19 20:01:01 [debug]: MouseClick handled by "retrieve image from clipboard" SingleStepButton.

@Susko3
Copy link
Member Author

Susko3 commented Apr 19, 2024

Apparently the image could be dropped at any time since we don't own it, so the only solution I see for this is to copy the Image itself in SetImage and pass that to the callback context.

If we're copying the image, we're going to allocate memory (and might do some processing to change the pixel format) -- might as well save it to the clipboard directly.

@Speykious
Copy link
Contributor

If we're copying the image, we're going to allocate memory (and might do some processing to change the pixel format) -- might as well save it to the clipboard directly.

I think I was for whatever reason thinking about returning the image in multiple different formats at the same time, but I guess it's unnecessary...

@Speykious
Copy link
Contributor

That being said, it still fails for me. :(
image

@Susko3
Copy link
Member Author

Susko3 commented Apr 19, 2024

@FreezyLemon your logs are a bit concerning. SDL_HasClipboardData is returning true, but then getting the data fails. Of course, it's not guaranteed that if SDL_HasClipboardData == true then SDL_GetClipboardData will always have some data (TOCTOU, etc.), but it failing in such a simple case warrants a bug report upstream.

@Susko3
Copy link
Member Author

Susko3 commented Apr 19, 2024

That being said, it still fails for me. :(

Right, the callback can get called multiple times. SDL internally copies the BMP data on windows.

This is completely valid, as you can paste the same image multiple times.
@FreezyLemon
Copy link
Contributor

@FreezyLemon your logs are a bit concerning. SDL_HasClipboardData is returning true, but then getting the data fails. Of course, it's not guaranteed that if SDL_HasClipboardData == true then SDL_GetClipboardData will always have some data (TOCTOU, etc.), but it failing in such a simple case warrants a bug report upstream.

The pipe timeout does feel like an upstream bug, yeah. I'll try and create a repro and then report it.

@bdach
Copy link
Collaborator

bdach commented Apr 22, 2024

X11

Seems to work fine on X.

This is used for native callbacks that own the passed in managed object.
bdach
bdach previously approved these changes Apr 23, 2024
Copy link
Collaborator

@bdach bdach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about whatever that wayland breakage is but I don't really personally much care for wayland at this point. Broken protocols/wms be broken. Let upstream deal with it.

Probably needs that macOS check before merge though.

@Susko3
Copy link
Member Author

Susko3 commented Apr 23, 2024

macOS should have full support for all clipboard data, but I'm mostly wondering which image formats are supported and compatible with other apps. Looking online, I see tiff, png and jpg mentioned.

We can always put multiple formats on the clipboard 😉

diff --git a/osu.Framework/Platform/MacOS/MacOSGameHost.cs b/osu.Framework/Platform/MacOS/MacOSGameHost.cs
index 0cb6f2362..45657c24a 100644
--- a/osu.Framework/Platform/MacOS/MacOSGameHost.cs
+++ b/osu.Framework/Platform/MacOS/MacOSGameHost.cs
@@ -11,6 +11,10 @@
 using osu.Framework.Input.Bindings;
 using osu.Framework.Input.Handlers;
 using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Platform.SDL;
+using SixLabors.ImageSharp.Formats.Jpeg;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Formats.Tiff;
 
 namespace osu.Framework.Platform.MacOS
 {
@@ -35,7 +39,7 @@ public override IEnumerable<string> UserStoragePaths
             }
         }
 
-        protected override Clipboard CreateClipboard() => new MacOSClipboard();
+        protected override Clipboard CreateClipboard() => new SDL3Clipboard(TiffFormat.Instance /* or PngFormat, JpegFormat */);
 
         protected override ReadableKeyCombinationProvider CreateReadableKeyCombinationProvider() => new MacOSReadableKeyCombinationProvider();
 

Copy link
Contributor

@smoogipoo smoogipoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have tested on Wayland

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

@smoogipoo smoogipoo May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're passing true here which reads as if we should be cleaning this up, however that's not being done and appears incorrect to do in this callback. Should this be changed to false to prevent incorrect use?

Comment on lines 161 to 167
var objectHandle = new ObjectHandle<ClipboardCallbackContext>(userdata, true);

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

@smoogipoo smoogipoo May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not do using var objectHandle = ... here and above (with the appropriate false parameter)? Simply based on the principle of ensuring all IDisposable objects are disposed.

I suppose there should be no case where GetTarget() fails to resolve but the GCHandle is still allocated, though it strikes me as a bit odd to manually handle disposal like this.

Comment on lines +199 to +212
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();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is pinned once and nulled afterwards, what's the purpose of this existing as a method as opposed to being done directly in the ctor and avoiding having to call this method/do the assertions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is to make use of deferred rendering of clipboard content. If no application requests the clipboard data, this code never runs and we don't need to serialise it to bytes.

This isn't of much use in this PR as the image is immediately serialised.

/// <summary>
/// Length of the <see cref="ReadOnlyMemory{T}"/> returned by the <see cref="dataProvider"/>.
/// </summary>
public UIntPtr DataLength;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about { private set; }?

@smoogipoo
Copy link
Contributor

I've also tested macOS with SDL3Clipboard + PngFormat.Instance. Not sure what else to test here.

@smoogipoo
Copy link
Contributor

On both Wayland and macOS, if I have something already in the clipboard, SetClipboardText("") doesn't clear it. The return code is 0 and there's no error in SDL_GetError().

SDL_ClearClipboardData() also seems to do nothing in this case, I assume that's because it's clearing data and not text.

Looks like an SDL issue.

@Susko3
Copy link
Member Author

Susko3 commented May 9, 2024

On both Wayland and macOS, if I have something already in the clipboard, SetClipboardText("") doesn't clear it. The return code is 0 and there's no error in SDL_GetError().

SDL_ClearClipboardData() also seems to do nothing in this case, I assume that's because it's clearing data and not text.

Looks like an SDL issue.

Definitely an SDL issue as SetClipboardText() is just shorthand for SDL_SetClipboardData with text/plain mime type. Plus, calling SetClipboardText(NULL) or SetClipboardText("") actually calls SDL_ClearClipboardData()

Copy link
Contributor

@smoogipoo smoogipoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's give this a shot. Leaving MacOSGameHost as is right now, even though it appears to work.

@smoogipoo smoogipoo enabled auto-merge May 10, 2024 05:57
@smoogipoo smoogipoo merged commit 9c6dee5 into ppy:master May 10, 2024
13 checks passed
@Susko3 Susko3 deleted the sdl-clipboard-data branch May 10, 2024 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants