Outside Context Problem
VaaS Part 1

Thinking about how to approach the VaaS idea, I figured the path of least resistance was to modify the visualisation executable to take an optional second parameter; the name of a file to output a recording to (the first parameter is which visualisation to run.)

Next was to investigate how to output a recording. A bit of Googling turned up the SharpAvi NuGet package, which looked like it would do exactly what I needed. According to its documentation, you just throw bitmaps at it. Perfect.

Before proceeding I needed to make sure I can get bitmaps of what is being rendered to the screen. Google to the rescue again. I found a method to do such a thing. Excellent, looks like the pieces are in place.

I added a property, OutputAviPath to my VisualisationBase class. If this is set, it will setup SharpAvi during initialisation.

if (! string.IsNullOrWhiteSpace(OutputAviPath))
{
    _aviWriter = new AviWriter(OutputAviPath)
                 {
                     FramesPerSecond = 30,
                     EmitIndex1 = true
                 };

    _aviStream = _aviWriter.AddVideoStream();

    _aviStream.Width = GraphicsDeviceManager.PreferredBackBufferWidth;

    _aviStream.Height = GraphicsDeviceManager.PreferredBackBufferHeight;

    _aviStream.BitsPerPixel = BitsPerPixel.Bpp24;
}

Cool. MonoGame has an overridable EndDraw method that is called right after a frame is rendered to the window. This looked like a great place to hook into for capturing frames. So in my base class, I overrode it as follows.

if (_aviStream != null)
{
    using (var bitmap = new Bitmap(GraphicsDeviceManager.PreferredBackBufferWidth,
                                   GraphicsDeviceManager.PreferredBackBufferHeight,
                                   PixelFormat.Format32bppRgb))
    {
        using (Graphics graphics = Graphics.FromImage(bitmap))
        {
            var bounds = Window.ClientBounds;

            graphics.CopyFromScreen(new Point(bounds.Left, bounds.Top), 
                                    Point.Empty, 
                                    new Size(bounds.Size.X, bounds.Size.Y));
        }

        var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bounds.Size.X, bounds.Size.Y), 
                                         ImageLockMode.ReadOnly, 
                                         PixelFormat.Format32bppRgb);

        var span = new Span<byte>(bitmapData.Scan0.ToPointer(), bitmapData.Stride * bitmapData.Height);

        _aviStream?.WriteFrame(true, span);

        bitmap.UnlockBits(bitmapData);
    }
}

Once the visualisation is complete, we just need to close the writer.

if (_aviWriter != null)
{
    _aviStream = null;

    _aviWriter.Close();
}

Aside from some logic changes to determine when recording should stop (I went for 10 seconds after the end-state of the puzzle has been rendered), that's pretty much it.

Next steps are: