GLESGAE:Setting Up A Window and Context
Contents |
GLESGAE - Setting Up A Window and Context
Introduction
For the most part, opening up a Window and generating a rendering Context is a pretty simple task, and gets you on your way to pushing stuff to the screen.
Of course, doing so in a manner that's open enough to add differing platforms to at later date can be a bit tricky - especially so when some platforms have widely different ideas on what a Window actually is, and how that Window gets created.
For the GLESGAE engine, we will be using C++ predominantly to abstract things out for us.
That's not to say you couldn't do the same thing in C, but I find that even just the addition of classes makes things cleaner to work with - specially with multiple platforms.
With that out of the way, let's have a look at getting a window opened using X directly.
Why X? Why not SDL?
SDL is good.. I do like SDL.. but SDL can also be a bit heavy - especially if all you're wanting to do is use it to open up a Window!
Granted, generally you want to steal use of it's event system as well for access to controllers.. but our Pandora has some rather custom controllers so even then you're jumping out of SDL to use them.
We're also going to be using GL ES rather than pushing pixels directly, so.. perhaps in this instance, why SDL?
I also like coding down as close to the hardware if at all possible - as then if something goes wrong, it's my own fault, and not some black box library that I'm not sure what's going on in.
There's also the real possibility of perhaps porting your work to a platform that SDL hasn't been ported to yet; or has a poor implementation of. You'd need to start adding a ton of custom classes just for that platform, where if you ignored it from the start, your custom class framework already exists so you're not trying to jerry-rig it in!
Therefore, GLESGAE will be battering X directly, as that's what the base library is on Linux - and by extension - Pandora.
I'll also add WinAPI support into the SVN later on, but for now, only dealing with Linux/Pandora is best.
Checking out the SVN
svn co -r 2 http://svn3.xp-dev.com/svn/glesgae/trunk glesgae This tutorial uses SVN revision 2, so be sure to check that out for the full code.
Opening A Window
Actually opening a Window is fairly trivial.
It sometimes looks like a crazy long piece of madness, but you get a lot of control over how your window appears.
WinAPI is especially mad - huge amount of code just to open a window, but you get quite fine-grained control over it.
As we're aiming to be cross-platform from the outset, we'll split some stuff up out the road, so let's go create some directory hierarchy.
- Graphics
- Window
- Context
We'll deal with the Window bit first.
The Window Class
Some systems allow us to create multiple windows, others use the entire screen as one window. In a game application, we're usually only interested in the latter, so we'll design primarily for that use case.
Window.h
#ifndef _WINDOW_H_
#define _WINDOW_H_
namespace GLESGAE
{
class RenderContext;
class Window
{
public:
Window()
: mContext(0)
, mWidth(0U)
, mHeight(0U)
{
}
virtual ~Window() {}
/// Open this window with the given width and height
virtual void open(const unsigned int width, const unsigned int height) = 0;
/// Set this Window's Render Context
virtual void setContext(RenderContext* const context) = 0;
/// Get this Window's Render Context - returning the specified type
template<typename T_Context> const T_Context* getContext() const { return reinterpret_cast<T_Context*>(mContext); }
/// Get this Window's Render Context
const RenderContext* getContext() const { return mContext; }
/// Get the Width of this Window
const unsigned int getWidth() const { return mWidth; }
/// Get the Height of this Window
const unsigned int getHeight() const { return mHeight; }
/// Refresh the Window
virtual void refresh() = 0;
/// Close this window
virtual void close() = 0;
protected:
RenderContext* mContext;
unsigned int mWidth;
unsigned int mHeight;
};
}
#endif
This is our Window class.
We'll be using the Interface paradigm a lot, which is why these are all pure virtual functions ( means they must be overloaded in derived classes. )
There's also one funny thing we're doing here with getContext - and that's making it a templated function so we can directly cast to the type we want via GLES1Context* myContext(myWindow->getContext<GLES1Context>()); rather than having to pull the pointer out, then waste another line to reinterpret_cast the pointer ourselves.
Of course, we also specify a standard getContext should we just need to use the standard interface - but grabbing the specific type in one line is particularly handy at times.
So, let's implement the X11 window open-up-er!
X11 Window Class
X11Window.h
#ifndef _X11_WINDOW_H_
#define _X11_WINDOW_H_
#include "Window.h"
namespace X11 {
#include <X11/Xlib.h>
}
namespace GLESGAE
{
class X11Window : public Window
{
public:
X11Window();
~X11Window();
/// Open this window with the given width and height
void open(const unsigned int width, const unsigned int height);
/// Set this Window's Render Context
void setContext(RenderContext* const context);
/// Refresh the Window
void refresh();
/// Close this window
void close();
/// Returns the Display for platform specific bits
X11::Display* const getDisplay() const { return mDisplay; }
/// Returns the Window for platform specific bits
X11::Window const getWindow() const { return mWindow; }
private:
X11::Display* mDisplay;
X11::Window mWindow;
};
}
#endif
This one does some mad stuff.
We wrap the Xlib include around an X11 namespace. This might seem mad but.. we've just created our own Window class, and X11 has it's own Window class so there's a conflict there.
Luckily with C++, you can set namespaces to separate chunks of code - and that's exactly what we're doing here!
NOTE: I've actually removed the namespace hacks and just renamed everything to stop conflicting... the namespaces for GL and X11 were starting to conflict themselves and it just proved to be a vicious nightmare when implementing shader support... so I redid it all! That said, as we're using SVN here, the SVN repository version and this guide still match up, so feel free to follow through, but remember that it changes later on!
X11Window.cpp
#include "X11Window.h"
#include "../Context/RenderContext.h"
namespace X11 {
#include <X11/Xlib.h>
}
using namespace GLESGAE;
using namespace X11;
X11Window::X11Window()
: mDisplay(XOpenDisplay(0))
, mWindow(0)
{
}
X11Window::~X11Window()
{
if (0 != mWindow)
close();
XDestroyWindow(mDisplay, mWindow);
XCloseDisplay(mDisplay);
}
void X11Window::open(const unsigned int width, const unsigned int height)
{
// Store the width and height.
mWidth = width;
mHeight = height;
// Create the actual window and store the pointer.
mWindow = XCreateWindow(mDisplay // Pointer to the Display
, DefaultRootWindow(mDisplay) // Parent Window
, 0 // X of top-left corner
, 0 // Y of top-left corner
, width // requested width
, height // requested height
, 0 // border width
, CopyFromParent // window depth
, CopyFromParent // window class - InputOutput / InputOnly / CopyFromParent
, CopyFromParent // visual type
, 0 // value mask
, 0); // attributes
// Map the window to the display.
XMapWindow(mDisplay, mWindow);
}
void X11Window::setContext(RenderContext* const context)
{
mContext = context;
}
void X11Window::refresh()
{
mContext->refresh();
XFlush(mDisplay); // this is a crutch till we handle events.. ignore!
}
void X11Window::close()
{
XDestroyWindow(mDisplay, mWindow);
}
There's our namespace hack again, and some actual code that does something!
As stated in the comment, ignore that XFlush.. it's because we haven't written anything to deal with events yet, so that flushes all events out.
The CreateWindow call has all it's parameters commented to let you know what each part is.. on a Linux machine, typing man XCreateWindow will give you the man page for what they all mean. Linux manpages are very useful in coding!
We now need a RenderContext to play with... and this is where the fun begins!
Render Contexts
As we're striving for multi-platform goodness, we need to create a base Render Context implementation that they all conform to.
We also want to ensure that our Window and Render Context stuff is separate from one another - which is what we're doing here - as then we can do things like the X11Window Class, which'll run on standard Linux machines as well as the Pandora. It's not much, but the less duplicated code in an Engine, the easier it is to maintain!
For our purposes, we have two types of Render Contexts we can create on the Pandora - GLES1 and GLES2.
Discussion - ES1 vs ES2
Choosing one over the other is not quite a trivial process as you'd think, as they each have their own little pros and cons.
ES1 is fixed function and is pretty much designed for low end hardware.
ES2 is shader based - you have full control over the vertex and fragment processing parts of the graphics pipeline.
If you're not wholly sure what this Graphics Pipeline malarkey is, I suggest you read up on it. Here's some handy links:
- http://en.wikipedia.org/wiki/Graphics_pipeline
- http://developer.nvidia.com/page/documentation.html - particularly the free books on CG and GPU Gems 1-3 - for theory if nothing else.
- http://www.khronos.org/opengles/2_X/ - specially the images at the bottom which shows what gets replaced!
However, as the old adage goes, with great power comes great responsibility. As the images on Khronos' site shows - ES2 replaces a rather large chunk of the fixed function pipeline. You therefore need to deal with the following yourself:
- Matrix Stacks for Model, View, Projection and Textures ( though Texture Stack isn't usually used much. )
- Alpha Test ( this can be an absolute killer.. )
- Transform and Lighting ( that's effectively the Matrix Stacks, and lighting is done in either Vertex or Fragment shaders now )
OpenGL 1.5 vs ES1
Generally, OpenGL 1.5 is essentially what ES1 is based on. Both are fixed function predominantly, but ES1 does rip out a number of things:
- No Immediate Mode (glBegin/glEnd).
- Tends to favour fixed point arithmetic.
- No quad rendering.
- Some Stack handling you need to do yourself.
- Display Lists, Accumulators, and a bunch of other stuff...
Generally however, if your app has been using VBOs or Vertex Arrays, there shouldn't be a great deal of a change.
One sneaky gotcha is that there are two main ES1 profiles - Common and Common Lite ... Common Lite ONLY has fixed function, where as Common has both.
Additionally, ES1.0 does not support VBOs whereas ES1.1 does.
OpenGL 2.0 vs ES2
Again, OpenGL 2.0 is essentially what ES2 is based on... but specifically, the programmable pipeline part - all fixed function pipeline functions have now been removed. Therefore, as stated above, you need to deal with:
- Matrix Stacks for Model, View, Projection and Textures ( though Texture Stack isn't usually used much. )
- Alpha Test ( this can be an absolute killer.. )
- Transform and Lighting ( that's effectively the Matrix Stacks, and lighting is done in either Vertex or Fragment shaders now )
Along with:
- Vertex Attributes
- Shader Management
- Headaches - especially when trying to balance load across the vertex and fragment processors!
The RenderContext Class
Like Window, we'll have a common interface to deal with things.
This is only a simple tutorial to instantiate a Window and Context without actually rendering anything as yet, so these classes are still pretty simple.
As we're wanting to support ES1 and ES2, we're going to do things a bit differently from our Window class as well...
RenderContext.h
#ifndef _RENDER_CONTEXT_H_
#define _RENDER_CONTEXT_H_
namespace GLESGAE
{
class Window;
class RenderContext
{
public:
RenderContext() {}
virtual ~RenderContext() {}
/// Initialise this Context
virtual void initialise() = 0;
/// Shutdown this Context
virtual void shutdown() = 0;
/// Refresh this Context
virtual void refresh() = 0;
/// Bind to a Window
virtual void bindToWindow(Window* const window) = 0;
};
}
#endif
Pretty simple.. no crazy tricks as in Window.
FixedFunctionContext.h
#ifndef _FIXED_FUNCTION_CONTEXT_H_
#define _FIXED_FUNCTION_CONTEXT_H_
namespace GLESGAE
{
class FixedFunctionContext
{
public:
FixedFunctionContext() {}
virtual ~FixedFunctionContext() {}
};
}
#endif
This is where things get a bit more interesting... this class is empty as we're not defining anything, but you may be wondering what the point of this actually is?
As stated already, I'm aiming GLESGAE to be multi-platform... some platforms have both Fixed Function and Shader Based contexts, and it's not to say you can't emulate Fixed Function on a Shader Based context either, so we shall separate the two styles and pull them in using multiple inheritance later on.
ShaderBasedContext.h
#ifndef _SHADER_BASED_CONTEXT_H_
#define _SHADER_BASED_CONTEXT_H_
namespace GLESGAE
{
class ShaderBasedContext
{
public:
ShaderBasedContext() {}
virtual ~ShaderBasedContext() {}
};
}
#endif
Again, empty.. but as stated above.. some platforms can support both.
This'll come in handy for when we want to do some development tests on our Desktop machines before pushing over to the Pandora, for instance; or any other platform we write support for.
In the repository, there is a GLXContext class which'll run under Linux; which shows how easy it is to add support for other Contexts. We're interested in the Pandora here though, so we'll generate Contexts suitable for it now.
GLES1 Context Class
Again, we want to be able to be multi platform.. we know where Pandora keeps it's EGL and GLES files, but that _can_ change depending on the platform.. so we need to protect this a bit.
GLES1Context.h
#ifndef _GLES1_CONTEXT_H_
#define _GLES1_CONTEXT_H_
#if defined(PANDORA)
#include <EGL/egl.h>
#endif
#include "RenderContext.h"
#include "FixedFunctionContext.h"
namespace GLESGAE
{
class Window;
class X11Window;
class GLES1Context : public RenderContext, public FixedFunctionContext
{
public:
GLES1Context();
~GLES1Context();
/// Initialise this Context
void initialise();
/// Shutdown this Context
void shutdown();
/// Refresh this Context
void refresh();
/// Bind us to a Window
void bindToWindow(Window* const window);
private:
X11Window* mWindow;
EGLDisplay mDisplay;
EGLContext mContext;
EGLSurface mSurface;
};
}
#endif
GLES1 only supports fixed function, so we bring that in so that when we write the Renderer, we call the right functions on it.
Technically, being dependent on X11Window is a bit naughty.. but we can deal with that later...
Nothing crazy here, so we shall continue...
GLES1Context.cpp
#if defined(GLES1)
#include <cstdio>
#include "GLES1Context.h"
#include "../Window/X11Window.h"
#if defined(PANDORA)
#include <GLES/gl.h>
#endif
using namespace GLESGAE;
GLES1Context::GLES1Context()
: mWindow(0)
, mDisplay(0)
, mContext(0)
, mSurface(0)
{
}
GLES1Context::~GLES1Context()
{
shutdown();
}
void GLES1Context::initialise()
{
// Get the EGL Display..
mDisplay = eglGetDisplay( (reinterpret_cast<EGLNativeDisplayType>(mWindow->getDisplay())) );
if (EGL_NO_DISPLAY == mDisplay) {
printf("failed to get egl display..\n");
}
// Initialise the EGL Display
if (0 == eglInitialize(mDisplay, NULL, NULL)) {
printf("failed to init egl..\n");
}
// Now we want to find an EGL Surface that will work for us...
EGLint eglAttribs[] = {
EGL_BUFFER_SIZE, 16 // 16bit Colour Buffer
, EGL_NONE
};
EGLConfig eglConfig;
EGLint numConfig;
if (0 == eglChooseConfig(mDisplay, eglAttribs, &eglConfig, 1, &numConfig)) {
printf("failed to get context..\n");
}
// Create the actual surface based upon the list of configs we've just gotten...
mSurface = eglCreateWindowSurface(mDisplay, eglConfig, reinterpret_cast<EGLNativeWindowType>(mWindow->getWindow()), NULL);
if (EGL_NO_SURFACE == mSurface) {
printf("failed to get surface..\n");
}
// Setup the EGL Context
EGLint contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 1
, EGL_NONE
};
// Create our Context
mContext = eglCreateContext (mDisplay, eglConfig, EGL_NO_CONTEXT, contextAttribs);
if (EGL_NO_CONTEXT == mContext) {
printf("failed to get context...\n");
}
// Bind the Display, Surface and Contexts together
eglMakeCurrent(mDisplay, mSurface, mSurface, mContext);
// Set up our viewport
glViewport(0, 0, mWindow->getWidth(), mWindow->getHeight());
}
void GLES1Context::shutdown()
{
eglDestroyContext(mDisplay, mContext);
eglDestroySurface(mDisplay, mSurface);
eglTerminate(mDisplay);
}
void GLES1Context::refresh()
{
// Trigger a buffer swap
eglSwapBuffers(mDisplay, mSurface);
// Clear the buffers for the next frame.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void GLES1Context::bindToWindow(Window* const window)
{
// Rememeber the Window we're bound to
mWindow = reinterpret_cast<X11Window*>(window);
// Set the context as us.
mWindow->setContext(this);
}
#endif
This one can catch you offguard as we #ifdef the entire file to ensure we're not compiling if we're specified a GLX context.
I'm also being lazy here in not actually dealing with the failure operations... technically you really should be...
The other thing to get your head around is there are two Displays here.. there is the EGLDisplay and the X11 Display.
This is just something you need to deal with, as on Android for instance, the "X11 Display" is dalvik's renderer window, but the EGLDisplay calls remain the same. It's for cross-platform compatibility more than anything else.
GLES2 Context Class
ES2 is actually almost exactly the same as ES1 at this point.. just that the class names are different, it pulls in ShaderBasedContext, and what we pass in to the EGLConfig and Context are slightly different.
For completeness sake, here are the two files anyway.
GLES2Context.h
#ifndef _GLES2_CONTEXT_H_
#define _GLES2_CONTEXT_H_
#if defined(PANDORA)
#include <EGL/egl.h>
#endif
#include "RenderContext.h"
#include "ShaderBasedContext.h"
namespace GLESGAE
{
class Window;
class X11Window;
class GLES2Context : public RenderContext, public ShaderBasedContext
{
public:
GLES2Context();
~GLES2Context();
/// Initialise this Context
void initialise();
/// Shutdown this Context
void shutdown();
/// Refresh this Context
void refresh();
/// Bind us to a Window
void bindToWindow(Window* const window);
private:
X11Window* mWindow;
EGLDisplay mDisplay;
EGLContext mContext;
EGLSurface mSurface;
};
}
#endif
GLES2Context.cpp
#if defined(GLES2)
#include <cstdio>
#include "GLES2Context.h"
#include "../Window/X11Window.h"
#if defined(PANDORA)
#include <GLES2/gl2.h>
#endif
using namespace GLESGAE;
GLES2Context::GLES2Context()
: mWindow(0)
, mDisplay(0)
, mContext(0)
, mSurface(0)
{
}
GLES2Context::~GLES2Context()
{
shutdown();
}
void GLES2Context::initialise()
{
// Get the EGL Display..
mDisplay = eglGetDisplay( (reinterpret_cast<EGLNativeDisplayType>(mWindow->getDisplay())) );
if (EGL_NO_DISPLAY == mDisplay) {
printf("failed to get egl display..\n");
}
// Initialise the EGL Display
if (0 == eglInitialize(mDisplay, NULL, NULL)) {
printf("failed to init egl..\n");
}
// Now we want to find an EGL Surface that will work for us...
EGLint eglAttribs[] = {
EGL_BUFFER_SIZE, 16 // 16bit Colour Buffer
, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT // We want an ES2 config
, EGL_NONE
};
EGLConfig eglConfig;
EGLint numConfig;
if (0 == eglChooseConfig(mDisplay, eglAttribs, &eglConfig, 1, &numConfig)) {
printf("failed to get context..\n");
}
// Create the actual surface based upon the list of configs we've just gotten...
mSurface = eglCreateWindowSurface(mDisplay, eglConfig, reinterpret_cast<EGLNativeWindowType>(mWindow->getWindow()), NULL);
if (EGL_NO_SURFACE == mSurface) {
printf("failed to get surface..\n");
}
// Setup the EGL context
EGLint contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2
, EGL_NONE
};
// Create our Context
mContext = eglCreateContext (mDisplay, eglConfig, EGL_NO_CONTEXT, contextAttribs);
if (EGL_NO_CONTEXT == mContext) {
printf("failed to get context...\n");
}
// Bind the Display, Surface and Contexts together
eglMakeCurrent(mDisplay, mSurface, mSurface, mContext);
// Setup the viewport
glViewport(0, 0, mWindow->getWidth(), mWindow->getHeight());
}
void GLES2Context::shutdown()
{
eglDestroyContext(mDisplay, mContext);
eglDestroySurface(mDisplay, mSurface);
eglTerminate(mDisplay);
}
void GLES2Context::refresh()
{
// Trigger a buffer swap
eglSwapBuffers(mDisplay, mSurface);
// Clear the buffers for the next frame.
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void GLES2Context::bindToWindow(Window* const window)
{
// Rememeber the Window we're bound to
mWindow = reinterpret_cast<X11Window*>(window);
// Set the context as us.
mWindow->setContext(this);
}
#endif
A Simple Test
Of course, you probably want to see this in action!
It's a bit anti-climatic, but it works at least.. so let's create a little test program ( this is WindowContextTest in the SVN )
main.cpp
#include <cstdlib>
#if defined(LINUX)
#include "../../Graphics/Window/X11Window.h"
#endif
#if defined(GLX)
#include "../../Graphics/Context/GLXContext.h"
#elif defined(GLES1)
#include "../../Graphics/Context/GLES1Context.h"
#elif defined(GLES2)
#include "../../Graphics/Context/GLES2Context.h"
#endif
using namespace GLESGAE;
int main(void)
{
#if defined(GLX)
GLXContext* context(new GLXContext);
#elif defined(GLES1)
GLES1Context* context(new GLES1Context);
#elif defined(GLES2)
GLES2Context* context(new GLES2Context);
#endif
#if defined(LINUX)
X11Window* window(new X11Window);
#endif
context->bindToWindow(window);
window->open(800, 480);
context->initialise();
window->refresh();
while(1)
window->refresh();
}
It's not that scary honest! Most of it is just dealing with what version of Window and RenderContext to pull in.
As said, the SVN has a GLXContext for Linux as well... WinAPI support will be added at some point as well.
So, logic wise, we create a Window and a RenderContext.
We then bind the Window to the Context, then open said Window with a specified Width and Height ( 800 x 480 in this case. )
Then we initialise the Context.
Finally, we go into an infinite loop, refreshing the Window ( and in turn, the Context bound to it. )
Simple!
To stop it, just close the Window.. as we're not catching events yet, this will cause it to crash.
Building the Example
In the SVN there are Makefiles already setup for you.. just trigger make -f MakefileES1.pandora or whatever your chosen configuration is, and it'll happily build for you and spit out a GLESGAE.pandora binary for you to run.
Alternatively, if you use CodeLite, there's a Workspace/Project set for you preconfigured.
Gotcha I had an issue about not having -lgcc_s ... I did have /lib/libgcc_s.so.1 so all I had to do was sudo ln -s /lib/libgcc_s.so.1 /lib/libgcc_s.so and it was happy again.
Next Time
We'll be looking at an Event Queue next time.. dealing with the Window Events and some of those lovely Pandora controls!
Then after that, we shall setup the Renderers and get some stuff on screen.