So, I finally had time to take a deeper look at the OpenGL support and I want to share my experience and findings with this post -> related to the MonoGame.Forms feature set.
The difference from the GitHub example from @SpiceyWolf and the MonoGame.Forms library is, that MonoGame.Forms doesn’t use the Game class as well as it does not make use of the GraphicsDeviceManager from the MonoGame.Framework, because originally I had the vision of communicating with the MonoGame.Framwork from out of custom UserControl based classes, powered by Service classes to define own update routines and extra functionallity making the integration in a WindowsForms environment more smooth and easier to use by the end user, because we don’t need an extra game project to hook up.
The caveat is that we need to initialize and create MonoGame specific stuff manually by hand. So let my start explaining from the very beginning:
Creating the GraphicsDevice
When you simply try to create a GraphicsDevice in a DesktopGL project you will instantly receive errors. Right from the start it’s quite obvious that we need to modify the source of the MonoGame.Framework to make that work.
To create and initialize the GraphicsDevice successfully, it’s necessary to construct the SDLGamePlatform, which for itself creates the SDLGameWindow by using the default BackBuffer Width and Height, which is okay. The only thing we are adding here is the SDL_WindowFlag SDL_WINDOW_OPENGL to make sure to internally create an OpenGL Context. Additionally and right after this, we initializing platform specific stuff by extracting the function PlatformInitialize from the GraphicsDeviceManager.SDL class.
Now we can successfully create a GraphicsDevice and pointing a reference to it inside the SDLGameWindow for later: ((SdlGameWindow)SdlGameWindow.Instance).GetGraphicsDevice = GraphicsDevice;
Creating the (pseudo) SwapChainRenderTarget.GL
To render stuff from the BackBuffer to the GraphicsDeviceControl (Windows.Forms.Control), we need to get the BackBuffer Data from the GraphicsDevice and create a Bitmap from that to draw it in the OnPaint event.
We are using this technique from the GitHub repo of @SpiceyWolf.
GraphicsDevice.GetBackBufferData()
is using Gl.ReadPixels [ref], which can be slow when calling this in short intervalls. It also increases the RAM usage by around ~30 to ~80 MB.
You will also notice the decrease of performance the bigger the custom control is we are rendering to. Having a maximized window on a FullHD resolution with 2 rendering surfaces and the shortest intervall timer (1ms) raised my CPU usage to ~30%. Raising the intervall to 50ms results in CPU usage ~12% and still having a smooth editing experience (tested with the MultipleControls-Scenario of MonoGame.Forms and scrolling the HexMaps).
Having a ControlSize of 728x399 and intervall of 1ms leads to ~60fps and ~10% CPU usage.
Tested with Intel Core i5-7600K (non-overclocked).
I also found a solution to make the GL.ReadPixels calls significantly faster by using Pixel Buffer Objects (PBO), but for that we need a BufferTarget which is currently not implemented in MonoGame which is GL_PIXEL_UNPACK_BUFFER. I tried implementing this by myself but received an invalid enum exception from OpenGL. So someone with more knowledge is needed here. This task should be very simple.
Input Support (Keyboard/GamePad/Mouse)
Because we are creating the SDLGameWindow with the SDL_WINDOW_HIDDEN flag (it’s invisible and never shown to the user), we are not receiving Keyboard, GamePad or Mouse input anymore.
To resolve this we need to bring back the SDLEventQueue by calling SDLRunLoop manually after each draw of the BackBuffer. This atleast brings the GamePad input back.
Keyboard and Mouse input are still not working because the hidden SDLWindow can’t receive SDLEvents for Mouse and Keyboard.
We resolve this by invoking these events manually by using the Sdl.PushEvent function.
Well, the truth is a bit more complicated, because we first need to catch the Keyboard and Mouse events from WindowsForms and converting them to a format which SDL “understands”.
Here is an example of a Keyboard input event:
public Sdl.Event GetKeyEvent(SDLK.Key key, SDLK.ModifierKeys modifierKeys, bool down)
{
Sdl.Event evt = new Sdl.Event();
evt.Key.Keysym.Scancode = 0;
evt.Key.Keysym.Sym = (int)key;
evt.Key.Keysym.Mod = (int)modifierKeys;
if (down)
{
evt.Key.State = (byte)SDLB.ButtonKeyState.Pressed;
evt.Type = Sdl.EventType.KeyDown;
}
else
{
evt.Key.State = (byte)SDLB.ButtonKeyState.NotPressed;
evt.Type = Sdl.EventType.KeyUp;
}
return evt;
}
protected override void OnKeyUp(KeyEventArgs e)
{
if (!LockKeyboardInput)
{
try
{
if (!designMode)
{
Sdl.Event evt = GetKeyEvent((SDLK.Key)Enum.Parse(typeof(SDLK.Key), e.KeyCode.ToString()), (SDLK.ModifierKeys)e.Modifiers, false);
Sdl.PushEvent(out evt);
}
}
catch { }
}
base.OnKeyUp(e);
}
We do the same for MouseButtons and MouseMotion and start receiving Keyboard and Mouse events in the SDLEventQueue, which makes it also possible to use Keyboard.GetState() and Mouse.GetState() from the MonoGame.Framework (GamePad.GetState() also works of course).
Window Resizing
At some point I added window resizing support to MonoGame.Forms were it’s possible to resize the window, which contains a custom user control and everything got updated accordingly, when a resize event occured (the drawn contents as well as the Camera2D position and custom RenderTargets).
Critical here is the call to Sdl.Window.SetSize() which updates the hidden SDLWindow in the background for us.
RenderTargets
Currently RenderTargets are not functional, because it’s not possible to set or dispose them. The reason for this is the Blocking of the UI Thread. When avoiding those calls we would receive GL Errors. If someone has a clue I will be happy to hear a solution.
Conclusion
Making this changes and placing additions to the SDL backend made OpenGL support in MonoGame.Forms happen without changing in the core how the library works.
Having an intervall timer led to an acceptable performance - even for UpdateWindows in realtime. It’s also possible to make GL.ReadPixels faster in the future by using PBOs.
DrawWindows with AutomaticInvalidation turned off only consuming ~1% CPU usage, which suites them perfect as preview controls for textures and all kind of stuff were a lot of draw calls to update the controls are not necessary.
Now that we support more than 1 platform, I need to restructure the project. I thought of this:
- MonoGame.Forms.Core
- MonoGame.Forms.DX
- MonoGame.Forms.GL
- MonoGame.Forms.Tests.DX
- MonoGame.Forms.Tests.GL
So before making this publicity available I still need to take some more time, effort and work. But I think I can make this available in the next couple of weeks. We will see. (this is not a promise ;))
So let me hear what you think and most importantly: think about the above mentioned problems and try to help making the library better!
Thank you all for your attention and suggestions / ideas so far! It’s really appreciated.
Have a nice one!