This tutorial will get you started with OpenGL ES 2+ (GLES2) on AmigaOS. It'll show you how to set up a window and render to it. The set up will be done using AmigaOS APIs directly (no GLUT, SDL or other cross-platform library).

OpenGL (ES) 2+ is a popular cross-platform shader based graphics API. It's used extensively on mobile devices and is also what WebGL is based on. If you're interested in graphics/games programming and want to write software that'll run on more than just AmigaOS, then this is for you.

What You'll Need

  • AmigaOS 4.1+ (plus the necessary hardware to run it)
  • The AmigaOS 4.x SDK
  • The OpenGL ES 2 library (can be obtained via the AmiStore as part of the Enhancer Software Pack)
  • Basic C programming knowledge

Some graphics programming knowledge would be helpful. I also highly recommend using CodeBench as code editor.

Libraries & Headers

The first step is to open the following: Warp3DNova.library, graphics.library, and intuition.library. We're also going to be using a few other bits and pieces, so here's the set of header files:

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>

#include <classes/requester.h>

#include <proto/exec.h>
#include <proto/graphics.h>
#include <proto/intuition.h>
#include <proto/requester.h>

#include <proto/ogles2.h>
#include <inline4/ogles2.h>

NOTE: The OpenGL ES 2 wrapper currently has non-standard headers. This will be fixed in a future version at which point you'll be able to use the standard headers (e.g., GLES2/gl2.h). For now, use the headers listed above.

IMPORTANT: Don't worry about understanding every detail just yet, things will become clearer the more you practise.

Now let's set up the library & interface pointers:

// Library pointers
struct Library *GfxBase = NULL;
struct GraphicsIFace *IGraphics = NULL;
struct Library *IntuitionBase = NULL;
struct IntuitionIFace *IIntution = NULL;
struct Library *OGLES2Base = NULL;
struct OGLES2IFace *IOGLES2 = NULL;

We need to specify minimum library versions to ensure certain features are available:

// Minimum library versions
#define MIN_GRAPHICS_VERSION 54
#define MIN_INTUITION_VERSION 0 
#define MIN_OGLES2_VERSION 0

Now onto the library opening code:

/** Opens a library (and its main interface).
 * This will display an error requester if failed.
 *
 * @param libName the library's name
 * @param minVers the minimum version to open
 * @param iFacePtr pointer to where the opened interface should be written
 *
 * struct Library* pointer to the opened library, or NULL if failed
 */
struct Library* openLib(const char *libName, unsigned int minVers, struct Interface **iFacePtr) {
	struct Library *libBase = IExec->OpenLibrary(libName, minVers);
	if(libBase) {
		struct Interface *iFace = IExec->GetInterface(libBase, "main", 1, NULL);
		if(!iFace) {
			// Failed
			IExec->CloseLibrary(libBase);
			libBase = NULL; // Lets the code below know we've failed
		}
		
		if(iFacePtr) {
			// Write the interface pointer 
			*iFacePtr = iFace;
		}
	}
	if(!libBase) {
		// Opening the library failed. Show the error requester
		const char errMsgRaw[] = "Couldn't open %s version %u+.\n";
		if(!showErrorRequester(errMsgRaw, libName, minVers)) {
			// Use printf() as a backup
			printf(errMsgRaw, libName, minVers);
		}
	}
	
	return libBase;
}

/** Opens all libraries.
 */
static BOOL openLibs() {
	IntuitionBase = openLib("intuition.library", MIN_INTUITION_VERSION, (struct Interface**)&IIntuition);
	GfxBase = openLib("graphics.library", MIN_GRAPHICS_VERSION, (struct Interface**)&IGraphics);
	OGLES2Base = openLib("ogles2.library", MIN_OGLES2_VERSION, (struct Interface**)&IOGLES2);
	if(!OGLES2Base) { return FALSE; }
	
	return TRUE;
}

NOTE: The code above uses a function called showErrorRequester(). I won't cover that here because it's minor support code. This function is included in the complete code which can be downloaded at the end of this article.

The libraries also need to be closed when the program exists:

/** Closes all libraries.
 */
static void closeLibs() {
	if(IOGLES2) {
		IExec->DropInterface((struct Interface*)IOGLES2);
		IOGLES2 = NULL;
	}
	if(OGLES2Base) {
		IExec->CloseLibrary(OGLES2Base);
		OGLES2Base = NULL;
	}
	
	if(IGraphics) {
		IExec->DropInterface((struct Interface*)IGraphics);
		IGraphics = NULL;
	}
	if(GfxBase) {
		IExec->CloseLibrary(GfxBase);
		GfxBase = NULL;
	}
	
	if(IIntution) {
		IExec->DropInterface((struct Interface*)IIntution);
		IIntution = NULL;
	}
	if(IntuitionBase) {
		IExec->CloseLibrary(IntuitionBase);
		IntuitionBase = NULL;
	}
}

The Window and the OpenGL Context

With the libraries opened and ready to go, the next task is to open a window and set up an OpenGL context. We'll store these two items together in a structure to make life easier later:

/** The display context.
 */
typedef struct RenderContext_s {
	// The window 
	struct Window *window;
	
	// The GLES2 context
	void *context;
} RenderContext;

Creating the Context

Opening a window and creating the OpenGL context is as follows:

/** Creates a render context. 
 * This opens a window and creates a corresponding OpenGL context for rendering.
 *
 * NOTE: This will display error messages on failures
 *
 * @param width the desired width in pixels
 * @param height the desired height in pixels
 *
 * @return RenderContext* pointer to the new context, or NULL if failed
 */
static RenderContext* rcCreate(uint32 width, uint32 height) {
	RenderContext *renderContext = calloc(1, sizeof(RenderContext));
	if(!renderContext) {
		showErrorRequester("Out of memory when\nallocating a render context");
		return NULL;
	}
	
	// Create the window on Workbench (or the default public screen)
	renderContext->window = IIntuition->OpenWindowTags(NULL,
		WA_Title,			PROG_TITLE,
		WA_Activate,		TRUE,
		WA_RMBTrap,			TRUE,
		WA_DragBar,			TRUE,
		WA_DepthGadget,		TRUE,
		WA_SimpleRefresh,	TRUE,
		WA_SizeGadget,		TRUE,
		WA_CloseGadget,		TRUE,
		WA_IDCMP,			IDCMP_REFRESHWINDOW | IDCMP_CLOSEWINDOW,
		WA_InnerWidth,		WIDTH,
		WA_InnerHeight,		HEIGHT,
		WA_MinWidth,		MIN(WINDOW_MINWIDTH, width),
		WA_MinHeight,		MIN(WINDOW_MINHEIGHT, height),
		WA_MaxWidth,		MAX(WINDOW_MAXWIDTH, width),
		WA_MaxHeight,		MAX(WINDOW_MAXHEIGHT, height),
		WA_BackFill, 		LAYERS_NOBACKFILL, // Don't want default backfill
							TAG_DONE);
	if(!renderContext->window) {
		rcDestroy(renderContext);
		showErrorRequester("Couldn't open window.");
		return NULL;
	}
	
	// Create the context
	ULONG errCode;
	renderContext->context = aglCreateContextTags(&errCode,
		OGLES2_CCT_WINDOW, (ULONG)renderContext->window, 
		OGLES2_CCT_VSYNC, 0,
		TAG_DONE);
	if(!renderContext->context) {
		rcDestroy(renderContext);
		showErrorRequester("Couldn't create an OpenGL context for\n"
			"the Workbench (or default public) screen. Error code: %u\n", errCode);
		return NULL;
	}
	
	// Tell GLES2 to use the new context (make it current)
	aglMakeCurrent(renderContext->context);
	
	return renderContext;
}

Let's go through this step by step. OpenWindowTags() creates a window on the Workbench screen (or the default public screen). In its tag-list there are IDCMP flags:

IDCMP_REFRESHWINDOW | IDCMP_CLOSEWINDOW

These flags indicate that our application wants to receive events when the window needs to be refreshed or the window's close gadget is clicked. We're not listening to IDCMP_NEWSIZE events, because the GLES2 library automatically handles this for us. You should add flags for any other events you may wish to respond to (e.g., mouse and keyboard input).

Here's another tag worth paying attention to:

WA_BackFill,      LAYERS_NOBACKFILL, // Don't want default backfill

No backfill is needed because we'll be rendering to the entire window, so the line above disables it.

The next task is creating the window's OpenGL context:

// Create the context
ULONG errCode;
renderContext->context = aglCreateContextTags(&errCode,
	OGLES2_CCT_WINDOW, (ULONG)renderContext->window, 
	OGLES2_CCT_VSYNC, 0,
	TAG_DONE);

Finally, the OpenGL context needs to be made current so that all gl*() calls will render to the window:

// Tell GLES2 to use the new context (make it current)
aglMakeCurrent(renderContext->context);

Detroying the Context

The context also needs to be destroyed when the program ends. Here's the function that performs this:

/** Destroys a render context.
 *
 * @param renderContext pointer to the context to destroy
 */
static void rcDestroy(RenderContext *renderContext) {
	if(renderContext) {
		if(renderContext->context) {
			aglDestroyContext(renderContext->context);
			renderContext->context = NULL;
		}
		if(renderContext->window) {
			IIntuition->CloseWindow(renderContext->window);
			renderContext->window = NULL;
		}
		
		free(renderContext);
	}
}

Displaying the Rendered Image (Swapping the Buffers)

Rendering directly to the window is a bad idea because the user could easily end up with partially rendered graphics. So, we render everything to a back buffer and "swap" the front and back buffers to make it visible. This is pretty easy to do:

/** Swaps the display buffers so that the rendered image is displayed.
 * NOTE: This will flush the render context before performing the swap.
 */
static void rcSwapBuffers() {
	// First flush the render pipeline, so that everything gets drawn
	glFinish();
	
	// Swap the buffers (if any)
	aglSwapBuffers();
}

Calling glFinish() before swapping the buffers ensures that everything gets drawn. OpenGL queues multiple draw operations together for efficiency, and calling glFinish() ensures that they're all submitted and completed before the buffer swap occurs.

The Main Loop

The code above handles the set up, so let's put everything together and build a working program!

Setup

First, the libraries need to be opened:

int main(int argc, const char **argv) {
	RenderContext *renderContext = NULL;
	BOOL quit = FALSE;
	BOOL refreshWindow = FALSE;
	
	// -- Set up --
	// Set up the callback for cleanup on exit
	atexit(cleanup);
	
	// Open the libraries
	if(!openLibs()) {
		// Error messages already displayed...
		return 5;
	}

NOTE: The atexit() function ensures that closeLibs() gets called when the program ends.

Next, we create the RenderContext. Thanks to our hard work earlier, this is really easy:

	// Create the render context
	renderContext = rcCreate(WIDTH, HEIGHT);
	if(!renderContext) {
		return 10;
	}

With this done it's time to set the initial OpenGL state. We won't be rendering any objects in this tutorial, so for now just set the clear colour:

	// Set the clear colour
	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

The Actual Main Loop

The main loop is the core of the program. Its responsible for receiving and responding to any events, and that includes redrawing the window.

The window is still empty when the main loop is first entered, so the first task is to render our scene:

	// -- Main Loop --
	refreshWindow = TRUE; // Want to perform an initial render
	
	while(!quit) {
		// Putting this here so that we can perform an initial render without needing to wait
		// for a message to come in
		if(refreshWindow) {
			// Render our scene...
			glClear(GL_COLOR_BUFFER_BIT);
			
			// Swap the buffers, so that our newly rendered image is displayed
			rcSwapBuffers();

			// All done
			refreshWindow = FALSE;
		}

I've deliberately kept the rendering code simple because we're focusing on window & context setup and handling. The glClear() call clears the screen to the colour set previously with glClearColor(). Once the scene is rendered, rcSwapBuffers() fliips the buffers and displays the new image. Finally, refreshWindow is set to FALSE. Redrawing the display unnecessarily is just a waste.

At this point the app doesn't have to do anything until there's an event to respond to. So, it waits for the IDCMP events that were activated with OpenWindowTags():

		// Wait for an event to happen that we need to respond to
		struct Window *window = renderContext->window;
		IExec->WaitPort(window->UserPort);
		struct IntuiMessage *msg;
		while ((msg = (struct IntuiMessage *) IExec->GetMsg (window->UserPort))) {
			switch (msg->Class)
			{
			case IDCMP_REFRESHWINDOW:
				refreshWindow = TRUE;
				break;
			case IDCMP_CLOSEWINDOW:
				// The user says quit!
				quit = TRUE;
				break;
			default:
				; // Do nothing
			}
			IExec->ReplyMsg((struct Message *) msg);
		}
	}

The code above sets flags in response to events; the code that performs each task is elsewhere (which makes the code easier to follow). If the window needs to be refreshed, then refreshWindow is set; and, if the close window gadget is clicked, then quit is set to TRUE, and the program exits the main loop.

Cleanup

When the user clicks on the window's close button, then it's time to destroy the render context, and shut down. Once again, the hard work is already done in rcDestroy(), reducing the cleanup code to:

	// -- Cleanup --
	if(renderContext) {
		rcDestroy(renderContext);
		renderContext = NULL;
	}

	return 0;
}

Conclusion

All done! We have a basic working OpenGL ES 2 app running on AmigaOS. It has a window, an event loop, and renders a blank screen.
NOTE: You can download the entire code below.

GLTutorial1 screenshot

Most of the code is generic AmigaOS code that could be used for non-OpenGL apps. In fact, the code itself was taken and adapted from a similar Warp3D Nova "getting started" tutorial.

Next time we'll render something more interesting than a black screen.

Download the complete code for this tutorial: GLTutorial1.lha