0

Follow-on question to this one.

I am trying to generate and save a series of images. Rendering is done by Helix Toolkit, which I am told utilises the WPF composite render thread. This is causing problems because it executes asynchronously.

My original problem was that I couldn't save a given image because it hadn't yet been rendered at that time I was trying to save it. The above answer provides a workaround to this by putting the 'save' operation inside an Action which is called with low priority, thus ensuring that the rendering completes first.

This is fine for one image, but in my application I require multiple images. As it stands I cannot keep control of the sequence of events because they occur asynchronously. I am using a For loop which just continues regardless of the progress of rendering and saving the images. I need the images to be generated one by one, with enough time for rendering and saving before starting the next one.

I have tried putting delays in the loop but that causes its own problems. For instance an async await as commented in the code causes cross-threading issues because the data was created on a different thread from where the rendering is being done. I tried putting in a simple delay but then that just locks everything up - I think in part because the save operation I am waiting on has very low priority.

I cannot simply treat it as a batch of separate unrelated asynchronous tasks because I am using a single HelixViewport3D control in the GUI. The images have to be generated sequentially.

I did try a recursive method where SaveHelixPlotAsBitmap() calls DrawStuff() but that wasn't working out very well, and it doesn't seem a good approach.

I tried setting a flag ('busy') on each loop and waiting for it to be reset before continuing but that wasn't working - again, because of the asynchronous execution. Similarly I tried using a counter to keep the loop in step with the number of images that had been generated but ran into similar problems.

I seem to be going down a rabbit hole of threading and asynchronous operations that I don't want to be in.

How can I resolve this?

class Foo {
    public List<Point3D> points;
    public Color PointColor;
    public Foo(Color col) { // constructor creates three arbitrary 3D points
        points = new List<Point3D>() { new Point3D(0, 0, 0), new Point3D(1, 0, 0), new Point3D(0, 0, 1) };
        PointColor = col;
    }
}

public partial class MainWindow : Window
{
    int i = -1; // counter
    public MainWindow()
    {
        InitializeComponent();
    }
    private void Go_Click(object sender, RoutedEventArgs e) // STARTING POINT
    {
        // Create list of objects each with three 3D points...
        List<Foo> bar = new List<Foo>(){ new Foo(Colors.Red), new Foo(Colors.Green), new Foo(Colors.Blue) };

        foreach (Foo b in bar)
        {

            i++;
            DrawStuff(b, SaveHelixPlotAsBitmap); // plot to helixViewport3D control ('points' = list of 3D points)

            // This is fine the first time but then it runs away with itself because the rendering and image grabbing
            // are asynchronous. I need to keep it sequential i.e.
            // Render image 1 -> save image 1
            // Render image 2 -> save image 2
            // Etc.

        }
    }
    private void DrawStuff(Foo thisFoo, Action renderingCompleted)
    {

        //await System.Threading.Tasks.Task.Run(() =>
        //{

        Point3DCollection dataList = new Point3DCollection();
        PointsVisual3D cloudPoints = new PointsVisual3D { Color = thisFoo.PointColor, Size = 5.0f };
        foreach (Point3D p in thisFoo.points)
        {
            dataList.Add(p);
        }
        cloudPoints.Points = dataList;

        // Add geometry to helixPlot. It renders asynchronously in the WPF composite render thread...
        helixViewport3D.Children.Add(cloudPoints);
        helixViewport3D.CameraController.ZoomExtents();

        // Save image (low priority means rendering finishes first, which is critical)..
        Dispatcher.BeginInvoke(renderingCompleted, DispatcherPriority.ContextIdle);

        //});

    }
    private void SaveHelixPlotAsBitmap()
    {
        Viewport3DHelper.SaveBitmap(helixViewport3D.Viewport, $@"E:\test{i}.png", null, 4, BitmapExporter.OutputFormat.Png);
    }
}
wotnot
  • 261
  • 1
  • 12
  • 1
    [Asynchronous Programming](https://learn.microsoft.com/en-us/dotnet/csharp/async) - read it carefully. In addition class `Progress` implementing `IProgress` interface, synchronized callback may help in case of concurrent execution. Avoid using `Dispatcher` directly, it will make the code weird. – aepot Aug 14 '20 at 17:33
  • Also consider [this](https://stackoverflow.com/a/62611838/12888024) `CancellationToken` usage example. – aepot Aug 14 '20 at 17:39
  • I just looked at the source code and it looks like `PointsVisual3D` derives from `FrameworkElement` so you can listen to the `Loaded` event to know that an individual point is rendered. You could combine these events into an event that says the render is complete. Id guess that making a blocking behavior so you can screenshot would be easiest to implement via a task queue that dispatches the render and screenshot calls as seperate tasks to the ui thread and then somthing like a taskcompletionsource to async wait for the previous unit of work. – Jason Aug 14 '20 at 19:24

2 Answers2

2

Note These examples are just to prove a concept, there is work needed on the TaskCompletionSource to handle errors

Given this test window

<Window x:Class="WpfApp2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <StackPanel x:Name="StackPanel"/>
    </Grid>
</Window>

Here is an example of how to use events to know when the view is in the state that you want.

using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp2
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DoWorkAsync();
        }

        private async Task DoWorkAsync()
        {
            for (int i = 0; i < 10; i++)
            {
                await RenderAndCapture();
            }
        }

        private async Task RenderAndCapture()
        {
            await RenderAsync();
            CaptureScreen();
        }

        private Task RenderAsync()
        {
            var taskCompletionSource = new TaskCompletionSource<object>();
            Dispatcher.Invoke(() =>
            {
                var panel = new TextBlock {Text = "NewBlock"};
                panel.Loaded += OnPanelOnLoaded;

                StackPanel.Children.Add(panel);

                void OnPanelOnLoaded(object sender, RoutedEventArgs args)
                {
                    panel.Loaded -= OnPanelOnLoaded;
                    taskCompletionSource.TrySetResult(null);
                }
            });

            return taskCompletionSource.Task;
        }

        private void CaptureScreen()
        {
            // Capture Image
        }
    }
}

If you want to have your sync method called from outside you can implement a task queue.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace WpfApp2
{
    public class TaskQueue
    {
        private readonly SemaphoreSlim _semaphore;
        public TaskQueue()
        {
            _semaphore = new SemaphoreSlim(1);
        }

        public async Task Enqueue(Func<Task> taskFactory)
        {
            await _semaphore.WaitAsync();
            try
            {
                await taskFactory();
            }
            finally
            {
                _semaphore.Release();
            }
        }
    }

    public partial class MainWindow : Window
    {
        private readonly TaskQueue _taskQueue;

        public MainWindow()
        {
            _taskQueue = new TaskQueue();
            InitializeComponent();
            DoWork();
        }

        private void DoWork()
        {
            for (int i = 0; i < 10; i++)
            {
                QueueRenderAndCapture();
            }
        }

        private void QueueRenderAndCapture()
        {
            _taskQueue.Enqueue(() => RenderAndCapture());
        }

        private async Task RenderAndCapture()
        {
            await RenderAsync();
            CaptureScreen();
        }

        private Task RenderAsync()
        {
            var taskCompletionSource = new TaskCompletionSource<object>();
            Dispatcher.Invoke(() =>
            {
                var panel = new TextBlock {Text = "NewBlock"};
                panel.Loaded += OnPanelOnLoaded;

                StackPanel.Children.Add(panel);

                void OnPanelOnLoaded(object sender, RoutedEventArgs args)
                {
                    panel.Loaded -= OnPanelOnLoaded;
                    taskCompletionSource.TrySetResult(null);
                }
            });

            return taskCompletionSource.Task;
        }

        private void CaptureScreen()
        {
            // Capture Screenshot
        }
    }
}

This will make sure the UI is in the state required for each iteration Render in stages

You will of course need to expand this so that you listen to the Loaded event of each point that you wish to render.

Edit: As PointsVisual3D does not have the Loaded event you can complete the task by hooking onto the event you had previously used. Not ideal, but it should work.

private Task RenderAsync()
{
    var taskCompletionSource = new TaskCompletionSource<object>();
    Dispatcher.Invoke(() =>
    {
        var panel = new TextBlock {Text = "NewBlock"};

        StackPanel.Children.Add(panel);

        Dispatcher.BeginInvoke(new Action(() =>
        {
            taskCompletionSource.TrySetResult(null);
        }), DispatcherPriority.ContextIdle);
    });

    return taskCompletionSource.Task;
}
Jason
  • 1,505
  • 5
  • 9
  • Thanks for all the info. Very helpful. `PointsVisual3D` doesn't seem to have a Loaded event, though. Intellisense does not give me the option of `cloudPoints.Loaded`. – wotnot Aug 15 '20 at 12:49
  • That's a shame, I was checking on my phone so I missed the correct object hierarchy. In that case, you can hook onto your previous way of detecting render. I'll update the answer. – Jason Aug 15 '20 at 13:46
  • Thanks. Your input is much appreciated. :-) – wotnot Aug 15 '20 at 13:57
  • I didn't know it had been edited but I see it now. I will check it out. – wotnot Aug 15 '20 at 16:10
  • I don't want to speak too soon because this is a much-reduced version of the real thing, but yes that does seem to work. I have implemented your code, made minor modifications as required and it worked first time. It is generating images :-) I don't really understand the `taskCompletionSource.TrySetResult()` call but I presume this is the key to getting it to wait. Is the point that this is low thread priority, so execution pauses at that point until the rendering which precedes it has finished? – wotnot Aug 15 '20 at 16:49
  • 1
    The trySetResult is essentially an event signal in this case. So when the render is complete, a task is queued to continue the work after await `await RenderAsync()`. The image can be generated. The task queue can then take the next render request from the backlog and process that in the same way. – Jason Aug 15 '20 at 16:56
  • I have still got a problem with this - although much less so than before. In my application I am setting the camera position as well as rendering. Although the images now get generated, the camera positioning / zoom are unreliable or not taking effect. I am sure this is another synchronization problem because putting in a MessageBox immediately after setting the camera position solves it. It seems that breaking execution with the MessageBox provides the time required to catch up. – wotnot Aug 15 '20 at 21:20
  • I do think that I have answered the original question on how to handle async operations. Your specific use case might require some more tweaking as is common. One thing you can try is to add `await Task.Delay(1)` after `await RenderAsync(b);` and tweak the timeout as needed. – Jason Aug 15 '20 at 23:28
  • Yes, you have. If I cannot get past the latest problem I may post it as a separate question. Will try your suggestions ; thanks. – wotnot Aug 16 '20 at 05:10
0

Solution below. This is my implementation of the code provided in Jason's answer. All credit to Jason for the important bits.

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Go_Click(object sender, RoutedEventArgs e) // STARTING POINT
        {
            DoWorkAsync();
        }

        private async Task DoWorkAsync()
        {

            // Create list of objects each with three 3D points...
            List<Foo> bar = new List<Foo>() { new Foo(Colors.Red), new Foo(Colors.Green), new Foo(Colors.Blue) };
            
            int i = -1; // init counter
            foreach (Foo b in bar)
            {
                i++;
                await RenderAndCapture(b, i);
            }

        }

        private async Task RenderAndCapture(Foo b, int i)
        {
            await RenderAsync(b);
            SaveHelixPlotAsBitmap(i);
        }

        private Task RenderAsync(Foo b)
        {
            var taskCompletionSource = new TaskCompletionSource<object>();
            Dispatcher.Invoke(() =>
            {

                DrawStuff(b);

                Dispatcher.BeginInvoke(new Action(() =>
                {
                    taskCompletionSource.TrySetResult(null);
                }), DispatcherPriority.ContextIdle);
            });

            return taskCompletionSource.Task;
        }

        private void DrawStuff(Foo thisFoo)
        {

            Point3DCollection dataList = new Point3DCollection();
            PointsVisual3D cloudPoints = new PointsVisual3D { Color = thisFoo.PointColor, Size = 5.0f };
            
            foreach (Point3D p in thisFoo.points)
            {
                dataList.Add(p);
            }
            cloudPoints.Points = dataList;
            
            // Add geometry to helixPlot. It renders asynchronously in the WPF composite render thread...
            helixPlot.Children.Add(cloudPoints);
            helixPlot.CameraController.ZoomExtents();
            
        }
        private void SaveHelixPlotAsBitmap(int i) // screenshot
        {
            Viewport3DHelper.SaveBitmap(helixPlot.Viewport, $@"E:\test{i}.png", null, 4, BitmapExporter.OutputFormat.Png);
        }

    }
wotnot
  • 261
  • 1
  • 12