-
-
Notifications
You must be signed in to change notification settings - Fork 852
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
New Image.WrapMemory<TPixel>(void*) overloads #1419
Conversation
Can kill 2 birds with one stone by using a |
Codecov Report
@@ Coverage Diff @@
## master #1419 +/- ##
==========================================
- Coverage 83.48% 83.48% -0.01%
==========================================
Files 740 741 +1
Lines 32559 32575 +16
Branches 3652 3652
==========================================
+ Hits 27181 27194 +13
- Misses 4665 4668 +3
Partials 713 713
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
@Sergio0694 only concern here is that this is a very unsafe API (inherently). We should double check everything that goes on this front to make sure we choose the right thing from all alternatives. I think there is a reason why there is no Can you fill a BCL-style API proposal for this, showing motivation, alternatives and some examples on how real life interop code would interact with such an API? So far I only played with System.Drawing interop, and based on that experience having API that interacts with high level classes like |
@antonfirsov Updated the OP with a full proposal using the template from the runtime, let me know what you think! 😄 |
@Sergio0694 in your example there is a lifecycle issue (locking and unlocking buffers) similar to the one in I believe that adding dumb The solution should be rather a smart combination of the following approaches:
public static class ImageSharpUwpExtensions
{
// image.Dispose() will unlock the buffer
public static Image<TPixel> LockBufferAsImage<TPixel>(this SoftwareBitmap bitmap);
} |
I agree that such an API would be considered unsafe (I mean, it also literally is
My point is that in this scenario, I'm not sure I see how the Similar issue with things such as delegates, eg. in this case you'd just be creating an unnecessary closure that you don't really need anyway. That'd likely just make the code even more complex/verbose compared to just doing the above. To recap, if we added these new overloads to public static unsafe void ProcessFrame(ProcessVideoFrameContext context)
{
using BitmapBuffer sourceBuffer = context.InputFrame.SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Read);
using BitmapBuffer targetBuffer = context.OutputFrame.SoftwareBitmap.LockBuffer(BitmapBufferAccessMode.Write);
using IMemoryBufferReference sourceReference = sourceBuffer.CreateReference();
using IMemoryBufferReference targetReference = targetBuffer.CreateReference();
((IMemoryBufferByteAccess)sourceReference).GetBuffer(out byte* sourcePtr, out uint sourceCapacity);
((IMemoryBufferByteAccess)targetReference).GetBuffer(out byte* targetPtr, out _);
BitmapPlaneDescription bufferLayout = sourceBuffer.GetPlaneDescription(0);
int length = bufferLayout.Height * bufferLayout.Width;
using (Image<Rgba32> image = Image.WrapMemory<Rgba32>(sourcePtr, bufferLayout.Width, bufferLayout.Height))
{
image.Mutate(x => x.DetectEdges());
}
Buffer.MemoryCopy(sourcePtr, targetPtr, sourceCapacity, sourceCapacity);
} I'll admit I'm a bit biased here and might be particularly lax with respect to having "dangerous APIs" in a codebase, but I do feel like in this case there are valid arguments that would make having such an API worth to have? 🤔 Thoughts? 😄 EDIT: to be clear, I have no problems to also offer an overload taking a |
My main concern is the bad integration with On the other hand, I understand the concerns about how difficult and time consuming it is to provide a proper ways of integration with the current API-s, opened dotnet/runtime#44412, let's see reactions of Levi or others. |
@antonfirsov Might want to call out that this doesn't just apply to UWP in particular, but to any similar scenario too? Also note, the link to this issue gives a 404, you accidentally used |
Ah yeah totally valid concern, updating my OP. |
@Sergio0694 sorry, I'm not ignoring you in dotnet/runtime#44412, just want to keep it focused on the BCL stuff. I want to point out that in your original example you are modifying Based on your dotnet/runtime#44412 (comment), and #1422, I'm wondering if this is what we actually need: public class Image
{
public static void ProcessAsImage<TPixel>(ReadOnlySpan<TPixel> input, Span<TPixel> output, Action<IImageProcessingContext> operations);
} (Although not possible to implement yet.) |
@antonfirsov Ahahah no worries, that makes sense! To answer your question, yes that was by design, I was thinking the API would just work destructively on the input buffer just like normal Like, he did make a good point about our other Having an actual P.S.: by the way, really enjoying these API brainstorming sessions! 😄 |
@Sergio0694 feel free to proceed on this. I'm happy to merge the 3 proposed variants now, since based on the feedback in the runtime issue, there is really no better way to achieve this today. Note that many devs may end up misusing this API thanks to the fact that As for the overloads taking |
@antonfirsov That's... Actually concerning. And not just for this API, but also thinking about other developers that in general might be thrown off by those I guess at the very least I can add more info in the XML summary for these overloads? Like, with some additional notes on lifetime, and about checking whether one of the other overloads wouldn't work first before trying these new ones? |
That would be really appreciated 👍 |
/// For instance, if the input <see cref="Memory{T}"/> instance is one retrieved from an <see cref="IMemoryOwner{T}"/> instance | ||
/// rented from a memory pool (such as <see cref="MemoryPool{T}"/>), and that owning instance is disposed while the image is still | ||
/// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other | ||
/// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers properly. | ||
/// </summary> | ||
/// <typeparam name="TPixel">The pixel type</typeparam> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// </para>
/// <para>
/// Please Note: Using this method does not transfer the ownership of the underlying buffer of the input <see cref="Memory{T}"/>
/// to the new <see cref="Image{TPixel}"/> instance. This means that consumers of this method must ensure that the input buffer
/// is either self-contained, (for example, a <see cref="Memory{T}"/> instance wrapping a new array that was
/// created), or that the owning object is not disposed until the returned <see cref="Image{TPixel}"/> is disposed.
/// </para>
/// <para>
/// If the input <see cref="Memory{T}"/> instance is one retrieved from an <see cref="IMemoryOwner{T}"/> instance
/// rented from a memory pool (such as <see cref="MemoryPool{T}"/>), and that owning instance is disposed while the image is still
/// in use, this will lead to undefined behavior and possibly runtime crashes (as the same buffer might then be modified by other
/// consumers while the returned image is still working on it). Make sure to control the lifetime of the input buffers appropriately.
/// </para>
/// </summary>
/// prone and avoid having to pin the underlying memory buffer in use. This method is primarily meant to be used when | ||
/// doing interop or working with buffers that are located in unmanaged memory. | ||
/// </summary> | ||
/// <typeparam name="TPixel">The pixel type</typeparam> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// <summary>
/// <para>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels allowing viewing/manipulation as
/// an <see cref="Image{TPixel}"/> instance.
/// </para>
/// <para>
/// Please Note: This method relies on callers to carefully manage the target memory area being referenced by the
/// pointer and that the lifetime of such a memory area is at least equal to that of the returned
/// <see cref="Image{TPixel}"/> instance. For example, if the input pointer references an unmanaged memory area,
/// callers must ensure that the memory area is not freed as long as the returned <see cref="Image{TPixel}"/> is
/// in use and not disposed. The same applies if the input memory area points to a pinned managed object, as callers
/// must ensure that objects will remain pinned as long as the <see cref="Image{TPixel}"/> instance is in use.
/// Failing to do so constitutes undefined behavior and will likely lead to memory corruption and runtime crashes.
/// <para>
/// Note also that if you have a <see cref="Memory{T}"/> or an array (which can be cast to <see cref="Memory{T}"/>) of
/// either <see cref="byte"/> or <typeparamref name="TPixel"/> values, it is highly recommended to use one of the other
/// available overloads of this method instead (such as <see cref="WrapMemory{TPixel}(Configuration, Memory{byte}, int, int)"/>
/// or <see cref="WrapMemory{TPixel}(Configuration, Memory{TPixel}, int, int)"/>, to make the resulting code less error
/// prone and avoid having to pin the underlying memory buffer in use. This method is primarily meant to be used when
/// doing interop or working with buffers that are located in unmanaged memory.
/// </para>
/// </summary>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM.... Well I mean, scares the crap out of me but the code is good 😛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Product code LGTM, one nit in tests.
} | ||
|
||
[Fact] | ||
public unsafe void WrapSystemDrawingBitmap_FromPointer_WhenObserved() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public unsafe void WrapSystemDrawingBitmap_FromPointer_WhenObserved() | |
public unsafe void WrapSystemDrawingBitmap_FromPointer() |
Nit: "observation" is a concept related to Memory<T>
and IMemoryOwner
, not applicable here.
Unless it ends up with a terrible code bloat, I would also try to inline the locking from BitmapMemoryManager
to make it more clear what are we testing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean with "inline the locking"? I mean, I thought the point here was to showcase how you could have a self-contained memory manager wrapping a System.Drawing
image and controlling its lifetime 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant inlining the logic. Today was a day of terrible typos.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean: this is the case when you dont have MemoryManager. If you had one, you could just pass it (or the Memory).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But this is really just a nit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well I got busy with the other optimizations and James merged this PR before I could do this 😆
Let me know if you want me to create another PR to refactor this bit! 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to but only if you have the time.
New Image.WrapMemory<TPixel>(void*) overloads
Background and Motivation
Following the conversation with @JimBobSquarePants in the C# Discord server, this proposal is about adding some new
Image.WrapMemory
overloads to make it easy to interoperate with native memory buffers. In particular, having this feature built-in will allow users to avoid having to roll their own custom memory manager every time they need to do this, vastly reducing the boilerplate code they need to useImageSharp
in these situations.Proposed API
Usage Examples
The idea for this proposal was a developer that asked for help about how to interoperate between the
BitmapBuffer
type andImageSharp
, working in UWP. In particular, they wanted to apply theDetectEdges
processor to a graphics buffer stored in native memory (retrieved through some WinRT APIs), without incurring in unnecessary allocations. They were particularly struggling with this sinceImageSharp
had no built-in way to support wrapping native buffers. Here's the solution I came up with for them, which involved using a customUnmanagedMemoryManager<T>
type (the same as the one I included in this PR, essentially). Here's the code with my solution:You can see how having a built-in API to wrap a
void*
would make these cases much easier to work with. Such an API would allow for easy interoperation with a variety of buffer types, regardless of where they're coming from (eg. in this case, some WinRT API returning a COM interface wrapping a native buffer).Alternative Designs
We might want to call the new APIs
DangerousWrapMemory
if we wanted to highlight the "unsafe" nature of these APIs. Not really sure that's necessary though since the fact an input parameter isvoid*
already means the API could only be used when a developer is already within anunsafe
context anyway.Risks
This method is technically "not safe" since a user might accidentally use the
Image
instance afterwards, causing segfaults, but I'd argue that's not really a concern since in these scenarios the devs would already be dealing with pretty unsafe stuff such as COM interfaces, native buffers on their end as well, etc. It's only expected that they should also be aware of the lifetime of types they interact with in the first place.Open questions
These APIs all act as a non-owned buffer. Should we also have some owning overloads? If so, thoughts on the signature?
For instance, we could take:
MemoryHandle
void*
pointer + anIPinnable
instancevoid*
pointer + anIDisposable
instancevoid*
pointer + anAction
that disposesvoid*
pointer + a customReleaseMemoryCallback
delegate that receives thevoid*
pointer as inputI'm thinking we might want to have something like:
void*
: takes just a pointer, the image doesn't perform any kind of lifetime validation on the underlying memory. Users need to ensure the image is not used after the underlying memory is unpinned or freed.MemoryHandle
: this let's the image take ownership over pinning. That is, when the image is disposed, the handle will be released.void*
+ReleaseMemoryCallback
: full ownership, the delegate is invoked when the image is disposed, and it can do whatever logic it needs to free the buffer (it received the input pointer as input, so no need to capture it)Prerequisites