0

I had replaced some dodgy GDI+ routines with SharpDX to load a bitonal TIFF image from a Stream, render text on it, then save it back to TIFF format as a Stream.

But the SharpDX code is taking much longer to do the same thing, and I'm wondering if I am doing something wrong.

As you can see from the sample here, I have 2 different functions:

  • RenderImageFromExistingImage
  • SaveRenderedImage

    using System;
    using System.Diagnostics;
    using System.IO;
    using SharpDX;
    using SharpDX.Direct2D1;
    using SharpDX.DirectWrite;
    using SharpDX.DXGI;
    using SharpDX.WIC;
    using Factory = SharpDX.Direct2D1.Factory;
    using FactoryType = SharpDX.Direct2D1.FactoryType;
    using PixelFormat = SharpDX.WIC.PixelFormat;
    using WicBitmap = SharpDX.WIC.Bitmap;
    
    public class ImageCreator2
    {
        private static ImagingFactory _wicFactory;
        private static Factory _d2DFactory;
        private static SharpDX.DirectWrite.Factory _dwFactory;
        private int _imageWidth = 1000, _imageHeight = 500;
        private readonly int _imageDpi = 96;
    
        public ImageCreator2()
        {
            _wicFactory = new ImagingFactory();
            _d2DFactory = new Factory(FactoryType.SingleThreaded);
            _dwFactory = new SharpDX.DirectWrite.Factory(SharpDX.DirectWrite.FactoryType.Shared);
        }
    
        private void RenderImage(WicRenderTarget renderTarget)
        {
            using (var blackBrush = new SolidColorBrush(renderTarget, Color4.Black))
            using (var tformat = new TextFormat(_dwFactory, "Arial", 30f))
            using (var tformat2 = new TextFormat(_dwFactory, "Arial", 11f))
            {
                renderTarget.BeginDraw();
                renderTarget.Clear(Color.White);
                renderTarget.DrawText("TEST", tformat, new RectangleF(300f, 30f, 100f, 20f), blackBrush);
                renderTarget.DrawText("MORE TEST", tformat2, new RectangleF(30f, 150f, 100f, 20f), blackBrush);
                renderTarget.DrawLine(new Vector2(0f, 25f), new Vector2(500f, 25f), blackBrush);
                renderTarget.DrawLine(new Vector2(0f, 210f), new Vector2(500f, 210f), blackBrush);
                renderTarget.EndDraw();
            }
        }
    
        public void BuildImageFromExistingImage(byte[] image, Stream systemStream)
        {
            using (var checkStream = new MemoryStream(image))
            using (
                var inDecoder = new BitmapDecoder(_wicFactory, checkStream, DecodeOptions.CacheOnDemand))
            using (var converter = new FormatConverter(_wicFactory))
            {
                if (inDecoder.FrameCount > 0)
                {
                    using (var frame = inDecoder.GetFrame(0))
                    {
                        converter.Initialize(frame, PixelFormat.Format32bppPRGBA, BitmapDitherType.None, null, 0.0f,
                            BitmapPaletteType.MedianCut);
                        _imageWidth = converter.Size.Width;
                        _imageHeight = converter.Size.Height;
                    }
                }
                else
                {
                    throw new Exception();
                }
                var renderProperties = new RenderTargetProperties(
                    RenderTargetType.Software,
                    new SharpDX.Direct2D1.PixelFormat(Format.Unknown, AlphaMode.Unknown),
                    _imageDpi,
                    _imageDpi,
                    RenderTargetUsage.None,
                    FeatureLevel.Level_DEFAULT);
                using (var wicBitmap = new WicBitmap(
                    _wicFactory,
                    converter,
                    BitmapCreateCacheOption.CacheOnDemand))
    
                using (
                    var renderTarget = new WicRenderTarget(_d2DFactory, wicBitmap,
                        renderProperties))
                {
                    RenderImage(renderTarget);
    
                    using (
                        var encoder = new BitmapEncoder(_wicFactory,
                            ContainerFormatGuids.Tiff))
                    {
                        encoder.Initialize(systemStream);
    
                        using (var bitmapFrameEncode = new BitmapFrameEncode(encoder))
                        {
                            var pixFormat = PixelFormat.Format32bppPRGBA;
                            bitmapFrameEncode.Initialize();
                            bitmapFrameEncode.SetSize(_imageWidth, _imageHeight);
                            bitmapFrameEncode.SetResolution(96, 96);
                            bitmapFrameEncode.SetPixelFormat(ref pixFormat);
    
                            //This takes 30-40ms per image.
                            var watch = new Stopwatch();
                            try
                            {
                                watch.Start();
                                bitmapFrameEncode.WriteSource(wicBitmap);
                            }
                            finally
                            {
                                watch.Stop();
                            }
                            Console.WriteLine("Saved real image in {0} ms.",
                                watch.Elapsed.TotalMilliseconds);
    
                            bitmapFrameEncode.Commit();
                        }
                        encoder.Commit();
                    }
                }
            }
        }
    
        public void SaveRenderedImage(Stream systemStream)
        {
            var renderProperties = new RenderTargetProperties(
                RenderTargetType.Default,
                new SharpDX.Direct2D1.PixelFormat(Format.Unknown, AlphaMode.Unknown),
                _imageDpi,
                _imageDpi,
                RenderTargetUsage.None,
                FeatureLevel.Level_DEFAULT);
    
            using (var wicBitmap = new WicBitmap(
                _wicFactory,
                _imageWidth,
                _imageHeight,
                PixelFormat.Format32bppBGR,
                BitmapCreateCacheOption.CacheOnDemand
                ))
            using (var renderTarget = new WicRenderTarget(_d2DFactory, wicBitmap, renderProperties))
            {
                RenderImage(renderTarget);
    
                using (
                    var encoder = new BitmapEncoder(_wicFactory,
                        ContainerFormatGuids.Tiff))
                {
                    encoder.Initialize(systemStream);
    
                    using (var bitmapFrameEncode = new BitmapFrameEncode(encoder))
                    {
                        bitmapFrameEncode.Initialize();
                        bitmapFrameEncode.SetSize(_imageWidth, _imageHeight);
                        bitmapFrameEncode.SetResolution(_imageDpi, _imageDpi);
                        //This takes 8-10ms per image.
                        var watch = new Stopwatch();
                        try
                        {
                            watch.Start();
                            bitmapFrameEncode.WriteSource(wicBitmap);
                        }
                        finally
                        {
                            watch.Stop();
                        }
                        Console.WriteLine("Saved generated image in {0} ms.",
                            watch.Elapsed.TotalMilliseconds);
    
                        bitmapFrameEncode.Commit();
                    }
                    encoder.Commit();
                }
            }
        }
    }
    

They are mostly identical, and do roughly the same thing, except the first one (RenderImageFromExistingImage) takes in an existing 1000x500 bitonal TIFF image to use as the base image, and the second one (SaveRenderedImage) creates a similarly-sized WIC Bitmap from scratch.

The function that takes an existing image takes about 30-40ms to execute, with the bulk of that time (~30ms) taken up by BitmapFrameEncode.WriteSource. This function is equivalent to the GDI+ code that was replaced.

The function that creates a WicBitmap from scratch takes 8-10ms to execute, without taking significant time in BitmapFrameEncode.WriteSource, which is roughly the same amount of time as the GDI+ function which it replaced. The only difference is this function is not loading a prexisting image, which is what I need.

Why is BitmapFrameEncode.WriteSource (which appears to be a thin wrapper around IWICBitmapFrameEncode) so slow in BuildImageFromExistingImage, compared to SaveRenderedImage?

My guess is that BuildImageFromExistingImage is slower because it is doing an extra conversion on the incoming image (using FormatConverter), to convert it to a pixel format that D2D will handle, and that the time penalty for doing this does not come into play until BitmapFrameEncode.WriteSource happens.

Is there something I am doing wrong? Or are WIC (Windows Imaging Components) just slow compared to GDI+-based calls?

Ideally I need the first case (BuildImageFromExistingImage) to be as fast as the GDI+ code it replaced, and would expect that it should be possible to make it as fast, if not faster, than the GDI+ code it is intended to replace.

Benji L.
  • 197
  • 11

1 Answers1

-1

On Windows 7 and above, GDI+ uses the WIC encoders and decoders, so there's no difference in the actual encoding step. If your two methods produce the same pixels, they'll encode at the same speed, and that speed will be the same as from GDI+.

Although it may seem like WriteSource() is the bottleneck in your example, that's a bit misleading. WIC uses a lazy pipeline for its processing, meaning that all steps that you perform are delayed until the pixels are requested with a call to CopyPixels(). In your case, since you never call CopyPixels() yourself, that call is made by the WriteSource(), and all the processing is performed at that time.

I'm not really familiar with SharpDX, but my guess is you probably want to use BitmapCreateCacheOption.CacheOnLoad when creating the WicBitmap object you use for your drawing. Allowing the encoder to materialize the bitmap means it will do it one scanline at a time, and that probably has a negative effect on your drawing performance. I believe the CacheOnLoad option materializes the bitmap immediately, so it will be decoded and converted at that time instead of during the encode. If nothing else, that may help you isolate the bottleneck.

Also, I don't know if this will cause a performance issue, but BitmapPaletteType.MedianCut is only intended to be used for index color pixel types. Normally, you would use BitmapPaletteType.Custom when you don't require quantization.

And finally, as far as I know, the canonical pixel format for Direct2D is PixelFormat.Format32bppPBGRA. You may incur additional overhead by using the PRGBA variant.

See here for an example doing something similar to yours.

saucecontrol
  • 1,446
  • 15
  • 17
  • Thanks for the tips and explanation, I kinda figured that `WriteSource()` is just where everything actually happened. `CacheOnLoad` vs `CacheOnDemand` does not make an appreciable difference, neither does `Format32bppPBGRA` vs `Format32bppPRGBA`. I will try `BitmapPaletteType.Custom` and see what results I get. – Benji L. Jun 02 '16 at 23:13
  • Does the `CacheOnLoad` shift the timing around at all? I wouldn't expect it to make the overall execution time decrease, but it should have some impact on the `WriteSource` call. Also, you might want to check the value of `pixFormat` after your call to `bitmapFrameEncode.SetPixelFormat(ref pixFormat)`. If the format of your input image isn't supported by the encoder, it will do a conversion during save and will tell you the format it will be converting to. That would tell you if the save itself is incurring a conversion cost. – saucecontrol Jun 02 '16 at 23:41