-
-
Notifications
You must be signed in to change notification settings - Fork 853
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
Image.WrapMemory<TPixel> APIs wrapping Memory<byte> #1314
Conversation
/// <exception cref="ArgumentNullException">The configuration is null.</exception> | ||
/// <exception cref="ArgumentNullException">The metadata is null.</exception> | ||
/// <returns>An <see cref="Image{TPixel}"/> instance</returns> | ||
public static Image<TPixel> WrapMemory<TPixel>( |
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.
https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines
Seems beneficial to consider exposing IMemoryOwner<T>
overloads of this to allow ownership transfer?
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 didn't include them here on purpose as they weren't part of the initial API proposal in #1097.
@antonfirsov should this be added here or would that be for another PR?
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.
No preference, other than it's always easier to review multiple smaller (subsequent) PR-s.
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.
Sounds like a preference to me 😄
Will keep this PR restricted to consumed buffers then, and we can create another one after this one is merged to also add support for owned ones through IMemoryOwner<byte>
.
@JimBobSquarePants my build is failing locally due to a bunch of errors about a missing assembly key in Is there some new additional configuration step I'm missing? Thanks! |
@Sergio0694 Likely |
@JimBobSquarePants I had already tried to clean the solution and to run both |
@JimBobSquarePants Well surprise, I disabled the .NET 5 Preview SDK and I can build just fine now 😅 |
How do you disable a preview? I've been terrified to install any of them. |
@JimBobSquarePants It's pretty easy actually, you need to go to Tools > Options > Environment > Preview Features and then untick the option regarding preview versions of the .NET Core SDK, and restart VS: I could finally debug the tests locally with that out of the picture, fixed crossed for the CI now! 🚀 |
Codecov Report
@@ Coverage Diff @@
## master #1314 +/- ##
==========================================
- Coverage 82.77% 82.77% -0.01%
==========================================
Files 689 690 +1
Lines 30956 30973 +17
Branches 3511 3511
==========================================
+ Hits 25625 25639 +14
- Misses 4610 4613 +3
Partials 721 721
Flags with carried forward coverage won't be shown. Click here to find out more.
Continue to review full report at Codecov.
|
CI passed! 🚀 The PR should be ready for review with the original API surface mentioned in the linked issue. |
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.
Just a few quick remarks, will do deeper review later.
/// <exception cref="ArgumentNullException">The configuration is null.</exception> | ||
/// <exception cref="ArgumentNullException">The metadata is null.</exception> | ||
/// <returns>An <see cref="Image{TPixel}"/> instance</returns> | ||
public static Image<TPixel> WrapMemory<TPixel>( |
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.
No preference, other than it's always easier to review multiple smaller (subsequent) PR-s.
@Sergio0694 I will get to this. Just fixing a critical issue #1316 first |
@JimBobSquarePants Don't worry, no rush! 😄 |
@JimBobSquarePants Just noticed this bit while adding the unit tests: ImageSharp/src/ImageSharp/Memory/MemoryOwnerExtensions.cs Lines 16 to 22 in 801961d
Wouldn't it be more efficient to just have |
@Sergio0694 Wow! Never actually looked at the code for |
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.
Not sure we need the new code analysis attributes (we don't use them elsewhere) but otherwise looks great! 👍
@@ -38,6 +38,7 @@ public abstract partial class Image | |||
{ | |||
Guard.NotNull(configuration, nameof(configuration)); | |||
Guard.NotNull(metadata, nameof(metadata)); | |||
Guard.IsTrue(pixelMemory.Length == width * height, nameof(pixelMemory), "The length of the input memory doesn't match the specified image size"); |
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.
Remind me to do a pass one day to remove all these guard allocations.
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.
Wait what allocations? 🤔
Also do you want me to create an issue about this to make sure we don't forget in the future?
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.
Thought that would get you thinking. We always allocate the string whether true or false.
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 string though? Both of those are constants, so the JIT will just load the address here.
I feel like I might be missing something obvious 😅
Eg. if you have this (full repro here):
public static void Test(Memory<int> memory)
{
IsTrue(false, nameof(memory), "This is a sample constant string");
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static void IsTrue(bool flag, string a, string b)
{
}
You get this:
C.Test(System.Memory`1<Int32>)
L0000: mov r8, 0x16e9fc8ab98
L000a: mov r8, [r8]
L000d: mov rdx, 0x16e9fc8ab28
L0017: mov rdx, [rdx]
L001a: xor ecx, ecx
L001c: mov rax, C.IsTrue(Boolean, System.String, System.String)
L0026: jmp rax
So you only pay the two indirect memory accesses to load the strings, but you have no allocations.
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.
Ah yeah, I'm thinking when we interpolate (which I thought we were doing here). It might seem useful but it adds a lot of overhead
## PR Type What kind of change does this PR introduce? <!-- Please uncomment one or more that apply to this PR. --> - Feature ## What is the current behavior? <!-- Please describe the current behavior that you are modifying, or link to a relevant issue. --> Right now there is no (easy) way to cast a `Memory<TFrom>` instance to a `Memory<TTo>` instance. There are APIs to to do that for `Span<T>` instances, but not for `Memory<T>`. The reason for that is that with a `Span<T>` it's just a matter of retrieving the wrapped reference, reinterpreting it and then adjusting the size, then creating a new `Span<T>` instance. But a `Memory<T>` instance is completely different: it wraps an object which could be either a `T[]` array, a `MemoryManager<T>` instance, etc. The result is that currently there are no APIs in the BCL nor in the toolkit to just "cast" a `Memory<T>`. This feature has been requested by a number of developers, including in a well known library such as `ImageSharp`: > Yes, that's exactly what I would need. But I'm wondering how would you implement it. > It's certainly non trivial to cast a `Memory<byte>` to a `Memory<TPixel>` and if there's an API for that I would gladly want to know... > So I pressume `ImageSharp` would need to do some work under the hood. (_`ImageSharp` issue, [here](SixLabors/ImageSharp#1097 (comment)) To solve that, I created a very simplified version of the code included in this PR, into a PR [here](SixLabors/ImageSharp#1314). Having this available right out of the box in the `HighPerformance` package would be helpful in a number of similar situations, especially with `Memory<T>` APIs becoming more and more common across libraries now (as they've been out for a while). ## What is the new behavior? <!-- Describe how was this issue resolved or changed? --> This PR includes 4 new extensions for the `Memory<T>` and `ReadOnlyMemory<T>` types that enable the following: ```csharp // Cast between two Memory<T> instances... Memory<byte> memoryOfBytes = new byte[128].AsMemory(); Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>(); // ...any number of times is needed Memory<int> memoryOfInts = memoryOfFloats.Cast<float, int>(); Memory<byte> backToBytesMemory = memoryOfInts.Cast<int, byte>(); // Or just convert into bytes directly Memory<int> sourceAsInts = new int[128].AsMemory(); Memory<byte> sourceAsBytes = sourceAsInts.AsBytes(); // Want to get a stream from a string? Why not! 😄 using (Stream stream = "Hello world".AsMemory().AsBytes().AsStream()) { // Use the stream here, which reads *directly* from the string data! } ``` Here is the full list of the new APIs introduced in this PR: ```csharp namespace Microsoft.Toolkit.HighPerformance.Extensions { public static class MemoryExtensions { public static Memory<byte> AsBytes<T>(this Memory<T> memory) where T : unmanaged; public static Memory<TTo> Cast<TFrom, TTo>(this Memory<TFrom> memory) where TFrom : unmanaged where TTo : unmanaged; } public static class ReadOnlyMemoryExtensions { public static ReadOnlyMemory<byte> AsBytes<T>(this ReadOnlyMemory<T> memory) where T : unmanaged; public static ReadOnlyMemory<TTo> Cast<TFrom, TTo>(this ReadOnlyMemory<TFrom> memory) where TFrom : unmanaged where TTo : unmanaged; } } ``` ## Notes Marking as draft as this is still being worked on, but feedbacks and reviews are welcome! 😄 ## PR Checklist Please check if your PR fulfills the following requirements: - [X] Tested code with current [supported SDKs](../readme.md#supported) - [ ] ~~Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs). Link: <!-- docs PR link -->~~ - [ ] ~~Sample in sample app has been added / updated (for bug fixes / features)~~ - [ ] ~~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https://github.com/windows-toolkit/WindowsCommunityToolkit-design-assets)~~ - [X] Tests for the changes have been added (for bug fixes / features) (if applicable) - [X] Header has been added to all new source files (run *build/UpdateHeaders.bat*) - [X] Contains **NO** breaking changes
## PR Type What kind of change does this PR introduce? <!-- Please uncomment one or more that apply to this PR. --> - Feature ## What is the current behavior? <!-- Please describe the current behavior that you are modifying, or link to a relevant issue. --> Right now there is no (easy) way to cast a `Memory<TFrom>` instance to a `Memory<TTo>` instance. There are APIs to to do that for `Span<T>` instances, but not for `Memory<T>`. The reason for that is that with a `Span<T>` it's just a matter of retrieving the wrapped reference, reinterpreting it and then adjusting the size, then creating a new `Span<T>` instance. But a `Memory<T>` instance is completely different: it wraps an object which could be either a `T[]` array, a `MemoryManager<T>` instance, etc. The result is that currently there are no APIs in the BCL nor in the toolkit to just "cast" a `Memory<T>`. This feature has been requested by a number of developers, including in a well known library such as `ImageSharp`: > Yes, that's exactly what I would need. But I'm wondering how would you implement it. > It's certainly non trivial to cast a `Memory<byte>` to a `Memory<TPixel>` and if there's an API for that I would gladly want to know... > So I pressume `ImageSharp` would need to do some work under the hood. (_`ImageSharp` issue, [here](SixLabors/ImageSharp#1097 (comment)) To solve that, I created a very simplified version of the code included in this PR, into a PR [here](SixLabors/ImageSharp#1314). Having this available right out of the box in the `HighPerformance` package would be helpful in a number of similar situations, especially with `Memory<T>` APIs becoming more and more common across libraries now (as they've been out for a while). ## What is the new behavior? <!-- Describe how was this issue resolved or changed? --> This PR includes 4 new extensions for the `Memory<T>` and `ReadOnlyMemory<T>` types that enable the following: ```csharp // Cast between two Memory<T> instances... Memory<byte> memoryOfBytes = new byte[128].AsMemory(); Memory<float> memoryOfFloats = memoryOfBytes.Cast<byte, float>(); // ...any number of times is needed Memory<int> memoryOfInts = memoryOfFloats.Cast<float, int>(); Memory<byte> backToBytesMemory = memoryOfInts.Cast<int, byte>(); // Or just convert into bytes directly Memory<int> sourceAsInts = new int[128].AsMemory(); Memory<byte> sourceAsBytes = sourceAsInts.AsBytes(); // Want to get a stream from a string? Why not! 😄 using (Stream stream = "Hello world".AsMemory().AsBytes().AsStream()) { // Use the stream here, which reads *directly* from the string data! } ``` Here is the full list of the new APIs introduced in this PR: ```csharp namespace Microsoft.Toolkit.HighPerformance.Extensions { public static class MemoryExtensions { public static Memory<byte> AsBytes<T>(this Memory<T> memory) where T : unmanaged; public static Memory<TTo> Cast<TFrom, TTo>(this Memory<TFrom> memory) where TFrom : unmanaged where TTo : unmanaged; } public static class ReadOnlyMemoryExtensions { public static ReadOnlyMemory<byte> AsBytes<T>(this ReadOnlyMemory<T> memory) where T : unmanaged; public static ReadOnlyMemory<TTo> Cast<TFrom, TTo>(this ReadOnlyMemory<TFrom> memory) where TFrom : unmanaged where TTo : unmanaged; } } ``` ## Notes Marking as draft as this is still being worked on, but feedbacks and reviews are welcome! 😄 ## PR Checklist Please check if your PR fulfills the following requirements: - [X] Tested code with current [supported SDKs](../readme.md#supported) - [ ] ~~Pull Request has been submitted to the documentation repository [instructions](..\contributing.md#docs). Link: <!-- docs PR link -->~~ - [ ] ~~Sample in sample app has been added / updated (for bug fixes / features)~~ - [ ] ~~Icon has been created (if new sample) following the [Thumbnail Style Guide and templates](https://github.com/windows-toolkit/WindowsCommunityToolkit-design-assets)~~ - [X] Tests for the changes have been added (for bug fixes / features) (if applicable) - [X] Header has been added to all new source files (run *build/UpdateHeaders.bat*) - [X] Contains **NO** breaking changes
Prerequisites
Closes #1097
Description
This PR adds the following APIs to the
Image
class:The internal implementation includes a new
ByteMemoryManager<T>
type that acts as a bridge to wrapMemory<byte>
instances and use that data to produce aMemory<TPixel>
instance that can be used to load the image as requested.