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

Implement CachedBitmap functionality in System.Drawing #8822

Closed
reflectronic opened this issue May 8, 2020 · 17 comments · Fixed by #8987
Closed

Implement CachedBitmap functionality in System.Drawing #8822

reflectronic opened this issue May 8, 2020 · 17 comments · Fixed by #8987
Assignees
Labels
api-approved (4) API was approved in API review, it can be implemented area-System.Drawing System.Drawing issues

Comments

@reflectronic
Copy link

Background

Drawing a Bitmap in GDI+ has relatively poor performance, since the image is stored in a device-independent format. The CachedBitmap class stores an image in a format that is optimized for display on a particular device. Thus, rendering an image stored in a CachedBitmap is fast, because no processing time is spent converting the image to the format required by the display device. Naturally, such a capability can substantially improve graphics performance in many scenarios. However, for whatever reason, it's not directly usable from the System.Drawing APIs.

API Proposal

This API is derived from this page of the GDI+ flat API. (I don't know what GdipEmfToWmfBits is doing on that page, but System.Drawing doesn't support saving metafiles anyway.)

namespace System.Drawing.Imaging
{
+   public sealed class CachedBitmap : MarshalByRefObject, IDisposable
+   {
+       public CachedBitmap(Bitmap bitmap, Graphics graphics);
+       public void Dispose();  
+   }
} 

namespace System.Drawing
{
    public sealed class Graphics : MarshalByRefObject, IDisposable, IDeviceContext
    {
+       public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y);	 
    }
}

As an aside: currently, libgdiplus doesn't implement this functionality. I imagine it would be fine to just PNSE when not on Windows for now (or, alternatively, making it "lie" about being a CachedBitmap, and simply calling DrawImage from DrawCachedBitmap).

@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-System.Drawing System.Drawing issues untriaged The team needs to look at this issue in the next triage labels May 8, 2020
@ghost
Copy link

ghost commented May 8, 2020

Tagging subscribers to this area: @safern, @tannergooding
Notify danmosemsft if you want to be subscribed.

@john-h-k
Copy link

john-h-k commented May 8, 2020

Agreed, this would be really useful

@reflectronic
Copy link
Author

I've implemented this on my own fork, with tests and doc comments: https://github.com/reflectronic/runtime/tree/cachedbitmap

As for performance, I have a benchmark comparing drawing with Bitmap and CachedBitmap:

Benchmark
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

using System.Drawing;
using System.Drawing.Imaging;
using System.Reflection;

namespace ConsoleApp1
{
    [SimpleJob(RuntimeMoniker.NetCoreApp50)]
    public class DrawingBenchmark
    {
        Image surface;
        Graphics graphics;

        Bitmap bitmap;
        CachedBitmap cachedBitmap;

        [GlobalSetup]
        public void GlobalSetup()
        {
            bitmap = new Bitmap(Assembly.GetExecutingAssembly().GetManifestResourceStream("Image"));

            surface = new Bitmap(bitmap.Width, bitmap.Height);
            graphics = Graphics.FromImage(surface);

            cachedBitmap = new CachedBitmap(bitmap, graphics);
        }

        [Benchmark]
        public void DrawBitmap()
        {
            graphics.DrawImage(bitmap, 0, 0);
        }

        [Benchmark]
        public void DrawCachedBitmap()
        {
            graphics.DrawCachedBitmap(cachedBitmap, 0, 0);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run<DrawingBenchmark>();
        }
    }
}
Method Mean Error StdDev
DrawBitmap 447.40 us 4.445 us 3.712 us
DrawCachedBitmap 96.92 us 0.325 us 0.271 us

As you can see, CachedBitmap is a pretty simple optimization that can bring a lot of value in various scenarios.

@patrickklaeren
Copy link

Well thought out, 👍

@tannergooding tannergooding added api-suggestion (1) Early API idea and discussion, it is NOT ready for implementation and removed untriaged The team needs to look at this issue in the next triage labels May 13, 2020
@tannergooding
Copy link
Member

I think overall the proposed API makes sense and matches the GDI+ API exposed. It might be interesting to see what would be required to add support to libgdiplus.

@safern, if you don't have any concerns, I think this could be marked "ready for review".

@safern
Copy link
Member

safern commented May 13, 2020

It might be interesting to see what would be required to add support to libgdiplus.

Yeah I think we should figure out the correct story in Unix for this... The problem I see is that libgdiplus doesn't ship as part of System.Drawing.Common and we use whatever is installed on the machine, so if people don't have the libgdiplus version that supports this we would need to communicate that correctly.

or, alternatively, making it "lie" about being a CachedBitmap, and simply calling DrawImage from DrawCachedBitmap

I think that could work as well for Linux story until we get proper support.

Also, should implement MarshalByRefObject? This type doesn't exist in Full Framework so I don't think it should.

Overall looks good other than the MarshalByRefObject question.

Also, just to have it in the issue info, WinForms already exposes a CachedBitmap class.

@reflectronic
Copy link
Author

reflectronic commented May 14, 2020

Also, should implement MarshalByRefObject? This type doesn't exist in Full Framework so I don't think it should.

Well, .NET Framework is really the only platform where MarshalByRefObject has significance. Maybe you are thinking of DispatchProxy? Okay, now I see what you mean, sorry for the misunderstanding. I guess it would be fine to remove it, although if the updated System.Drawing.Common package would work on .NET Framework, then there might be value.

Also, just to have it in the issue info, WinForms already exposes a CachedBitmap class.

I think you are thinking of WPF's CachedBitmap (which of course doesn't wrap GDI+).

@safern safern removed the api-suggestion (1) Early API idea and discussion, it is NOT ready for implementation label May 14, 2020
@terrajobst terrajobst added the api-approved (4) API was approved in API review, it can be implemented label Jun 25, 2020
@terrajobst
Copy link
Member

terrajobst commented Jun 25, 2020

Video

  • Looks good as proposed
namespace System.Drawing.Imaging
{
+   public sealed class CachedBitmap : MarshalByRefObject, IDisposable
+   {
+       public CachedBitmap(Bitmap bitmap, Graphics graphics);
+       public void Dispose();  
+   }
} 

namespace System.Drawing
{
    public sealed class Graphics : MarshalByRefObject, IDisposable, IDeviceContext
    {
+       public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y);   
    }
}

@safern
Copy link
Member

safern commented Jun 25, 2020

@reflectronic interested in putting up a PR?

@reflectronic
Copy link
Author

Yup, you can assign me. Right now, I am working on getting it implemented in libgdiplus as well. Hopefully, that can get reviewed and merged quickly once I put that up.

As for the versioning problem, it looks like libgdiplus exposes the version with GetLibgdiplusVersion(), so we can gracefully fail when CachedBitmap is not supported.

@safern
Copy link
Member

safern commented Jun 25, 2020

it looks like libgdiplus exposes the version with GetLibgdiplusVersion(), so we can gracefully fail when CachedBitmap is not supported.

GetLibgdiplusVersion is not supported in some old versions of libgdiplus so we should consider that when we implement that and trhowing if the version doesn't support it or if we can't get the version at all.

@safern safern added this to the Future milestone Jun 25, 2020
@safern
Copy link
Member

safern commented Jun 25, 2020

Btw, I set the milestone to future as there's no rush to implement this, but if we're able to get it in the next couple of weeks and make it into 5.0, great 😄

@bartonjs
Copy link
Member

@terrajobst The API you copy/pasted included the MarshalByRefObject base class. Didn't we say no to that part?

@reflectronic
Copy link
Author

I opened a pull request for the libgdiplus implementation, if anybody wants to take a look mono/libgdiplus#654

@danmoseley
Copy link
Member

@reflectronic it's been a while, but are you interested in offering a PR? It's not clear from above. If not, we can mark up for grabs.

As you probably know, we have removed the Unix implementation of Drawing.

@reflectronic
Copy link
Author

I would certainly like to offer a PR, but I think this needs to go through API review again, since #8833 missed this API. So, we would need something like:

namespace System.Drawing.Imaging
{
    public sealed class CachedBitmap : IDisposable
    {
        public CachedBitmap(Bitmap bitmap, Graphics graphics);
+       public IntPtr Handle { get; }
+       public static CachedBitmap FromHandle(IntPtr handle);
        public void Dispose();
    }
} 

namespace System.Drawing
{
    public sealed class Graphics : MarshalByRefObject, IDisposable, IDeviceContext
    {
        public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y);   
    }
}

Since this has to go through API review again, I also think we should change the namespace... System.Drawing.Imaging is mostly about advanced encoding and decoding functionality; I think System.Drawing.Drawing2D is much more appropriate, as the types in it relate to advanced drawing functionality (i.e. for use with Graphics). So, the final proposal would look something like:

namespace System.Drawing.Drawing2D
{
+   public sealed class CachedBitmap : IDisposable
+   {
+       public CachedBitmap(Bitmap bitmap, Graphics graphics);
+       public IntPtr Handle { get; }
+       public static CachedBitmap FromHandle(IntPtr handle);
+       public void Dispose();
+   }
} 

namespace System.Drawing
{
    public sealed class Graphics : MarshalByRefObject, IDisposable, IDeviceContext
    {
+       public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y);
    }
}

Does that look good to you? I don't know who the area owners are, since Santi left and I can't see who's in @dotnet/area-system-drawing. It would also be nice to have a status update re: triaging the other APIs proposed for System.Drawing... they are pretty straightforward and I think can be flipped ready-for-review pretty easily.

@JeremyKuhne JeremyKuhne modified the milestones: Future, .NET 8.0 Mar 14, 2023
@JeremyKuhne JeremyKuhne transferred this issue from dotnet/runtime Mar 14, 2023
@JeremyKuhne
Copy link
Member

@reflectronic I'm going to implement this as is to get this moving. I think there are reasonable arguments for both namespaces. The native handle APIs can be spun out into a separate API proposal when someone has a pressing need for it.

JeremyKuhne added a commit to JeremyKuhne/winforms that referenced this issue Apr 13, 2023
Implements GDI+ `CachedBitmap` wrapper which allows caching a device dependent copy of `Bitmap`. Rendering is signficantly faster (5x+), although it has a few caveats:

- It needs to be regenerated if color depth changes
- It cannot be rendered to a Graphics object that has a tranform other than translation (no rotation, scaling)

Fixes dotnet#8822
JeremyKuhne added a commit to JeremyKuhne/winforms that referenced this issue Apr 13, 2023
Implements GDI+ `CachedBitmap` wrapper which allows caching a device dependent copy of `Bitmap`. Rendering is signficantly faster (5x+), although it has a few caveats:

- It needs to be regenerated if color depth changes
- It cannot be rendered to a Graphics object that has a tranform other than translation (no rotation, scaling)

Fixes dotnet#8822
@ghost ghost added the 🚧 work in progress Work that is current in progress label Apr 13, 2023
JeremyKuhne added a commit that referenced this issue Apr 14, 2023
Implements GDI+ `CachedBitmap` wrapper which allows caching a device dependent copy of `Bitmap`. Rendering is signficantly faster (5x+), although it has a few caveats:

- It needs to be regenerated if color depth changes
- It cannot be rendered to a Graphics object that has a tranform other than translation (no rotation, scaling)

Fixes #8822
@ghost ghost removed the 🚧 work in progress Work that is current in progress label Apr 14, 2023
@ghost ghost removed this from the .NET 8.0 milestone Apr 14, 2023
hughbe added a commit to hughbe/win32 that referenced this issue Apr 17, 2023
@ghost ghost locked as resolved and limited conversation to collaborators May 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved (4) API was approved in API review, it can be implemented area-System.Drawing System.Drawing issues
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants