2

I've been working with a 32-bit C# WPF application that displays a big amount of large images (1080p in many cases) in a ListBox. The problem is that retaining a BitmapSource object in my C# object (that I've bound to) increases memory dramatically because the bytes of the BitmapSource that I have created are duplicated/copied before render. If I keep the BitmapSource object in order to reuse it or redisplay it somewhere else, I end up with multiple copies of the raw image bytes floating around due to the copy-before-render. More specifically, CopyPixels(Int32Rect sourceRect, Array pixels, int stride, int offset) is called before render. A memory/heap analysis with a stack trace confirms the idea that bytes are copied before render.

The only "workaround" I have created, which generates the BitmapSource every time it is needed is as follows:

ImageData data = _backendImage.getData();
SWIGTYPE_p_unsigned_char rawData = _backendImage.getRawData();
IntPtr dataPointer = SWIGTYPE_p_unsigned_char.getCPtr(rawData).Handle;
GC.Collect(); // forces garbage collection on not-displayed images
return Utilities.GenerateBitmapSource((int)_backendImage.getWidth(), (int)_backendImage.getHeight(), _backendImage.getByteOrder(), dataPointer, data.Count);

The final line there is my own function to actually generate a BitmapSource object and is out of the scope of this problem.

The workaround is extremely poor on performance, since I'm doing not just one, but two copies of the data (one into the BitmapSource, one to render) before every render into the ListBox. Keeping the BitmapSource around removes all of the duplicate copy operations, but is extremely heavy on memory usage.

Here is my ListBox XAML:

<ListBox Name="SlideShowListBox" ItemsSource="{Binding SlideData.PreviewData}" 
                 SelectedIndex="{Binding SelectedIndex}" 
                 ScrollViewer.VerticalScrollBarVisibility="Visible"
                 SelectionMode="Extended"
                 VirtualizingStackPanel.VirtualizationMode="Recycling">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <VirtualizingStackPanel Orientation="Horizontal" Margin="0, 2.5, 0, 2.5">
                <Label Content="{Binding data.ImageNumber}" VerticalAlignment="Top" Width="30" HorizontalContentAlignment="Right" Margin="0,-6.5,0,0"/>
                <Grid>
                    <Image Source="{Binding data.ImageThumbnail}" RenderOptions.BitmapScalingMode="HighQuality" VerticalAlignment="Top" HorizontalAlignment="Left"
                           Name="ListImage"
                           MaxWidth="{Binding ElementName=ListBoxItemSizer,
                                            Path=ActualWidth, Converter={ikriv:MathConverter}, ConverterParameter=(x - 20)}">
                        <Image.Style>
                            <Style TargetType="{x:Type Image}">
                                <Style.Triggers>
                                    <DataTrigger Binding="{Binding data.IsHidden}" Value="True">
                                        <Setter Property="Opacity" Value="0.5"/>
                                    </DataTrigger>
                                </Style.Triggers>
                            </Style>
                        </Image.Style>
                    </Image>
                </Grid>
            </VirtualizingStackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Question: Is there any way to prevent WPF from copying the bytes before render when I already am storing all the bytes in RAM and have called .Freeze() on the image? I would like one copy of my image bytes to be in RAM: no more, no less.

Possibly related: .NET Memory issues loading ~40 images, memory not reclaimed -- Seems unrelated because I am building BitmapSource objects from raw bytes, not a (literal) stream object.

Edit: Interesting clarification -- I am showing these BitmapSource items in 2 different ListBox items on two different screens. If I keep the objects around, the RAM usage only increases on first render of the BitmapSource and not on subsequent renders, regardless of which screen or ListBox the BitmapSource appears on.

Community
  • 1
  • 1
Deadpikle
  • 356
  • 6
  • 22

1 Answers1

0

I was unable to prevent a copy before render. After trying everything from BitmapImage with CacheOption = BitmapCacheOption.None to loading from files rather than images in memory, the fix to keep 1 copy of bytes in RAM was relatively simple.

To fix it, create your own custom class that inherits from BitmapSource. Follow the code in the accepted answer here, making adjustments as necessary for your own image formats. For example, I needed to use my own stride value rather than the provided one because I was converting a 24bpp array into the Pbgra32 format. I used the unsafe copy code for faster copies (again, modified for my use case). I have copied my code into the bottom of this post, but it's very similar to the linked SO post.

Your custom BitmapSource still has 2 copies of the bytes, however. (The CopyPixels function name gives that away.) To get rid of the now-extraneous copy, just set _data = null and let the GC clean it up when it can. Tada! One copy of bytes in RAM, performance is fast, the ListBox scroll works, you can reuse your BitmapSource on other screens and in other places, and memory use is acceptable.

I am rather concerned that this would break CreateInstanceCore() if called after render, and this may break in other use cases other than my own.

class RGB24BitmapSource : BitmapSource
{
    private byte[] _data;
    private int _stride;
    private int _pixelWidth;
    private int _pixelHeight;

    public RGB24BitmapSource(int pixelWidth, int pixelHeight, IntPtr data, int dataLength, int stride)
    {
        if (dataLength != 0 && data != null && data.ToInt64() != 0)
        {
            _data = new byte[dataLength];
            Marshal.Copy(data, _data, 0, dataLength);
        }
        _stride = stride;
        _pixelWidth = pixelWidth;
        _pixelHeight = pixelHeight;
    }

    private RGB24BitmapSource(int pixelWidth, int pixelHeight, byte[] data, int stride)
    {
        _data = data;
        _stride = stride;
        _pixelWidth = pixelWidth;
        _pixelHeight = pixelHeight;
    }

    unsafe public override void CopyPixels(Int32Rect sourceRect, Array pixels, int stride, int offset)
    {
        if (_data != null)
        {
            fixed (byte* source = _data, destination = (byte[])pixels)
            {
                byte* dstPtr = destination + offset;
                for (int y = sourceRect.Y; y < sourceRect.Y + sourceRect.Height; y++)
                {
                    for (int x = sourceRect.X; x < sourceRect.X + sourceRect.Width; x++)
                    {
                        byte* srcPtr = source + _stride * y + 3 * x;
                        byte a = 255;
                        *(dstPtr++) = (byte)((*(srcPtr + 2)) * a / 256);
                        *(dstPtr++) = (byte)((*(srcPtr + 1)) * a / 256);
                        *(dstPtr++) = (byte)((*(srcPtr + 0)) * a / 256);
                        *(dstPtr++) = a;
                    }
                }
            }
        }
        _data = null; // it was copied for render, so next GC cycle could theoretically reclaim this memory. This is the magic fix.
    }

    protected override Freezable CreateInstanceCore()
    {
        return new RGB24BitmapSource(_pixelWidth, _pixelHeight, _data, _stride);
    }

    // DO. NOT. COMMENT. THESE. OUT. IF YOU DO, CRASHES HAPPEN!
#pragma warning disable 0067 // disable unused warnings
    public override event EventHandler<DownloadProgressEventArgs> DownloadProgress;
    public override event EventHandler DownloadCompleted;
    public override event EventHandler<ExceptionEventArgs> DownloadFailed;
    public override event EventHandler<ExceptionEventArgs> DecodeFailed;
#pragma warning restore 0067

    public override double DpiX
    {
        get { return 96; }
    }

    public override double DpiY
    {
        get { return 96; }
    }

    public override System.Windows.Media.PixelFormat Format
    {
        get { return PixelFormats.Pbgra32; }
    }

    public override BitmapPalette Palette
    {
        get { return BitmapPalettes.WebPalette; }
    }

    public override int PixelWidth
    {
        get { return _pixelWidth; }
    }

    public override int PixelHeight
    {
        get { return _pixelHeight; }
    }

    public override double Width
    {
        get { return _pixelWidth; }
    }

    public override double Height
    {
        get { return _pixelHeight; }
    }
}
Community
  • 1
  • 1
Deadpikle
  • 356
  • 6
  • 22