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: