Runtime: Implement CachedBitmap functionality in System.Drawing

Created on 8 May 2020  路  14Comments  路  Source: dotnet/runtime

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).

api-approved area-System.Drawing

All 14 comments

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

Agreed, this would be really useful

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.

Well thought out, 馃憤

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".

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.

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+).

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);   
    }
}

@reflectronic interested in putting up a PR?

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.

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.

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 馃槃

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

I opened a pull request for the libgdiplus implementation, if anybody wants to take a look https://github.com/mono/libgdiplus/pull/654

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jkotas picture jkotas  路  3Comments

yahorsi picture yahorsi  路  3Comments

bencz picture bencz  路  3Comments

GitAntoinee picture GitAntoinee  路  3Comments

omariom picture omariom  路  3Comments