Trouble with multisampling in a C# wrapper using FBO with renderbuffers

Hi everyone.

I wanted a quick drop-in 3D scene renderer for my experimental Winforms application. The most straight-forward solution I found was SharpGL project. It got me started quickly without any OpenGL knowledge and I got everything up and running in a very short time.
However, I noticed that my objects look jagged in the SharpGL.SceneControl. So I wanted to enable antialiasing.

SharpGL SceneControl is by default using FBO, which seems to be the recommended approach. So I found learnopengl article explaining how to enable antialias in different situations. It says, there are two ways of doing it, depending if I have renderbuffers or texture attachments.

From SharpGL FBO renderer code on GitHub (sorry no direct links allowed for me, look for github FBORenderContextProvider.cs) t has no references to texture attachments, so it looks like render buffer adjustments should be enough to enable multisampling.

According to the learnopengl article, the first step is as follows:

all we need to change is glRenderbufferStorage to glRenderbufferStorageMultisample when we configure the (currently bound) renderbuffer’s memory storage: glRenderbufferStorageMultisample(GL_RENDERBUFFER, 4, GL_DEPTH24_STENCIL8, width, height)

The naive attempt with this replacement yielded a black screen instead of my scene. Then I found that glReadPixels cannot be used on multisampled buffers directly, so I modified SharpGL’s Blit method to switch the buffers, according to some other articles I found.

BTW, OpenGL version is set to 2.1. for my project. Might be old, but seems safer than other versions? Not sure if it has any effect on my issue or not.

The final code flow is executed like this.

  1. Winforms control is created for the OpenGL container.
  2. A basic render window is created (shortened code fragment from SharpGL):
//  Create a new window class, as basic as possible.                
wndClass = new Win32.WNDCLASSEX();
wndClass.Init();
wndClass.style          = Win32.ClassStyles.HorizontalRedraw | Win32.ClassStyles.VerticalRedraw | Win32.ClassStyles.OwnDC;
wndClass.lpfnWndProc    = wndProcDelegate;
wndClass.cbClsExtra     = 0;
wndClass.cbWndExtra     = 0;
wndClass.hInstance      = IntPtr.Zero;
wndClass.hIcon          = IntPtr.Zero;
wndClass.hCursor        = IntPtr.Zero;
wndClass.hbrBackground  = IntPtr.Zero;
wndClass.lpszMenuName   = null;
wndClass.lpszClassName  = "SharpGLRenderWindow";
wndClass.hIconSm        = IntPtr.Zero;
Win32.RegisterClassEx(ref wndClass);
    
//  Create the window. Position and size it.
windowHandle = Win32.CreateWindowEx(0,
              "SharpGLRenderWindow",
              "",
              Win32.WindowStyles.WS_CLIPCHILDREN | Win32.WindowStyles.WS_CLIPSIBLINGS | Win32.WindowStyles.WS_POPUP,
              0, 0, width, height,
              IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

//  Get the window device context.
deviceContextHandle = Win32.GetDC(windowHandle);

//  Setup a pixel format.
Win32.PIXELFORMATDESCRIPTOR pfd = new Win32.PIXELFORMATDESCRIPTOR();
pfd.Init();
pfd.nVersion = 1;
pfd.dwFlags = Win32.PFD_DRAW_TO_WINDOW | Win32.PFD_SUPPORT_OPENGL | Win32.PFD_DOUBLEBUFFER;
pfd.iPixelType = Win32.PFD_TYPE_RGBA;
pfd.cColorBits = (byte)bitDepth;
pfd.cDepthBits = 16;
pfd.cStencilBits = 8;
pfd.iLayerType = Win32.PFD_MAIN_PLANE;

//  Match an appropriate pixel format 
int iPixelformat;
if((iPixelformat = Win32.ChoosePixelFormat(deviceContextHandle, pfd)) == 0 )
    return false;

//  Sets the pixel format
if (Win32.SetPixelFormat(deviceContextHandle, iPixelformat, pfd) == 0)
{
    return false;
}

//  Create the render context - important, this is OpenGL WGL function!
renderContextHandle = Win32.wglCreateContext(deviceContextHandle);

Win32.wglMakeCurrent(deviceContextHandle, renderContextHandle);

  1. The modified initialization (and also resize event) code is executed:
gl.GenFramebuffersEXT(2, ids);
frameBufferID = ids[0];
frameBufferMultiID = ids[1];

// binding the normal frame buffer, otherwise the code below would fail to create complete buffers
gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, frameBufferID);

//  Create the colour render buffer and bind it, then allocate storage for it.
gl.GenRenderbuffersEXT(1, ids);
colourRenderBufferID = ids[0];
gl.BindRenderbufferEXT(OpenGL.GL_RENDERBUFFER_EXT, colourRenderBufferID);
// this is my replacement of SharpGLs RenderbufferStorageEXT
gl.RenderbufferStorageMultisampleEXT(OpenGL.GL_RENDERBUFFER_EXT, 4, OpenGL.GL_RGBA, width, height);

//  Create the depth render buffer and bind it, then allocate storage for it.
gl.GenRenderbuffersEXT(1, ids);
depthRenderBufferID = ids[0];
gl.BindRenderbufferEXT(OpenGL.GL_RENDERBUFFER_EXT, depthRenderBufferID);
// this is my replacement of SharpGLs RenderbufferStorageEXT           gl.RenderbufferStorageMultisampleEXT(OpenGL.GL_RENDERBUFFER_EXT, 4, OpenGL.GL_DEPTH_COMPONENT24, width, height);

//  Attach the buffer for colour and depth.
gl.FramebufferRenderbufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, OpenGL.GL_COLOR_ATTACHMENT0_EXT,
    OpenGL.GL_RENDERBUFFER_EXT, colourRenderBufferID);
gl.FramebufferRenderbufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, OpenGL.GL_DEPTH_ATTACHMENT_EXT,
    OpenGL.GL_RENDERBUFFER_EXT, depthRenderBufferID);

// just to check if everything went right
uint status = gl.CheckFramebufferStatusEXT(OpenGL.GL_FRAMEBUFFER_EXT);
if (status != OpenGL.GL_FRAMEBUFFER_COMPLETE_EXT)
{
    // should not get here if everything was inited properly
}

  1. Some untouched SharpGL code is executed to complete DIB section creation:
dibSectionDeviceContext = Win32.CreateCompatibleDC(deviceContextHandle);

//  Create the DIB section.
dibSection.Create(dibSectionDeviceContext, width, height, bitDepth);
  1. It calls a few more OpenGL functions to setup the rendering parameters:
gl.ShadeModel(OpenGL.GL_SMOOTH);
gl.ClearColor(0.0f, 0.0f, 0.0f, 0.0f);
gl.ClearDepth(1.0f);
gl.Enable(OpenGL.GL_DEPTH_TEST);
gl.DepthFunc(OpenGL.GL_LEQUAL);
gl.Hint(OpenGL.GL_PERSPECTIVE_CORRECTION_HINT, OpenGL.GL_NICEST);
  1. It traverses the SharpGL scene and sets up stuff for rendering (creates OpenGL display lists etc.).
  2. Windows Paint event starts and SharpGL rendering iteration is executed:
Win32.wglMakeCurrent(deviceContextHandle, renderContextHandle);
...
Scene rendering:
gl.ClearColor(clear[0], clear[1], clear[2], clear[3]);
camera projection

gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT |
    OpenGL.GL_STENCIL_BUFFER_BIT);

Renders the tree of the scene

gl.Flush();
...

IntPtr handleDeviceContext = e.Graphics.GetHdc();
  1. Continue render on paint, now my modified blit code gets executed:
// Bind the multisampled FBO for reading
gl.BindFramebufferEXT(OpenGL.GL_READ_FRAMEBUFFER_EXT, frameBufferMultiID);
// Bind the normal FBO for drawing
gl.BindFramebufferEXT(OpenGL.GL_DRAW_FRAMEBUFFER_EXT, frameBufferID);
// Blit the multisampled FBO to the normal FBO
gl.BlitFramebufferEXT(0, 0, width, height, 0, 0, width, height, 
    OpenGL.GL_COLOR_BUFFER_BIT, OpenGL.GL_NEAREST);
//Bind the normal FBO for reading
gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, frameBufferID);

  1. Unmodified SharpGL code finalizes the render iteration:
gl.ReadBuffer(OpenGL.GL_COLOR_ATTACHMENT0_EXT);

//  Read the pixels into the DIB section.
gl.ReadPixels(0, 0, Width, Height, OpenGL.GL_BGRA, 
    OpenGL.GL_UNSIGNED_BYTE, dibSection.Bits);

//  Blit the DC (containing the DIB section) to the target DC.
Win32.BitBlt(hdc, 0, 0, Width, Height,
    dibSectionDeviceContext, 0, 0, Win32.SRCCOPY);

...
e.Graphics.ReleaseHdc(handleDeviceContext);

However, the renderer is now broken and I see only a black background instead of my scene with my 3D objects.

Most likely, I missing something or I’m doing something wrong in the init phase or during the render iteration. I’m pretty confident an experienced OpenGL developer will notice something wrong in my modifications.

Thank you for reading this long wall of text. Maybe there are too many details and you can ignore the Win32 stuff, but I wanted to make sure I included the entire flow how it’s implemented in SharpGL.