Skip to content

Conversation

@woutware
Copy link
Contributor

@woutware woutware commented Apr 29, 2018

Please check thoroughly, I think I might have overlooked some conditions for skipping the blending in the FillProcessor.

Also I haven't done the FillRegionProcessor yet, I'm gonna need help with that.

The speedup with the image constructor with clear color param compared to the fill is pretty nice! My whole test draw is about 10% faster now overall.

Copy link
Member

@tocsoft tocsoft left a comment

Choose a reason for hiding this comment

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

just a tweak needed to the logic of activating the fast path

var solidBrush = this.brush as SolidBrush<TPixel>;

// If there's no reason for blending, then avoid it.
if (solidBrush != null && this.options.BlendPercentage == 1f && solidBrush.Color.ToVector4().W == 1f)
Copy link
Member

Choose a reason for hiding this comment

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

fast path should be activated when solidBrush != null and any of these things are true:

this.options.BlenderMode == PixelBlenderMode.Normal && this.options.BlendPercentage == 1f && solidBrush.Color.ToVector4().W == 1f
OR

this.options.BlenderMode == PixelBlenderMode.Over && this.options.BlendPercentage == 1f && solidBrush.Color.ToVector4().W == 1f

OR

this.options.BlenderMode == PixelBlenderMode.Src

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed it, thanks for the help here!

@tocsoft
Copy link
Member

tocsoft commented Apr 29, 2018

you will find its nigh on impossible to apply the same sort of fast path logic to the region filling, as we need to take into account anti-aliasing at region edges which in turn requires applying pixel blending.

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.

Good start, would be even better if we could move the whole logic into SolidBrush<T> but it's not a trivial job, so we can do it later.

I would like to add better assertions to our Fill/Blend tests before we merge this. Gonna do a separate PR for that.

/// <summary>
/// Gets the clear color to initialize the image frame pixels with.
/// </summary>
internal TPixel? ClearColor => this.clearColor;
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if I like this.

Does it worth to pollute our object model in this manner just to make it easier to add frames of a "default" color when producing gif-s manually? (0.01% of ImageSharp use-cases I guess).
@JimBobSquarePants @tocsoft thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not a fan of nullable type properties on classes. I'm voting no.

Copy link
Contributor Author

@woutware woutware Apr 30, 2018

Choose a reason for hiding this comment

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

The thought behind it is that if you create an image with a background color, all its frames will have automatically that background color, which could be reasonably expected from a user stand point. Without it you'd have the first frame with the background color, and if the user adds another frame with ImageFrameCollection.CreateFrame(), it wouldn't have that background color.

What's the reason for not having nullable type properties on a class?

Copy link
Member

Choose a reason for hiding this comment

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

My issue with nullable properties is that you cannot now be confident as a consumer of their value. TPixel is a struct anyway so doesn’t need to be nullable. Assign a default(TPixel) initial value.

Copy link
Member

Choose a reason for hiding this comment

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

if we have this atall you would want to distinguish between default and not set because default(TPixel) would be == Transparent and you want to skip the code path if its not set at all.

Copy link
Member

Choose a reason for hiding this comment

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

oh yeah.. your right, ignore that in that case we can just skip the logic on default(TPixel) 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed with the default(TPixel) part, so you suggest always initializing the image frame with the clearColor then instead of using memoryManager.AllocateClean2D?

I don't agree with the nullable reasoning, if it's properly documented and default values are not illogical I don't see a reason to refrain from using certain language features, if TPixel were a class it'd have been the same. In this case non-nullable works as well, but in general if nullable is better suitable for a particular situation I wouldn't handicap the design/api out of fear users are not capable enough of using it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alright, committed the change to non-nullable clear color.

Copy link
Member

Choose a reason for hiding this comment

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

It wasn't a question of handicapping the API nor one of fear. Nullable properties have their place, just not in any API where you hold all the cards.

Take for example the new ResponseHeaders class from Microsoft.Http.Abstractions. The ContentLength property there is of type long? this makes perfect sense since the HttpResponse might not provide a content-length header.

We have absolute control over that property and assigning it so Nullable would have been the incorrect option design wise.

On that note... I wonder whether this should be a property of the ImageFrame<T> itself? That way we base any non-background color assigned frames on the root frame which would keep the property in line with the Width/Height properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've moved it to ImageFrame, it's indeed more consistent with the Width/Height properties.


// If there's no reason for blending, then avoid it.
if (solidBrush != null &&
(
Copy link
Member

Choose a reason for hiding this comment

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

I refactor this logic into an IsSolidBrushWithoutBlending property or similar for better readability.

@antonfirsov
Copy link
Member

Opened #553 to deal with the coverage issues. Hope it can be merged quickly.

@codecov
Copy link

codecov bot commented Apr 30, 2018

Codecov Report

Merging #552 into master will increase coverage by 0.02%.
The diff coverage is 94.61%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #552      +/-   ##
==========================================
+ Coverage   88.26%   88.28%   +0.02%     
==========================================
  Files         838      838              
  Lines       35096    35186      +90     
  Branches     2534     2538       +4     
==========================================
+ Hits        30976    31064      +88     
  Misses       3377     3377              
- Partials      743      745       +2
Impacted Files Coverage Δ
...ageSharp.Tests/Image/ImageFramesCollectionTests.cs 100% <100%> (ø) ⬆️
tests/ImageSharp.Tests/Image/ImageTests.cs 100% <100%> (ø) ⬆️
...geSharp.Tests/TestUtilities/TestImageExtensions.cs 81.59% <100%> (+0.34%) ⬆️
src/ImageSharp/ImageFrame{TPixel}.cs 86.59% <100%> (+3.93%) ⬆️
...g/Processing/Drawing/Brushes/SolidBrush{TPixel}.cs 96% <100%> (+0.54%) ⬆️
src/ImageSharp/Image{TPixel}.cs 81.63% <80%> (-1.3%) ⬇️
...ing/Processing/Drawing/Processors/FillProcessor.cs 92.45% <87.87%> (+3.56%) ⬆️
src/ImageSharp/ImageFrameCollection.cs 91.89% <93.75%> (+0.58%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6431723...72cb50c. Read the comment docs.

/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="clearColor">The color to clear the image with.</param>
internal ImageFrame(Configuration configuration, int width, int height, TPixel clearColor)
Copy link
Member

Choose a reason for hiding this comment

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

Could you please rename this to backgroundColor. Clear would be a verb.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed it. It's probably more user understandable, I liked the clear color as it implies better it's an one time initialization thing rather than a permanent state.

/// <summary>
/// Gets the clear color to initialize the image frame pixels with.
/// </summary>
internal TPixel? ClearColor => this.clearColor;
Copy link
Member

Choose a reason for hiding this comment

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

It wasn't a question of handicapping the API nor one of fear. Nullable properties have their place, just not in any API where you hold all the cards.

Take for example the new ResponseHeaders class from Microsoft.Http.Abstractions. The ContentLength property there is of type long? this makes perfect sense since the HttpResponse might not provide a content-length header.

We have absolute control over that property and assigning it so Nullable would have been the incorrect option design wise.

On that note... I wonder whether this should be a property of the ImageFrame<T> itself? That way we base any non-background color assigned frames on the root frame which would keep the property in line with the Width/Height properties.


this.MemoryManager = configuration.MemoryManager;
this.PixelBuffer = this.MemoryManager.Allocate2D<TPixel>(width, height, false);
this.Clear(configuration.ParallelOptions, clearColor);
Copy link
Member

Choose a reason for hiding this comment

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

this.PixelBuffer.Span.Fill()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I left in the parallel clearing, on my laptop it's still 10%-20% faster and I can really use all the speed there is as the drawing bit is quite a bit slower at the moment. The user can always set the parallel options to use 1 thread if he insists.

Copy link
Member

Choose a reason for hiding this comment

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

👍

/// </summary>
/// <param name="parallelOptions">The parallel options.</param>
/// <param name="value">The value to initialize the bitmap with.</param>
public void Clear(ParallelOptions parallelOptions, TPixel value) {
Copy link
Member

@JimBobSquarePants JimBobSquarePants Apr 30, 2018

Choose a reason for hiding this comment

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

Clearing is a process and belongs in the processing namespace; we don't want to clutter the ImageFrame class. We also never want to expose the ParallelOptions as a field. They are part of the Configuration Class and are globally set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed method from public to internal. I'd just see this as initialization rather than processing, akin to how Allocate2D allows initialization, which isn't in processing either, but doesn't allow a default value.

Copy link
Member

Choose a reason for hiding this comment

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

👍

/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="clearColor">The color to initialize the pixels with.</param>
public Image(Configuration configuration, int width, int height, TPixel clearColor)
Copy link
Member

Choose a reason for hiding this comment

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

clearColor => backgroundColor

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

/// <summary>
/// Gets the clear color to initialize the image frame pixels with.
/// </summary>
internal TPixel ClearColor => this.clearColor;
Copy link
Member

Choose a reason for hiding this comment

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

Move this to the ImageFrame<TPixel> and add to the constructor there. Use RootFrame as a default for subsequent frames

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, good point there.

{
private readonly Configuration configuration;
private readonly ImageFrameCollection<TPixel> frames;
private readonly TPixel clearColor;
Copy link
Member

Choose a reason for hiding this comment

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

No need for field. Assign straight to the property. (When you move all this to ImageFrame)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

/// <param name="buffer">The buffer</param>
/// <param name="value">The value to fill the buffer with.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Fill<T>(this IBuffer<T> buffer, T value)
Copy link
Member

Choose a reason for hiding this comment

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

Does this have a caller? The above code works directly on the Span

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed again, used this in one of my intermediate experiments I think.

@antonfirsov
Copy link
Member

From the Separation of Concerns point of view, I'm still not a big fan of introducing a BackgroundColor property on an imaging API. (It would be fine on a canvas API!)

But if you guys find it powerful and useful & vote to keep it, I would not resists.

One question:

  • Do we still need BackgroundColorProcessor?
  • If yes:
    • Shouln't we optimize it as well?
    • Shouldn't it change the ImageFrame.BackgroundColor for all frames?

@woutware
Copy link
Contributor Author

woutware commented Apr 30, 2018

@antonfirsov I can't comment on the BackgroundColorProcessor, but remember that before this change the pixels were initialized with zeroes by AllocateClean2D. If you're 100% puristic, you'd be making a processor for this as well, which I think at this level is overkill. You can consider the one time initializion of the pixels as integral part of the allocation similarly to how AllocateClean2D does it.

The change is a tremendous speedup: previously you'd always have 1) the initialization with zeroes, and 2) the slow Fill processor. Now you're skipping 2), and made 1) configurable with a default value. So you're basically twice as fast.

Some measured timings:

alloc + fill 00:00:00.0205244, alloc with background color 00:00:00.0093514

When not disposing the bitmap, so with a fresh alloc each time the difference becomes pretty much zero by the way, I'd have expected more difference here:

alloc + fill 00:00:00.0755986, alloc with background color 00:00:00.0741654

@antonfirsov
Copy link
Member

antonfirsov commented Apr 30, 2018

@woutware the constructor taking backgroundColor + it's auto-filling optimization are cool things, I have no issue with them!

The purist part of me just wants to drop the background color parameter after initialization, and leave the post-constructor job of initializing new frames with the proper color to the user. Only very a small minority of our users do manual multi-frame gif manipulation!

@woutware
Copy link
Contributor Author

I've no preference either way, I'll be using single frame myself only, so I'm fine with whatever fits the current philosophy best. Having said that, if you create an Image with a background color, it would feel more logical to me that new image frames have that same background color automatically. But again it's pretty much equal to me either way.

@antonfirsov
Copy link
Member

antonfirsov commented Apr 30, 2018

It's @JimBobSquarePants's job to decide it from now. He is the Grand High Eternal Dictator! 😆

(+ my question abut BackgroundColorProcessor still stands, mostly addressed to him.)

@JimBobSquarePants
Copy link
Member

@antonfirsov BackgroundColor does operate on all frames.

protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)

We need it because it performs a different action, doesn't require a brush (therefore can live in ImageSharp) and can operate only on the background layer (i.e you cannot overwrite original color data, only blend against it.)

It's super useful for padding out areas of non-transparent image formats following ResizeMode.Pad

/// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param>
/// <param name="backgroundColor">The color to clear the image with.</param>
internal ImageFrame(Configuration configuration, int width, int height, TPixel backgroundColor)
Copy link
Member

Choose a reason for hiding this comment

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

This is good! I actually wanted to introduce the configuration as a parameter and drop the MemoryManager overload so we're halfway there.

@antonfirsov
Copy link
Member

@JimBobSquarePants thanks, I understand now!

Shouldn't BackgroundColorProcessor alter ImageFrame.BackgroundColor (if we keep the property)?

@JimBobSquarePants
Copy link
Member

I think I get @antonfirsov 's issue with the BackgroundColor as a property. As soon as anyone writes to the image then it becomes invalid.

@woutware I think we'll have to drop the convenience of the property and simply rely on setting the background color per frame. (This is seriously fine grained stuff anyway so I doubt it will be an issue.)

@JimBobSquarePants
Copy link
Member

We're nearly there with this, looking forward to seeing it merged! 👍

@woutware
Copy link
Contributor Author

woutware commented May 1, 2018

Alright, removed the ImageFrame.Background property, and just using default if the user creates a new frame after the fact.

@JimBobSquarePants probably add some comments to the BackgroundColorProcessor code that you wrote here explaining the thoughts behind it, that'll be super useful for future users.

}

/// <inheritdoc/>
public ImageFrame<TPixel> CreateFrame()
Copy link
Member

Choose a reason for hiding this comment

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

Let's create an overload for this that accepts a color, then we cover all bases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see the method implements a member from interface IImageFrameCollection, wouldn't it be nice to change it to:

CreateFrame(TPixel backgroundColor = default)

Copy link
Member

Choose a reason for hiding this comment

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

I think we don’t actually need the interface. I can’t see why someone would need to implement it themselves.

Copy link
Member

@antonfirsov antonfirsov May 1, 2018

Choose a reason for hiding this comment

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

@JimBobSquarePants I remember having a conversation about optional arguments, with the conclusion that we should prefer overloads instead because of better binary compatibility.

Is this still a thing we should follow?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, always choose overloads.

woutware and others added 4 commits May 1, 2018 15:14
…nd added backgroundColor parameter to ImageFrameCollection.CreateFrame() method with default.
# Conflicts:
#	src/ImageSharp.Drawing/Processing/Drawing/Processors/FillProcessor.cs
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.

Pushed a few changes:

  • Overload CreateFrame() instead of optional arg
  • Minor code cleanups
  • Tests Tests Tests!

I think we are fine to merge this now!

@woutware
Copy link
Contributor Author

woutware commented May 1, 2018

Yay! Thanks guys!

@JimBobSquarePants
Copy link
Member

Thanks @woutware for taking the time to put this together. Much appreciated! 👍

@JimBobSquarePants JimBobSquarePants merged commit 6b4de97 into SixLabors:master May 2, 2018
@maxvoxel8
Copy link
Contributor

maxvoxel8 commented Jul 9, 2018

you will find its nigh on impossible to apply the same sort of fast path logic to the region filling, as we need to take into account anti-aliasing at region edges which in turn requires applying pixel blending.

@tocsoft could the same logic be used if antialiasing is off? Or if a scanline is all ones/zeros?

antonfirsov pushed a commit to antonfirsov/ImageSharp that referenced this pull request Nov 11, 2019
…lend-performance

Issue 551 solidbrush blend performance
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants