luca-piccioni / OpenGL.Net

Modern OpenGL bindings for C#.
MIT License
568 stars 108 forks source link

Unable to use two GlControls in the same form #70

Closed antoinecla closed 6 years ago

antoinecla commented 6 years ago

Step to reproduce:

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:

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

luca-piccioni commented 6 years ago

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

luca-piccioni commented 6 years ago

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:

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.

antoinecla commented 6 years ago

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

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

luca-piccioni commented 6 years ago

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.

luca-piccioni commented 6 years ago

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.