Search code examples
c#wpfwriteablebitmap

Deferred WriteableBitmap write on background thread causes flicker


I'm working on a custom WPF Framework element that writes to a WriteableBitmap, and then displays the bitmap in the elements OnRender().

As the writing to the WriteableBitmap can be a little slow (due to the algorithm I am computing at that point) and the fact that I need to display 36 of these elements, I want to do the work of updating the WriteableBitmap on a background thread.

So I have come up with the following which greatly improves the performance. The problem is that it works fine if I only create 8 or less of these elements, but if I create any more, like the requested 36, I get flicker on all elements past the first 8 when you resize the window?

Any ideas what might be causing this?

Renderer element:

public class Renderer : FrameworkElement
{
    private WriteableBitmap? _bitmap, _previousBitmap;
    private long _pBackBuffer = 0;
    private int _backBufferStride = 0, _backBufferWidth = 0, _backBufferHeight = 0;
    private SemaphoreSlim _semaphore = new(1, 1);

    private void ResizeBitmap()
    {
        int width = (int)ActualWidth;
        int height = (int)ActualHeight;

        if (width <= 0 || height <= 0) 
            return;
        
        if (_bitmap == null || width != _bitmap.PixelWidth || height != _bitmap.PixelHeight)
        {
            _previousBitmap = _bitmap;
            _bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.Pbgra32, null);
            _pBackBuffer = _bitmap.BackBuffer;
            _backBufferStride = _bitmap.BackBufferStride;
            _backBufferWidth = width;
            _backBufferHeight = height;

            // fill with blue for debugging purposes
            byte[] data = new byte[_bitmap.BackBufferStride * _bitmap.PixelHeight];
            for (int i = 0; i < _bitmap.PixelHeight; ++i)
            {
                for (int j = 0; j < _bitmap.PixelWidth; ++j)
                {
                    data[i * _bitmap.BackBufferStride + j * sizeof(int) + 0] = 255;
                    data[i * _bitmap.BackBufferStride + j * sizeof(int) + 1] = 0;
                    data[i * _bitmap.BackBufferStride + j * sizeof(int) + 2] = 0;
                    data[i * _bitmap.BackBufferStride + j * sizeof(int) + 3] = 255;
                }
            }

            _bitmap.WritePixels(new Int32Rect(0, 0, _bitmap.PixelWidth, _bitmap.PixelHeight), data, _bitmap.BackBufferStride, 0);
        }
    }

    public void InvalidateRender() => WriteToBitmapInWorkerThread();

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);

        if (_bitmap == null)
            return;

        _semaphore.Wait();

        _bitmap.Lock();
        _bitmap.AddDirtyRect(new Int32Rect(0, 0, (int)_bitmap.Width, (int)_bitmap.Height));
        _bitmap.Unlock();

        drawingContext.DrawImage(_bitmap, new Rect(0, 0, ActualWidth, ActualHeight));

        _semaphore.Release();
    }

    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        base.OnRenderSizeChanged(sizeInfo);

        _semaphore.Wait();
        ResizeBitmap();
        _semaphore.Release();

        WriteToBitmapInWorkerThread();
    }

    private void WriteToBitmapInWorkerThread()
    {
        // do some writing to the bitmap that is slow (so run on a worker thread)
        Task.Run(() =>
        {
            _semaphore.Wait();

            unsafe
            {
                int* pPointer = (int*)_pBackBuffer;
                int stride = _backBufferStride / 4;

                // simulate slowness
                Thread.Sleep(10);

                // render a gradient for demo purposes
                for (int i = 0; i < _backBufferHeight; ++i)
                {
                    byte x = (byte)(255d / _backBufferHeight * i);
                    for (int j = 0; j < _backBufferWidth; ++j)
                    {
                        pPointer[i * stride + j] = 255 << 24 | x << 16 | x << 8 | 255;
                    }
                }
            }

            _semaphore.Release();

            Dispatcher.BeginInvoke(DispatcherPriority.Render, () => InvalidateVisual());
        });
    }
}

MainWindow:

 <Grid Background="Orange">
     <ItemsControl ItemsSource="{Binding Items}">
         <ItemsControl.ItemsPanel>
             <ItemsPanelTemplate>
                 <UniformGrid />
             </ItemsPanelTemplate>
         </ItemsControl.ItemsPanel>
         <ItemsControl.ItemTemplate>
             <DataTemplate>
                 <Border Margin="2"
                         Padding="2"
                         BorderThickness="1"
                         BorderBrush="Red">
                     <local:Renderer />
                 </Border>
             </DataTemplate>
         </ItemsControl.ItemTemplate>
     </ItemsControl>
 </Grid>
 public partial class MainWindow : Window
 {
     public List<string> Items { get; } = Enumerable.Range(0, 36).Select(e => e.ToString()).ToList();

     public MainWindow()
     {
         InitializeComponent();
         DataContext = this;
     }
 }

Solution

  • The main cause of the flicker seems to be the fact that you draw the bitmap right after creation, but before its buffer is actually written to. This is because OnRender is called immediately after each size change.

    Besides that, it would be simpler and more efficient to create the new bitmap in the background thread. You would not need a WriteableBitmap, but could instead directly create a frozen BitmapSource from a pixel buffer as shown below without any flicker at all.

    You should also declare the WriteToBitmapInWorkerThread method async and await its call. Instead of an overridden OnRenderSizeChanged method, you would call it in a SizeChanged event handler, because event handlers are the only methods that may be declared async void.

    public class Renderer : FrameworkElement
    {
        private BitmapSource _bitmap;
    
        public Renderer()
        {
            SizeChanged += async (s, e) => await WriteToBitmapInWorkerThread();
        }
    
        public Task InvalidateRender() => WriteToBitmapInWorkerThread();
    
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
    
            if (_bitmap != null)
            {
                drawingContext.DrawImage(_bitmap, new Rect(0, 0, ActualWidth, ActualHeight));
            }
        }
    
        private async Task WriteToBitmapInWorkerThread()
        {
            // do some writing to the bitmap that is slow (so run on a worker thread)
    
            int width = (int)ActualWidth;
            int height = (int)ActualHeight;
    
            _bitmap = await Task.Run(() =>
            {
                int[] buffer = new int[width * height];
    
                // simulate slowness
                Thread.Sleep(10);
    
                // render a gradient for demo purposes
                for (int i = 0; i < height; ++i)
                {
                    int x = (int)(255d / height * i);
    
                    for (int j = 0; j < width; ++j)
                    {
                        buffer[i * width + j] = (255 << 24) | (x << 16) | (x << 8) | 255;
                    }
                }
    
                BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgra32, null, buffer, width * 4);
                bitmap.Freeze();
    
                return bitmap;
            });
    
            InvalidateVisual();
        }
    }
    

    In order to avoid multiple calls of the WriteToBitmapInWorkerThread method on each invocation of the SizeChanged event, you may use a timer like this:

    private readonly DispatcherTimer _updateTimer = new DispatcherTimer
    {
        Interval = TimeSpan.FromMilliseconds(100)
    };
    
    public Renderer()
    {
        _updateTimer.Tick += async (s, e) =>
        {
            _updateTimer.Stop();
            await WriteToBitmapInWorkerThread();
        };
    }
    
    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        _updateTimer.Stop();
        _updateTimer.Start();
    
        base.OnRenderSizeChanged(sizeInfo);
    }