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: