-
-
Notifications
You must be signed in to change notification settings - Fork 888
Issue 551 solidbrush blend performance #552
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
Issue 551 solidbrush blend performance #552
Conversation
…meter. This is good for performance because this avoids first needing to clear the buffer with zeroes.
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 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) |
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.
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
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.
Changed it, thanks for the help here!
|
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. |
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.
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.
src/ImageSharp/Image{TPixel}.cs
Outdated
| /// <summary> | ||
| /// Gets the clear color to initialize the image frame pixels with. | ||
| /// </summary> | ||
| internal TPixel? ClearColor => this.clearColor; |
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 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?
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'm not a fan of nullable type properties on classes. I'm voting no.
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.
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?
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.
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.
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.
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.
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.
oh yeah.. your right, ignore that in that case we can just skip the logic on default(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.
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.
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.
Alright, committed the change to non-nullable clear color.
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.
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.
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'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 && | ||
| ( |
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 refactor this logic into an IsSolidBrushWithoutBlending property or similar for better readability.
…ub.com/woutware/ImageSharp into Issue-551-solidbrush-blend-performance
|
Opened #553 to deal with the coverage issues. Hope it can be merged quickly. |
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
src/ImageSharp/ImageFrame{TPixel}.cs
Outdated
| /// <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) |
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.
Could you please rename this to backgroundColor. Clear would be a verb.
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.
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.
src/ImageSharp/Image{TPixel}.cs
Outdated
| /// <summary> | ||
| /// Gets the clear color to initialize the image frame pixels with. | ||
| /// </summary> | ||
| internal TPixel? ClearColor => this.clearColor; |
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.
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.
src/ImageSharp/ImageFrame{TPixel}.cs
Outdated
|
|
||
| this.MemoryManager = configuration.MemoryManager; | ||
| this.PixelBuffer = this.MemoryManager.Allocate2D<TPixel>(width, height, false); | ||
| this.Clear(configuration.ParallelOptions, clearColor); |
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.
this.PixelBuffer.Span.Fill()?
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 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.
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.
👍
src/ImageSharp/ImageFrame{TPixel}.cs
Outdated
| /// </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) { |
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.
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.
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.
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.
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.
👍
src/ImageSharp/Image{TPixel}.cs
Outdated
| /// <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) |
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.
clearColor => backgroundColor
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.
Done!
src/ImageSharp/Image{TPixel}.cs
Outdated
| /// <summary> | ||
| /// Gets the clear color to initialize the image frame pixels with. | ||
| /// </summary> | ||
| internal TPixel ClearColor => this.clearColor; |
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.
Move this to the ImageFrame<TPixel> and add to the constructor there. Use RootFrame as a default for subsequent frames
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.
Done, good point there.
src/ImageSharp/Image{TPixel}.cs
Outdated
| { | ||
| private readonly Configuration configuration; | ||
| private readonly ImageFrameCollection<TPixel> frames; | ||
| private readonly TPixel clearColor; |
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 need for field. Assign straight to the property. (When you move all this to ImageFrame)
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.
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) |
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.
Does this have a caller? The above code works directly on the Span
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.
Removed again, used this in one of my intermediate experiments I think.
|
From the Separation of Concerns point of view, I'm still not a big fan of introducing a But if you guys find it powerful and useful & vote to keep it, I would not resists. One question:
|
|
@antonfirsov I can't comment on the 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: 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: |
|
@woutware the constructor taking 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! |
|
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. |
|
It's @JimBobSquarePants's job to decide it from now. He is the Grand High Eternal Dictator! 😆 (+ my question abut |
|
@antonfirsov ImageSharp/src/ImageSharp/Processing/Overlays/Processors/BackgroundColorProcessor.cs Line 43 in 01d0426
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 |
| /// <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) |
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.
This is good! I actually wanted to introduce the configuration as a parameter and drop the MemoryManager overload so we're halfway there.
|
@JimBobSquarePants thanks, I understand now! Shouldn't |
|
I think I get @antonfirsov 's issue with the @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.) |
|
We're nearly there with this, looking forward to seeing it merged! 👍 |
|
Alright, removed the ImageFrame.Background property, and just using @JimBobSquarePants probably add some comments to the |
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public ImageFrame<TPixel> CreateFrame() |
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.
Let's create an overload for this that accepts a color, then we cover all bases.
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 see the method implements a member from interface IImageFrameCollection, wouldn't it be nice to change it to:
CreateFrame(TPixel backgroundColor = default)
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 think we don’t actually need the interface. I can’t see why someone would need to implement it themselves.
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.
@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?
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.
Yeah, always choose overloads.
…nd added backgroundColor parameter to ImageFrameCollection.CreateFrame() method with default.
# Conflicts: # src/ImageSharp.Drawing/Processing/Drawing/Processors/FillProcessor.cs
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.
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!
|
Yay! Thanks guys! |
|
Thanks @woutware for taking the time to put this together. Much appreciated! 👍 |
@tocsoft could the same logic be used if antialiasing is off? Or if a scanline is all ones/zeros? |
…lend-performance Issue 551 solidbrush blend performance
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.