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

Target Image<TPixel> loading/decoding APIs #1490

Closed
wants to merge 8,903 commits into from

Conversation

Sergio0694
Copy link
Member

@Sergio0694 Sergio0694 commented Jan 3, 2021

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Closes #1487

Description

This PR adds new APIs to load/decode images directly into reused Image<TPixel> instances.

Details

I've decided to add the overloads for the Image<TPixel> class to improve interoperability with other APIs.
I didn't feel great about either adding a new, separate IImageMemoryDecoder type, so this should help with consustency 😄

API additions (click to expand):
namespace SixLabors.ImageSharp.Formats
{
    public interface IImageDecoder
    {
        void Decode<TPixel>(Stream stream, Image<TPixel> image)
            where TPixel : unmanaged, IPixel<TPixel>;
        Task DecodeAsync<TPixel>(Stream stream, Image<TPixel> image, CancellationToken cancellationToken)
            where TPixel : unmanaged, IPixel<TPixel>;
    }
}

namespace SixLabors.ImageSharp
{
    public abstract partial class Image
    {
        // byte[]
        public static void Load<TPixel>(byte[] data, Image<TPixel> image, out IImageFormat format)
            where TPixel : unmanaged, IPixel<TPixel>;
        public static void Load<TPixel>(byte[] data, Image<TPixel> image, IImageDecoder decoder)
            where TPixel : unmanaged, IPixel<TPixel>;

        // ReadOnlySpan<byte>
        public static unsafe void Load<TPixel>(
            ReadOnlySpan<byte> data,
            Image<TPixel> image,
            IImageDecoder decoder)
            where TPixel : unmanaged, IPixel<TPixel>;
        public static unsafe void Load<TPixel>(
            ReadOnlySpan<byte> data,
            Image<TPixel> image,
            out IImageFormat format)
            where TPixel : unmanaged, IPixel<TPixel>;

        // Stream
        public static void Load<TPixel>(Stream stream, Image<TPixel> image, IImageDecoder decoder)
            where TPixel : unmanaged, IPixel<TPixel>;
        public static Task LoadAsync<TPixel>(
            Stream stream,
            Image<TPixel> image,
            IImageDecoder decoder,
            CancellationToken cancellationToken = default)
            where TPixel : unmanaged, IPixel<TPixel>;
        public static void Load<TPixel>(Stream stream, Image<TPixel> image, out IImageFormat format)
            where TPixel : unmanaged, IPixel<TPixel>;
        public static async Task<IImageFormat> LoadWithFormatAsync<TPixel>(
            Stream stream,
            Image<TPixel> image,
            CancellationToken cancellationToken = default)
            where TPixel : unmanaged, IPixel<TPixel>;

        // string
        public static Task LoadAsync<TPixel>(
            string path,
            Image<TPixel> image,
            IImageDecoder decoder,
            CancellationToken cancellationToken = default)
            where TPixel : unmanaged, IPixel<TPixel>;
        public static void Load<TPixel>(string path, Image<TPixel> image, out IImageFormat format)
            where TPixel : unmanaged, IPixel<TPixel>;
        public static void Load<TPixel>(string path, Image<TPixel> image, IImageDecoder decoder)
            where TPixel : unmanaged, IPixel<TPixel>;
    }
}

NOTE: opening the PR to have the CI run on it, but I still need to add unit tests.

Use case example

The new APIs allow for eg. GPU processing (assuming DirectX 12 buffers on NUMA architecture) like so (using ComputeSharp):

IImageInfo info = Image.Identify("cat.jpg", out IImageFormat format);

using ReadWriteBuffer<uint> gpuBuffer = Gpu.Default.AllocateReadWriteBuffer<uint>(info.Width * info.Height);

using (var buffer = gpuBuffer.GetBufferForWrite())
fixed (void* pointer = buffer.Span)
using (var image = Image.WrapMemory<Rgb24>(pointer, info.Width, info.Height))
{
    Image.Load("cat.jpg", image, out _);
}

// Do GPU processing...

using (var buffer = gpuBuffer.GetBufferForRead())
fixed (void* pointer = buffer.Span)
using (var image = Image.WrapMemory<Rgb24>(pointer, info.Width, info.Height))
{
    image.SaveAsJpeg("cat_processed.jpg");
}

The big advantage is that we can completely skip one entire copy of the image, as we can use the new APIs to load/decode the image directly into the temporary upload buffer on the GPU. Without these APIs, we'd instead have to load the image into CPU memory, then copy from there to the temporary upload GPU buffer, then copy again from there to the shader visible GPU buffer.

Open questions

❓ Right now I just throw a NotSupportedException for GIF files. Is this ok?
❓ Right now I just throw an ArgumentException if the image has the wrong size. Do we want to try to mutate it instead?

@@ -67,7 +67,7 @@ protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetad
public int Height => this.size.Height;

/// <inheritdoc/>
public ImageMetadata Metadata { get; }
public ImageMetadata Metadata { get; internal set; }
Copy link

@DaZombieKiller DaZombieKiller Jan 4, 2021

Choose a reason for hiding this comment

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

Should there be a public API for setting this somewhere? With the new setter being internal, a third party image decoder can't properly re-use an Image as it can't change the metadata.

Copy link
Member

Choose a reason for hiding this comment

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

You set individual properties on the metadata not the metadata itself.

See ImageMetadata

Choose a reason for hiding this comment

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

I see, that makes sense. Seeing different *Metadata classes had me confused for a moment, not noticing that ImageMetadata is sealed.

Copy link
Member

@antonfirsov antonfirsov Jan 4, 2021

Choose a reason for hiding this comment

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

@Sergio0694 we should rather fill the existing metadata in the decoders. Reasons: (1) We don't want the setter to be public. (2) 3rd party decoders should be able to function the same way our decoders do.

Copy link
Member

@antonfirsov antonfirsov left a comment

Choose a reason for hiding this comment

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

We should decide on API questions before proceeding.

I like the idea of having a Load(Async) overloads that work on an existing Image<T> instance over the static variants in the original proposal, since they separate the concerns of wrapping the memory and loading the image. This reduces the number of overloads we need to create and maintain to NumOf(WrapVariants) + NumOf(LoadVariants) from NumOf(WrapVariants) * NumOf(LoadVariants). (The numbers will likely keep growing.) On the other hand, the user needs to know the dimensions in advance, I'm not sure if it's always possible. @DaZombieKiller? (Also see my notes in the very end.)

If we stick with the instance API, we need to decide on the location of the new overloads. Here my preference is to have them as extension methods in either AdvancedImageExtensions or a new extension class under the Advanced namespace.

We need to decide if we are ok with the breaking changes of extending IImageDecoder (break a small number of advanced users to deliver a feature for another group of advanced users ... and kinda also break our promise to not break).

@JimBobSquarePants suggestions?


Right now I just throw an ArgumentException if the image has the wrong size. Do we want to try to mutate it instead?

If the stream image size is bigger, we clearly need to throw. For other cases, I see 3 options here:

  1. The user wants to change dimensions (mutate)
  2. The user wants to decode to a sub rectangle of the target image
  3. The user is expecting an exception if sizes mismatch (current behavior)

/// <exception cref="UnknownImageFormatException">Image format not recognised.</exception>
/// <exception cref="InvalidImageContentException">Image contains invalid content.</exception>
/// <typeparam name="TPixel">The pixel format.</typeparam>
public static void Load<TPixel>(Stream stream, Image<TPixel> image, IImageDecoder decoder)
Copy link
Member

Choose a reason for hiding this comment

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

I would either make these instance methods on Image or extension methods in AdvancedImageExtensions.

Copy link
Member

Choose a reason for hiding this comment

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

I'd say AdvancedImageExtensions

@@ -67,7 +67,7 @@ protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetad
public int Height => this.size.Height;

/// <inheritdoc/>
public ImageMetadata Metadata { get; }
public ImageMetadata Metadata { get; internal set; }
Copy link
Member

@antonfirsov antonfirsov Jan 4, 2021

Choose a reason for hiding this comment

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

@Sergio0694 we should rather fill the existing metadata in the decoders. Reasons: (1) We don't want the setter to be public. (2) 3rd party decoders should be able to function the same way our decoders do.

/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="image">The target <see cref="Image{TPixel}"/> instance.</param>
// TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110)
void Decode<TPixel>(Stream stream, Image<TPixel> image)
Copy link
Member

@antonfirsov antonfirsov Jan 4, 2021

Choose a reason for hiding this comment

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

This addition is gonna be a breaking change for users who implemented their own IImageDecoder. The reason @DaZombieKiller proposed IImageMemoryDecoder was to avoid this.

I'm undecided on this, with a slight preference to not break.

Copy link
Member

Choose a reason for hiding this comment

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

Hmmmm.... It's a fairly fundamental break and there are decoder out in the wild. I say new interface.

Copy link

@DaZombieKiller DaZombieKiller Jan 5, 2021

Choose a reason for hiding this comment

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

In that case, now comes the hard part: naming it. IImageMemoryDecoder doesn't seem descriptive enough, especially since it's no longer operating on Memory<T> like in the original proposal. Maybe IImageTargetDecoder or IImageTargetedDecoder, based on the name of this PR?

That also gives rise to another open question: What is expected to happen if you attempt to decode into an Image with a decoder that doesn't support doing that? It could potentially execute a slow path with a copy, but since this is an API intended for high-performance scenarios I think throwing makes more sense.

Copy link
Member

Choose a reason for hiding this comment

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

We'd have to expose the interface and use the type as the method parameter type. Can't call what you don't have then.

Choose a reason for hiding this comment

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

What about the overloads that don't take an IImageDecoder currently? Would they just be removed?

Copy link
Member

Choose a reason for hiding this comment

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

When someone calls the new Image.Load without a specified decoder we will loop through our config to find a matching decoder that implements the interface. If there is none it will through.

I generally don't recommend people use specific decoders unless they have to as they skip the format validation checks.

{
if (this.ImageWidth != image.Width || this.ImageHeight != image.Height)
{
ThrowHelper.ThrowArgumentException("The input image has an invalid size", nameof(image));
Copy link
Member

@antonfirsov antonfirsov Jan 4, 2021

Choose a reason for hiding this comment

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

Suggested change
ThrowHelper.ThrowArgumentException("The input image has an invalid size", nameof(image));
ThrowHelper.ThrowArgumentException("The input image should match the dimensions of the image in the stream.", nameof(image));

Same for the other decoders. In Jpeg we can throw on this much earlier in the decoding workflow.

@DaZombieKiller
Copy link

@antonfirsov

On the other hand, the user needs to know the dimensions in advance, I'm not sure if it's always possible. @DaZombieKiller?

In my use case they're always known as I just use Image.Identify to work out the dimensions and format so I can create a texture to fill.

ptasev and others added 24 commits January 12, 2021 20:15
Add PremultiplyAlpha to ResizeOptions
…rsion

Vectorize Jpeg Encoder Color Conversion
Assembly for loading in the loop went from:
```asm
vmovss xmm2, [rax]
vbroadcastss xmm2, xmm2
vmovss xmm3, [rax+4]
vbroadcastss xmm3, xmm3
vinsertf128 ymm2, ymm2, xmm3, 1
```
To:
```asm
vmovsd xmm3, [rax]
vbroadcastsd ymm3, xmm3
vpermps ymm3, ymm1, ymm3
```
Speed improvements to resize kernel (w/ SIMD)
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.

10 participants