Tutorial: Multiple Windows with GLFW3 and GLEW MX

Introduction

I recently wrote a short demo program on how to setup a Multi-window Multi-context OpenGL demo using GLFW3. I thought I’d write a tutorial on how to setup this demo for yourself.

The tutorial is specific to MS Visual Studio 2012, however all the code and 3rd party libs are platform independent, so it shouldn’t be hard to port this to Mac/Linux.

The full source code is available here.

Update: Full source code for Visual Studio 2013 can be found here.

Background

GLFW3, the successor to the popular GLFW2 cross-platform window management framework is nearing release. Update: GLFW3 was release some time ago now. Among it’s new features is support for multiple windows (each with a separate OpenGL context). Seeing this i decided to give it a go.

The Goal is to draw the same scene to two different windows, giving each window a separate camera so they can see the scene from different angles.  for the demo we’ll use a simple rotating quad as the “scene”.

Using multiple windows involves solving several challenges, including:

  • Tracking which OpenGL Context is active.
  • Sharing Data between the different OpenGl Contexts (and therefor Window).
  • Being able to re-size each window individually and have the correct OpenGL Context update.
  • Closing windows at any time.

Assumptions

This tutorial assumes the following:

  • That your familiar with OpenGL, including loading textures and shaders.
  • That you are proficient in C++.
  • That you know how to use visual Studio, specifically in regards to solution/project setup and linking to 3rd party libs.

If any of the above don’t apply to you then your going to have a hard time completing this tutorial.

Setting Things Up

Before we can start coding we’ll need to setup a Visual Studio solution as well as compile and link in the 3rd party library’s.

The Visual Studio Solution

To start with create a new, empty console project in Visual Studio, once created do the following:

  1. In the solution directory add a directory called “Lib” to hold the 3rd part libs. I’ll leave it up to you to decide how you want to organise this folder, I’d recommend creating sub folders for GLFW3, GLEW and GLM.
  2. Download the “constants.hpp” file from here and add it to the solution. This file contains some constants, including the shaders we’ll use, default screen size, etc.
  3. Add a “main.cpp” file to the project, this file is where we’ll write all our code.

Now we’re ready to start adding the libs.

Setting Up GLFW

The following instructions are for including GLFW3 as a static library. You’ll need to have CMake installed to build GLFW3.

UPDATE: GLFW 3.0 has recently been officially released by the developers. As a result you can now download The pre-compiled binaries from the GLFW website. If you still want to build it yourself you can find the official guide here.

  1. Download the latest GLFW3 source code from Github.
  2. Extract the GLFW3 source to a working directory.
  3. Copy the “GLFW” directory from the “include” folder to the “Lib” directory in the Visual Studio tutorial solution. This folder contains the GLFW3 header files.
  4. Run CMake, specify the  GLFW3 source directory.
  5. Setup a build directory (I use a sub folder in the working directory called “Build”).

    CMake
    Source and Build Directory for GLFW3 in CMake.
  6. Press Configure in CMake, When Prompted select “Visual Studio 11″ from the drop down. This will generate Visual Studio 2012 solution files for GLFW3.
  7. Press Generate to create a solution file.
  8. In the Build specified to CMake directory open up the GLFW.sln file.
  9. In Visual Studio, Select release and build the solution. Once the build has completed you can open the “test\Release” directory and run the test exe’s to make sure everything works.
  10. Copy the glfw3.lib file from the “src\Release” directory to the “Lib” directory in the Visual Studio tutorial solution.
  11. To include GLFW3 in your vVsual Studio Project do the set the following in the project settings (don’t forget to select “All Configurations”):
  • Under C/C++ -> General -> Additional Include Directories add the GLFW3 include folder (it should contain the GL folder we copied in step 3), for me this is: “$(ProjectDir)../Lib/glfw3/Include”
  • Under Linker -> General -> Additional Library Directories add the GLFW3 lib folder (where we copied the glfw3.lib in step 3), for me this is: “$(ProjectDir)../Lib/glfw3/Lib”
  • Under Linker -> Input -> Additional Dependencies add “glfw3.lib”, “opengl32.lib” (required by GLFW3) and “glu32.lib” (helps us check for OpenGL errors).

Setting UP GLEW

Once again we’ll use the Static Library for GLEW. Note that we will be using GLEW MX. GLEW MX is a version of GLEW that supports multiple OpenGL contexts in a thread safe way. There is some extra work needed to get this version of GLEW working which I’ll go over later.

  1. You can download the GLEW library files from here, choose the Window 32-bit binary package.
  2. Extract the zip file to a temporary directory.
  3. Copy the “glew-1.9.0\lib\glew32mxs.lib” file to our “Lib” directory.
  4. Copy the “glew-1.9.0\include\GL” (containing the GLEW headers) into our “Lib” directory.
  5. In our Project Properties set the following (again, don’t forget to select “All Configurations”):
  • Under C/C++ -> General -> Additional Include Directories add the GLEW include directory. for me this was: “$(ProjectDir)../Lib/glew-1.9.0/Include”
  • Under Linker -> General -> Additional Library Directories add the GLEW lib Directory, for me this was: “$(ProjectDir)../Lib/glew-1.9.0/Lib”
  • Under Linker -> Input -> Additional Dependencies add “glew32mxs.lib”.
  • Under C/C++ -> Preprocessor -> Preprocessor Definitions add “GLEW_STATIC” and “GLEW_MX”. GLEW_STATIC tells GLEW we are using the Static Lib and GLEW_MX tells GLEW that we are using the Multi-Context Version.

Note that the version of GLEW we downloaded from the website is compiled in visual studio 2010. While it works just fine If you want to compile it in visual Studio 2012 you can. The solution/project files provided with the GLEW source code convert without problems, however when you try to compile you’ll encounter a “RC2102: string literal too long” error (a bug has all ready been logged with the GLEW developers about this). Use the Error list to find the offending string (it’s the GLEW license in the glwe.rc file). You can solve the problem by breaking the string up into multiple ‘VALUE “Comments”‘ fields (I broke it up every \r\n) and don’t forget to add the ” at the end of the lines. Once it compiles just follow the instructions above.

Setting up GLM

We’re using GLM for our maths library. It’s a header only library and can be downloaded from here.

  1. Copy the “glm-0.9.4.3\glm” directory to our “Lib” directory.
  2. In our Project Properties, under C/C++ -> General -> Additional Include Directories add the GLM include directory, for me this was: “$(ProjectDir)../Lib/glm-0.9.4.3/”.

Coding it up

Now for the fun part :). Remember you can access the full source code here.

Boilerplate

We’ start by adding our header files:

// Note that the following includes must be defined in order:
 #include "PulsarTechDemoConfig.h"
 #include "GL\glew.h"
 #include "glfw\glfw3.h"

// Note that the following Includes do not need to be defined in order:
 #include
 #include
<map>
 #include
 #define GLM_SWIZZLE // Enable GLM Swizzling, must be before glm is included!
 #include "glm\glm.hpp" // Core GLM stuff, same as GLSL math.
 #include "glm\ext.hpp" // GLM extensions.

Next is some Function definitions and our main() function:

int Init();
int MainLoop();
int ShutDown();

int main(int argc, char* argv[])
{
     int iReturnCode = EC_NO_ERROR;

     iReturnCode = Init();
     if (iReturnCode != EC_NO_ERROR)
          return iReturnCode;

     iReturnCode = MainLoop();
     if (iReturnCode != EC_NO_ERROR)
          return iReturnCode;

     iReturnCode = ShutDown();
     if (iReturnCode != EC_NO_ERROR)
          return iReturnCode;

     return iReturnCode;
}

We’ll be placing all our initialization code in the Init() function, MainLoop() will hold our main render loop and ShutDown() will be responsible for clean up. Our main() function just calls each of these functions in order, checking their return code to make sure that everything worked as expected.

Window/Context Management

The hardest part of developing the original demo was working out how to manage which OpenGL context is current. The problem is that the multi-context version of GLEW needs to know which GLEW/OpenGL context is active. According to the GLEW Documentation you need to define a glewGetContext() function that returns a GLEWContext pointer. GLEW needs this function to know which OpenGL Context is issuing OpenGL calls. Note that a GLEWContext is different from a OpenGL Context, the OpenGL context is created for us by GLFW3 when we create a window. Therefor we need to create and initialize a GLEWContext for each Window/OpenGL context created using GLFW3 as well as have a way to track this relationship. We also need to track which OpenGL context is current and using this information we can make sure glewGetContext() returns a pointer to the corresponding GLEWContext. Because glewGetContext() gets called every time we use gl*() function we want it to be as small/fast as possible.

So to sum up we need to:

  • Track which OpenGL Context/Window matches which GLEWContext (decided when we first create them).
  • Track Which Context (GLEW and OpenGL) is active.
  • Have a glewGetContext() function that returns a pointer to the active GLEWContext.
  • Have a way to switch contexts easily for rendering that can handle have windows added/removed during the Main Render Loop.

Note: For a more general discussion of using Multiple OpenGL Contexts see my OpenGL Multi-Context ‘Fact’ Sheet.

We’ll start be declaring some utility functions, global variables at the top of our main.cpp:

struct Window
{
     GLFWwindow*  m_pWindow;
     GLEWContext* m_pGLEWContext;
     unsigned int m_uiWidth;
     unsigned int m_uiHeight;
     glm::mat4    m_m4Projection;
     glm::mat4    m_m4ViewMatrix;
     unsigned int m_uiID;
};
typedef Window* WindowHandle;

unsigned int g_uiWindowCounter = 0;
std::list g_lWindows;
WindowHandle g_hCurrentContext = nullptr;

void GLFWWindowSizeCallback(GLFWwindow* a_pWindow, int a_iWidth, int a_iHeight);
GLEWContext* glewGetContext(); // This needs to be defined for GLEW MX to work, along with the GLEW_MX define in the perprocessor!
void MakeContextCurrent(WindowHandle a_hWindowHandle);

The Window struct is used to collect all the data relevant to a single window. It holds the GLFW3 window handle, the GLEWContext the  window width/height, the Projection/View matrices (we want these to be unique to each window) and a unique window ID (you’ll see why we need this later). I’ve created a WindowHandle type as an easy way to get a pointer to a Window struct as we’ll be using them a lot.

The g_uiWindowCounter global variable is used to create unique IDs for each window.

The list g_lWindows will hold the handles to each window we create. Using the list makes it easy to loop through each window at any time and perform window/context specific actions. It also help us to close any window at any time.

The g_hCurrentContext variable points to the window whose OpenGL/GLEW Context is currently active.

The GLFWWindowSizeCallBack() function is provided to GLFW3 as the call back to be used whenever a window is resized.

The glewGetContext() has already been discussed, this is its definition as specified in the GLEW Docs. It uses g_hCurrentContext to know which Variable is currently active.

The MakeContextCurrent() function is a wrapper for the glfw3 function glfwMakeContextCurrent(), it takes in our WindowHandle type and makes sure to set g_hCurrentContext to the correct window when it changes the context. This is how we track which context is active.

Now that I’ve explained everything, here’s the code for the functions:

void GLFWWindowSizeCallback(GLFWwindow* a_pWindow, int a_iWidth, int a_iHeight)
{
     // find the window data corrosponding to a_pWindow;
     WindowHandle window = nullptr;

     for (auto& itr : g_lWindows)
     {
          if (itr->m_pWindow == a_pWindow)
          {
               window = itr;
               window->m_uiWidth = a_iWidth;
               window->m_uiHeight = a_iHeight;
               window->m_m4Projection = glm::perspective(45.0f, float(a_iWidth)/float(a_iHeight), 0.1f, 1000.0f);
          }
     }

     WindowHandle previousContext = g_hCurrentContext;
     MakeContextCurrent(window);
     glViewport(0, 0, a_iWidth, a_iHeight);
     MakeContextCurrent(previousContext);
}

 

GLEWContext* glewGetContext()
{
     return g_hCurrentContext->m_pGLEWContext;
}

 

void MakeContextCurrent(WindowHandle a_hWindowHandle)
{
     if (a_hWindowHandle != nullptr)
     {
          glfwMakeContextCurrent(a_hWindowHandle->m_pWindow);
          g_hCurrentContext = a_hWindowHandle;
     }
}

Initialisation

Now its time to setup Our Initialization Logic where we’ll create our windows as well as a quad to render.

Start by adding the following Declarations to the top of the main.cpp File:

std::map g_mVAOs;
unsigned int g_VBO = 0;
unsigned int g_IBO = 0;
unsigned int g_Texture = 0;
unsigned int g_Shader = 0;
glm::mat4 g_ModelMatrix;

struct Vertex
{
     glm::vec4 m_v4Position;
     glm::vec2 m_v2UV;
     glm::vec4 m_v4Colour;
};

void GLFWErrorCallback(int a_iError, const char* a_szDiscription);

WindowHandle CreateWindow(int a_iWidth, int a_iHeight, const std::string& a_szTitle, GLFWmonitor* a_pMonitor, WindowHandle a_hShare);

void CheckForGLErrors(std::string a_szMessage);

Anyone familiar with OpenGL will recognize what’s going on here. One thing to note; we are using a map of VAOs for a reason. We do this because they are not shared between OpenGL contexts (at least no on windows), unlike IBOs/VBO/Textures/Shaders. Remember the Window ID I talked about earlier? We’ll be using that to as index into the map so we can retrieve the correct VAO for the context we are currently rendering with.

Next add the definitions for GLFWErrorCallback() and CheckForGLErrors():

void GLFWErrorCallback(int a_iError, const char* a_szDiscription)
{
     printf("GLFW Error occured, Error ID: %i, Description: %s\n", a_iError, a_szDiscription);
}

 

void CheckForGLErrors(std::string a_szMessage)
{
     GLenum error = glGetError();
     while (error != GL_NO_ERROR)  // make sure we check all Error flags!
     {
          printf("Error: %s, ErrorID: %i: %s\n", a_szMessage.c_str(), error, gluErrorString(error));
          error = glGetError(); // get next error if any.
     }
}

This is all pretty self explanatory, all they do is help with debugging GLFW3 and OpenGL.

Next up is the CreateWindow() function:

WindowHandle CreateWindow(int a_iWidth, int a_iHeight, const std::string& a_szTitle, GLFWmonitor* a_pMonitor, WindowHandle a_hShare)
{
     // save current active context info so we can restore it later!
     WindowHandle hPreviousContext = g_hCurrentContext;

     // create new window data:
     WindowHandle newWindow = new Window();

     if (newWindow == nullptr)
          return nullptr;

     newWindow->m_pGLEWContext = nullptr;
     newWindow->m_pWindow = nullptr;
     newWindow->m_uiID = g_uiWindowCounter++; // set ID and Increment Counter!
     newWindow->m_uiWidth = a_iWidth;
     newWindow->m_uiHeight = a_iHeight;

     // Create Window:
     if (a_hShare != nullptr) // Check that the Window Handle passed in is valid.
     {
          newWindow->m_pWindow = glfwCreateWindow(a_iWidth, a_iHeight, a_szTitle.c_str(), a_pMonitor, a_hShare->m_pWindow); // Window handle is valid, Share its GL Context Data!
     }
     else
     {
          newWindow->m_pWindow = glfwCreateWindow(a_iWidth, a_iHeight, a_szTitle.c_str(), a_pMonitor, nullptr); // Window handle is invlad, do not share!
     }

     // Confirm window was created successfully:
     if (newWindow->m_pWindow == nullptr)
     {
          printf("Error: Could not Create GLFW Window!\n");
          delete newWindow;
          return nullptr;
     }

     // create GLEW Context:
     newWindow->m_pGLEWContext = new GLEWContext();
     if (newWindow->m_pGLEWContext == nullptr)
     {
          printf("Error: Could not create GLEW Context!\n");
          delete newWindow;
          return nullptr;
     }

     MakeContextCurrent(newWindow); // Must be done before init of GLEW for this new Window/Context! ;)

     // Init GLEW for this context:
     GLenum err = glewInit();

     if (err != GLEW_OK)
     {
          // a problem occured when trying to init glew, report it:
          printf("GLEW Error occured, Description: %s\n", glewGetErrorString(err));
          glfwDestroyWindow(newWindow->m_pWindow);
          delete newWindow;
          return nullptr;
     }

     // setup callback for window size changes:
     glfwSetWindowSizeCallback(newWindow->m_pWindow, GLFWWindowSizeCallback);

     // add new window to the list::
     g_lWindows.push_back(newWindow);

     // now restore previous context:
     MakeContextCurrent(hPreviousContext);

     return newWindow;
}

To summarize what’s happening here:

  1. We start by recording the current context (if any). This is important because to create a new window we must first change the context. We want to be able to restore the original state/context before we return.
  2. Next up the we new a Window struct to hold all our data and initialize it.
  3. We call glfwCreateWindow() to create our new window, passing it the window width/height, Title, desired monitor (outside the scope of this tutorial) and the Window whose context we want to share. This last parameter is important, if we pass GLFW3 a handle to an existing window then we can share Texture/VBOs/Shaders/Etc. between contexts saving us memory on the graphics card.
  4. Next up is the GLEW Context creation. This is a simple as going new GLEWContext(); then adding to the Window struct.
  5. After creating the GLEW Context we need to make the newly created window current. GLEW binds the context we create to the currently active OpenGL context, so If it is this is not done before we initialize the GLEWContext then we will end up with an invalid GLEW context.
  6. We initialize GLEW by calling glewInit(). If it isn’t clear already we need to do this for each GLEWContext we create.
  7. We setup the window resize call back we created earlier.
  8. Finally we restore the current context to what it was before.

Next we have the actual Init() function itself:

int Init()
{
     // Setup Our GLFW error callback, we do this before glfwInit so we know what goes wrong with it if it fails:
     glfwSetErrorCallback(GLFWErrorCallback);

     // Init GLFW:
     if (!glfwInit())
          return EC_GLFW_INIT_FAIL;

     // create our first window:
     WindowHandle hPrimaryWindow = CreateWindow(c_iDefaultScreenWidth, c_iDefaultScreenHeight, "First", nullptr, nullptr);

     if (hPrimaryWindow == nullptr)
     {
          glfwTerminate();
          return EC_GLFW_FIRST_WINDOW_CREATION_FAIL;
     }

     // Print out GLFW, OpenGL version and GLEW Version:
     int iOpenGLMajor = glfwGetWindowAttrib(hPrimaryWindow->m_pWindow, GLFW_CONTEXT_VERSION_MAJOR);
     int iOpenGLMinor = glfwGetWindowAttrib(hPrimaryWindow->m_pWindow, GLFW_CONTEXT_VERSION_MINOR);
     int iOpenGLRevision = glfwGetWindowAttrib(hPrimaryWindow->m_pWindow, GLFW_CONTEXT_REVISION);
     printf("Status: Using GLFW Version %s\n", glfwGetVersionString());
     printf("Status: Using OpenGL Version: %i.%i, Revision: %i\n", iOpenGLMajor, iOpenGLMinor, iOpenGLRevision);
     printf("Status: Using GLEW %s\n", glewGetString(GLEW_VERSION));

     // create our second window:
     WindowHandle hSecondaryWindow = CreateWindow(c_iDefaultScreenWidth, c_iDefaultScreenHeight, "second", nullptr, hPrimaryWindow);

     MakeContextCurrent(hPrimaryWindow);

     // create shader:
     GLint iSuccess = 0;
     GLchar acLog[256];
     GLuint vsHandle = glCreateShader(GL_VERTEX_SHADER);
     GLuint fsHandle = glCreateShader(GL_FRAGMENT_SHADER);
     glShaderSource(vsHandle, 1, (const char**)&c_szVertexShader, 0);
     glCompileShader(vsHandle);
     glGetShaderiv(vsHandle, GL_COMPILE_STATUS, &iSuccess);
     glGetShaderInfoLog(vsHandle, sizeof(acLog), 0, acLog);

     if (iSuccess == GL_FALSE)
     {
          printf("Error: Failed to compile vertex shader!\n");
          printf(acLog);
          printf("\n");
     }

     glShaderSource(fsHandle, 1, (const char**)&c_szPixelShader, 0);
     glCompileShader(fsHandle);
     glGetShaderiv(fsHandle, GL_COMPILE_STATUS, &iSuccess);
     glGetShaderInfoLog(fsHandle, sizeof(acLog), 0, acLog);

     if (iSuccess == GL_FALSE)
     {
          printf("Error: Failed to compile fragment shader!\n");
          printf(acLog);
          printf("\n");
     }

     g_Shader = glCreateProgram();
     glAttachShader(g_Shader, vsHandle);
     glAttachShader(g_Shader, fsHandle);
     glDeleteShader(vsHandle);
     glDeleteShader(fsHandle);

     // specify Vertex Attribs:
     glBindAttribLocation(g_Shader, 0, "Position");
     glBindAttribLocation(g_Shader, 1, "UV");
     glBindAttribLocation(g_Shader, 2, "Colour");
     glBindFragDataLocation(g_Shader, 0, "outColour");
     glLinkProgram(g_Shader);
     glGetProgramiv(g_Shader, GL_LINK_STATUS, &iSuccess);
     glGetProgramInfoLog(g_Shader, sizeof(acLog), 0, acLog);

     if (iSuccess == GL_FALSE)
     {
          printf("Error: failed to link Shader Program!\n");
          printf(acLog);
          printf("\n");
     }

     glUseProgram(g_Shader);
     CheckForGLErrors("Shader Setup Error");

     // create and load a texture:
     glm::vec4 *texData = new glm::vec4[256 * 256];
     for (int i = 0; i < 256 * 256; i += 256)
     {
          for (int j = 0; j < 256; ++j)           {                if (j % 2 == 0)                {                     texData[i + j] = glm::vec4(0, 0, 0, 1);                }                else                {                     texData[i + j] = glm::vec4(1, 1, 1, 1);                }           }      }      glGenTextures( 1, &g_Texture );      glBindTexture( GL_TEXTURE_2D, g_Texture );      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 256, 256, 0, GL_RGBA, GL_FLOAT, texData);      CheckForGLErrors("Texture Generation Error");      // specify default filtering and wrapping      glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);      glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);      glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );      glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );      // set the texture to use slot 0 in the shader      GLuint texUniformID = glGetUniformLocation(g_Shader,"diffuseTexture");      glUniform1i(texUniformID,0);      CheckForGLErrors("Texture Loading Error");      // Cleanup Texture Data:      delete[] texData; texData = nullptr;      // now create a quad:      Vertex aoVertices[4];      aoVertices[0].m_v4Position = glm::vec4(-2,0,-2,1);      aoVertices[0].m_v2UV = glm::vec2(0,0);      aoVertices[0].m_v4Colour.xyzw = 1.0f; // = glm::vec4(0,1,0,1);      aoVertices[1].m_v4Position = glm::vec4(2,0,-2,1);      aoVertices[1].m_v2UV = glm::vec2(1,0);      aoVertices[1].m_v4Colour = glm::vec4(1,0,0,1);      aoVertices[2].m_v4Position = glm::vec4(2,0,2,1);      aoVertices[2].m_v2UV = glm::vec2(1,1);      aoVertices[2].m_v4Colour = glm::vec4(0,1,0,1);      aoVertices[3].m_v4Position = glm::vec4(-2,0,2,1);      aoVertices[3].m_v2UV = glm::vec2(0,1);      aoVertices[3].m_v4Colour = glm::vec4(0,0,1,1);      unsigned int auiIndex[6] = {           3,1,0,           3,2,1      };      // Create VBO/IBO      glGenBuffers(1, &g_VBO);      glGenBuffers(1, &g_IBO);      glBindBuffer(GL_ARRAY_BUFFER, g_VBO);      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_IBO);      glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(Vertex), aoVertices, GL_STATIC_DRAW);      glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), auiIndex, GL_STATIC_DRAW);      // Now do window specific stuff, including:      // --> Creating a VAO with the VBO/IBO created above!
     // --> Setting Up Projection and View Matricies!
     // --> Specifing OpenGL Options for the window!

     for (auto window : g_lWindows)
     {
          MakeContextCurrent(window);

          // Setup VAO:
          g_mVAOs[window->m_uiID] = 0;
          glGenVertexArrays(1, &(g_mVAOs[window->m_uiID]));
          glBindVertexArray(g_mVAOs[window->m_uiID]);
          glBindBuffer(GL_ARRAY_BUFFER, g_VBO);
          glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_IBO);
          glEnableVertexAttribArray(0);
          glEnableVertexAttribArray(1);
          glEnableVertexAttribArray(2);
          glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
          glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), ((char*)0) + 16);
          glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), ((char*)0) + 24);

          CheckForGLErrors("Creating VAO Error");

          // Setup Matrix:
          window->m_m4Projection = glm::perspective(45.0f, float(window->m_uiWidth)/float(window->m_uiHeight), 0.1f, 1000.0f);
          window->m_m4ViewMatrix = glm::lookAt(glm::vec3(window->m_uiID * 8,8,8), glm::vec3(0,0,0), glm::vec3(0,1,0));

          // set OpenGL Options:
          glViewport(0, 0, window->m_uiWidth, window->m_uiHeight);
          glClearColor(0.25f,0.25f,0.25f,1);
          glEnable(GL_DEPTH_TEST);
          glEnable(GL_CULL_FACE);

          CheckForGLErrors("OpenGL Options Error");
     }

     return EC_NO_ERROR;
}

Again most of this will be familiar to anyone who knows their OpenGL, however there are a couple of things to point out:

  1. When creating our second window we are passing the first window through as the last parameter. As you’ll remember this will allow sharing between our two OpenGL Contexts.
  2. After creating our windows we need to bind one of them to have an active context (if you think back to the CreateWindow() function it should be obvious why).
  3. You’ll notice at the end of the function I’m using a ranged based for loop to iterate through our windows. The code in the body of the for loop sets up the things that are not shared between OpenGL Contexts/Windows.
  4. You’ll notice I’m access the correct VAO via the g_mVAOs using the Widow ID.

Main Loop

Now for the Main Loop, first we’ll setup a utility function that will tell us if we should close the demo. Start by adding the follow declaration to the top of the main.cpp file:

bool ShouldClose();

And for the function Definition:

bool ShouldClose()
{
     if (g_lWindows.empty())
          return true;

     std::list lToDelete;
     for (const auto& window : g_lWindows)
     {
          if (glfwWindowShouldClose(window->m_pWindow))
          {
               lToDelete.push_back(window);
          }
     }

     if (!lToDelete.empty())
     {
          // we have windows to delete, Delete them:
          for (auto& window : lToDelete)
          {
               delete window->m_pGLEWContext;
               glfwDestroyWindow(window->m_pWindow);
               delete window;
               g_lWindows.remove(window);
          }
     }

     if (g_lWindows.empty())
          return true;

     return false;
}

Basically this function will check to see if any of the open windows should close (e.g. the user has clicked on the ‘X’ for that window). If there are it deletes the window. The function will always return false so long as there is at least one window still open, the reason for this behaviour will become obvious when you see how its used in the main loop.

Speaking of the main loop here it is:

int MainLoop()
{
     while (!ShouldClose())
     {
          // Keep Running!

          // Get delta time for this iteration:
          float fDeltaTime = (float)glfwGetTime();

          // Spin the Qaud:
          glm::mat4 identity;
          g_ModelMatrix = glm::rotate(identity, fDeltaTime * 10.0f, glm::vec3(0.0f, 1.0f, 0.0f));

          // draw each window in sequence:
          for (const auto& window : g_lWindows)
          {
               MakeContextCurrent(window);

               // clear the backbuffer to our clear colour and clear the depth buffer
               glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
               glUseProgram(g_Shader);
               GLuint ProjectionID = glGetUniformLocation(g_Shader,"Projection");
               GLuint ViewID = glGetUniformLocation(g_Shader,"View");
               GLuint ModelID = glGetUniformLocation(g_Shader,"Model");

               glUniformMatrix4fv(ProjectionID, 1, false, glm::value_ptr(window->m_m4Projection));
               glUniformMatrix4fv(ViewID, 1, false, glm::value_ptr(window->m_m4ViewMatrix));
               glUniformMatrix4fv(ModelID, 1, false, glm::value_ptr(g_ModelMatrix));

               glActiveTexture(GL_TEXTURE0);
               glBindTexture( GL_TEXTURE_2D, g_Texture );
               glBindVertexArray(g_mVAOs[window->m_uiID]);
               glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

               glfwSwapBuffers(window->m_pWindow);

               CheckForGLErrors("Render Error");
          }

          glfwPollEvents(); // process events!
     }

     return EC_NO_ERROR;
}

By now it should be obvious to you what’s going on here. You’ll notice that were using ShouldClose() as the condition for the for loop (with a not ! in front of it), so as long as we have a window our main loop will keep running.

Shutdown

Now for our Shutdown/Clean up Logic. We can dive straight into this one:

int ShutDown()
{
     // cleanup any remaining windows:
     for (auto& window : g_lWindows)
     {
          delete window->m_pGLEWContext;
          glfwDestroyWindow(window->m_pWindow);
          delete window;
     }

     // terminate GLFW:
     glfwTerminate();
     return EC_NO_ERROR;
}

You’ll notice that I’m not doing anything to clean up the Textures/VBOs/Etc. There are two reason for this, first I didn’t want to over complicate the demo (see second point). Second, according to the OpenGL Spec, each context should clean up after itself, deleting any unused resources that it owns. In the case of shard resources, it is the responsibility of the last context who shared them to delete them on exit. Normally I wouldn’t rely on OpenGL to do this, just because its in the spec doesn’t mean any given OpenGL implementation will actually do it.

Conclusion

By this point, assuming my instructions are any good, you should have a working Multi-window multi-context OpenGL demo running. There a re probable other ways to solve the context management problem and I encourage people to leave a comment if they know of a different/better way to do it.

Eventually I’ll get around to a follow up to this tutorial, probable focused on setting up full screen windows, monitor selection and per window camera handling. Until then Have fun with GLFW3 and multiple windows!

About these ads

9 thoughts on “Tutorial: Multiple Windows with GLFW3 and GLEW MX”

  1. Excellent tutorial. I’ve been trying to build my own openGL project for the better part of the day (using dynamically-linked libraries), but I couldn’t seem to get glewInit() working. I didn’t go through the additional GLEW MX setup you laid out, and everything seems to be working as I’d hoped.

    Thanks a bunch for taking the time to make this.

    Like

  2. glfwSwapBuffers(window->m_pWindow); // make this loop through all current windows??

    That comment is weird, that line works the way its expected to? only swaps the buffers for the current glfw + glew context?
    Also Good tutorial, very solid better than almost all the other tutorials online for any openGL.

    Like

    1. Yeah that is a weird comment. Looks like it was a ‘Note to self’ to remind me to double check that glfwSwapBuffers() is in the right place. All fixed now :)

      To answer your question, Yes the line does work the way its supposed to, glfwSwapBuffers() needs to be run once at the end of each frame on a per window/openGL context Basis. Every Window/OpenGL Context has its own separate framebuffer which we are rendering into, thus the requirement to call this for each window.

      Here’s the relevant section of the GLFW docs.

      Like

  3. glfwMakeContextCurrent(newWindow->m_pWindow); // Must be done before init of GLEW for this new windows Context!
    MakeContextCurrent(newWindow); // and must be made current too

    second call is redundant, nice smiley thou

    Like

  4. Cool stuff!
    A quick question. How would you handle mouse/keyboard callbacks for different windows? Supposedly they have different behavior.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s