luca-piccioni / OpenGL.Net

Modern OpenGL bindings for C#.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unable to use two GlControls in the same form

antoinecla opened this issue · comments

Step to reproduce:

  • Open HelloTriangle Project
  • Open SampleForm
  • Select RenderControl, set Dock to None
  • Copy and paste GlControl somewhere else in Form
  • Build and Run

On my system the original control is solid white and the second one is black. Nothing else is shown. Property ContextSharing is set to OwnContext for both controls.

I was not able to find what is causing the issue in the code. Here is what I have found:

  • Setting the Animation to false for the second control fix the issue
  • Resizing the window to mask the second control and show only the first one fix the issue!!
  • Turning off animation for both controls and adding a timer that invalidates both controls works

A possible cause is the Invalidate you use inside OnPaint() to animate.

Very strange behavior, but seems very consistent. Seems that the first GlControl instance is excluded in drawing and update.

Ok, I found a way to workaround this issue. However, I have to admit that the actual culprit is not found yet. What I got is:

  • Calling Invalidate on the first GlControl, prevent the second one to draw
  • Calling Invalidate on the second one, but not on the first one, allow drawing two controls at the same time

My understanding is that Invalidate will issue a WM_PAINT message on the message loop, implementing a game loop without sacrificing other control messages (menus, buttons, ...). Instead what I see is that the second GlControl is affected by this message.

I tried to invoke ParentForm invalidation (ParentForm.Invalidate(true) instead of Invalidate()), but without success.

Here is the solution I came up:

Application.Idle += Application_Idle;

private void Application_Idle(object sender, EventArgs e)
{
	while (IsApplicationIdle()) {
		Invalidate(true);
	}
}

bool IsApplicationIdle()
{
	NativeMessage result;
	return PeekMessage(out result, IntPtr.Zero, (uint)0, (uint)0, (uint)0) == 0;
}

[StructLayout(LayoutKind.Sequential)]
public struct NativeMessage
{
	public IntPtr Handle;
	public uint Message;
	public IntPtr WParameter;
	public IntPtr LParameter;
	public uint Time;
	public Point Location;
}

[DllImport("user32.dll")]
public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);

	[StructLayout(LayoutKind.Sequential)]
	public struct NativeMessage
	{
		public IntPtr Handle;
		public uint Message;
		public IntPtr WParameter;
		public IntPtr LParameter;
		public uint Time;
		public Point Location;
	}

	[DllImport("user32.dll")]
	public static extern int PeekMessage(out NativeMessage message, IntPtr window, uint filterMin, uint filterMax, uint remove);

The implementation is scoped at the Form level, in order to invalidate all children. The problem here is that this solution is valid only for Windows platforms. I have no idea if Linux/Mono implementations can handle multiple GlControl instances.

Anyway, using two GlControl instances is very, very slow.

Anyway, using two GlControl instances is very, very slow.

Can you explain? Is there an overhead in using two or more GlControls?

Each GlControl creates its own GL context by default. The function pointers relative to the underlying context may depends on the device context which the GL context is bound to. I say "may" because this is a Windows specific issue: if the two GL contexts are bound to different device contexts, they can share GL function pointers only if all device contexts have the same pixel format set; in the other case, function pointers must be reloaded.

Being said this, OpenGL.Net ignore the pixel format set on the GlControl instances; this means that DeviceContext.MakeCurrent will reload all function pointers anyway, causing sensible delays, each time a different GlControl is updated.
With a single GlControl instance, a single GL context is current on the thread. DeviceContext.MakeCurrent optimize this case, avoiding function pointers reloading if the GL context made current does not change.
Remember that GL function pointer have are scoped to TLS.

Indeed, having more than one GlControl instance cause updating all those, and because they have different device contexts, function pointers are continuously reloaded. This is noticeable applying animation to two GlControl instances and apply the patch of the previous post: animations are no more fluid as the simple example HelloTriangle.

I've not profiled it, but I bet that Gl.BindAPI() is the bottleneck: it uses reflection to query and set function pointers. Maybe it could be optimized by emitting optimized methods.

Indeed my experiments lead to some awkward conclusion: Invalidate() executed by OnPaint() causes a synchronous refresh of the UserControl, leading to other control to the previous contents. Calling Invalidate() on other contexts (i.e. in a timer callback) works correctly.

Invalidate() invokes UnsafeNativeMethods.InvalidateRect, which points to WIN32 InvalidateRect; the man says:

The system sends a WM_PAINT message to a window whenever its update region is not empty and there are no other messages in the application queue for that window.

I wonder why the message is processed "immediately". If the Animation property is set on the last GlControl within the form, both controls can be redrawn, with a resize operation, while animating; but invalidating the first controls will lead to the same behavior (only the invalidated control will be redrawn).

I tried to invoke asynchronously Invalidate() using BeginInvoke without luck. At the moment I'm out of ideas.