This tutorial explains how to create a barebones GLERI application that draws something in a window and quits. Adhering to tradition, we'll print "Hello world!".

Building

The code for this tutorial is included in the GLERI tarball, located under the tut/hello subdirectory. When you build GLERI, the tutorials are automatically build as well. In your own makefile provide include paths to the location of gleri.h and the libgleri.a library. By default, these are installed to /usr/include and /usr/lib respectively. The project executable needs to be linked with libgleri.a.

To run the project, the gleris service must either be running, with UNIX socket in XDG_RUNTIME_DIR, TCP socket 6540, or be in PATH to be launched in single-client mode. The gleris service is the part of the system that will actually use Xlib and OpenGL to create windows and draw on them. Each GLERI client application will connect to a gleris process through a socket and send commands for window creation and drawing.

The Application Object

GLERI uses an object-oriented framework, with the root object being a singleton application object. All other objects are created and owned by this application object. This design is the easiest way to ensure that all objects get correctly destroyed in case of program termination. The application object will install signal handlers and will, in case of crashes, exceptions, C++ terminate, etc., clean up everything it owns on exit. Each program's application object must be derived from CGLApp, and must provide a singleton interface in form of the static Instance() call and a private constructor.

#include <gleri.h>

class CHello : public CGLApp {
    CHello (void) : CGLApp() {}
public:
    static CHello& Instance (void) {
	static CHello s_App; return (s_App);
    }
    void Init (argc_t argc, argv_t argv) {
	CGLApp::Init (argc, argv);
	CreateWindow<CHelloWindow>();
    }
};

GLERI_APP (CHello)

Here you see an empty private default constructor, simply initializing the base class. Then there is the singleton Instance call, that creates the application object. Using a local static variable registers the destructor via atexit, ensuring correct cleanup whenever the process exits. The Init function is called directly from main with process arguments. Here the only action is to create the hello window, with class CHelloWindow defined below. All windows must be created this way. CreateWindow will return a pointer to the created window. Window objects are always owned by the application object, so this pointer should not be deleted. In this tutorial, we do not need to access the window directly, so the pointer is ignored.

At the end, there must be exactly one line containing GLERI_APP, a macro to generate a main() for the application using the provided application class.

The Window Object

class CHelloWindow : public CWindow {
public:
                  CHelloWindow (iid_t wid) : CWindow(wid) {}
    virtual void  OnInit (void);
    ONDRAWDECL    OnDraw (Drw& drw) const;
    virtual void  OnKey (key_t key);
};

Window classes given to CGLApp::CreateWindow must be derived from the CWindow class. CWindow encapsulates all window-related operations, and is defined in gleri/window.h.

Each window class must define at least three functions. First is the constructor, taking one argument that is the window instance id. Second is the OnInit override, called after the connection to gleris has been established. Third is the OnDraw call that creates the drawlist for the window. You will also likely want to override one of the event handlers, like OnKey here.

An important design requirement that must be mentioned here is that resource creation and drawing must be done separately. There is a variety of complicated technical reasons for this limitation that more or less boil down to "it would be difficult and inefficient to implement it otherwise". From the library user's perspective this means that you can only use drawing commands in OnDraw - the ones that are implemented by the passed in drw object. Buffers and textures have to be created and modified elsewhere, typically in OnInit, OnResize, OnKey, OnTimer, or wherever else the actual changes are triggered. You may recognize this as a form of document-view architecture.

Another design point that must be mentioned here is that OnDraw is a template. To write a drawlist into a network packet you can do one of two things: put code in every drawing command to check if there is enough space in the buffer and resize as needed, or calculate the size of the drawlist first, allocate the buffer to the full size, and then write the data. GLERI uses the second approach, because it is considerably more efficient. The drawback is that OnDraw must be called twice - first to measure, then again to write. Because OnDraw is const and does not create or destroy anything, the size of the drawlist is typically very simple to calculate, and the first call becomes either a simple constant (as it will in this tutorial), or a very simple calculation. To make the template boilerplate code creation a little easier, two macros, ONDRAWDECL and ONDRAWIMPL are provided for declaring and defining OnDraw.

void CHelloWindow::OnInit (void)
{
    CWindow::OnInit();
    Open ("Hello World", 320, 240);
}

Somewhere in OnInit, each window object must call Open (see PRGL::Open in gleri/rglp.h) to tell gleris to create a window on the screen. Here, the first argument is the window title, followed by dimensions. A second Open variant is available for windows that need to specify more parameters. The second argument to that one is a G::WinInfo structure. After the Open call you can create resources, such as vertex buffers or textures, and call Draw(). This tutorial does not draw any primitives, and so does not need any resources.

ONDRAWIMPL(CHelloWindow)::OnDraw (Drw& drw) const
{
    CWindow::OnDraw (drw);
    drw.Clear (RGB(0,0,64));
    drw.Color (RGB(128,128,128));
    static const char c_HelloMessage[] = "Hello world!";
    drw.Text ((Info().w-Font()->Width(c_HelloMessage))/2,
              (Info().h-Font()->Height())/2,
	      c_HelloMessage);
}

The drawing function is defined with the help of the ONDRAWIMPL macro taking the class name. The drw parameter a templated type PDraw, defined in gleri/drawp.h, implementing the commands for drawlist creation. This drawlist consists of clearing the screen with a blue background, setting a gray drawing color, and printing "Hello world!" in the middle of the window.

For colors, RGBA function is also available for drawing transparent primitives. On the gleris side, the color becomes shader parameter "Color" for the default shader. In OpenGL 3.3, the fixed pipeline is gone and all applications must write their own shaders. For basic 2D stuff that can be rather cumbersome, so gleris implements two default shaders for 2D. The first one, enabled by default is the G::default_FlatShader, will draw flat-shaded primitives with the specified color. The second one, G::default_GradientShader, will draw smoothly shaded primitives and takes a per-vertex color value.

Although PDraw does provide this default 2D functionality, it most definitely is possible to draw in 3D. You'll just have to write your own shaders and transform matrices, just as you would when using plain OpenGL, but with the drw. prefix instead of gl. OpenGL programming is a pretty big subject and is outside the scope of this tutorial. Please consult the excellent tutorials available on www.opengl-tutorial.org. There are many more available around the net; just make sure you're looking at the ones targeting the GL3.3 core profile version. The API for the versions prior to 3.3 is very different and the its programming style is not supported in GLERI.

The Font call returns a G::Font::Info structure (defined in gleri/gldefs.h) for the default font, containing font metrics for measuring text as shown. Passing a font id to Font would return the info structure for the specified font. The default font, along with the default shaders, is available from the start and is shared among all applications connected to one gleris process. Resources created by each application are automatically shared among all windows created by a single connection. For example, if hello were to create two windows and load a buffer in one, the buffer could also be accessed from the other, but would not be available to other processes using the same gleris.

The Info call returns the window's G::WinInfo structure (defined in gleri/gldefs.h) that contains current window state. The info structure is updated every time the window receives a resize event, which happens at least once after the window is mapped to the screen.

void CHelloWindow::OnKey (key_t key)
{
    CWindow::OnKey (key);
    if (key == Key::Escape || key == 'q')
	Close();
}

Finally, the OnKey handler allows quitting the program. Here, Close is called to close this window. Because after it is closed there will be no windows left, the application object will automatically quit. You can also explicitly call CGLApp::Instance().Quit() to quit even when there are other windows currently open.

Key::Escape is defined in gleri/event.h, where you can find the other supported key codes. Printable characters are passed in as they are. Modifier keys are ORed onto the key value. For example, when Ctrl and q are pressed, you'll get KMod::Ctrl|'q'.

So now you know how to create a GLERI application, how to draw a text, and how to quit. This is enough to write a WordPerfect 5.1 clone.