This is the first of what's going to be a tutorial series for developing software with Warp3D Nova. By the time you've completed this tutorial, you'll have a basic Warp3D Nova app running. It'll have a resizable (albeit blank) window, a Warp3D Nova context, and a basic event loop for processing user input and redrawing the display. These are the foundations on which we'll build more interesting apps in future tutorials.

Warp3D Nova?

Warp3D Nova is a modern shader-based 2D/3D graphics system for AmigaOS 4.x that has just been released (April 2016). It's purpose is to enable app/game developers use the power that a modern Graphics Processing Unit (GPU) has to offer, and render impressive 2D/3D graphics. The package comes with everything that developers need, including comprehensive documentation and example code.

What You'll Need

  • AmigaOS 4.1+ (plus the necessary hardware to run it)
  • The AmigaOS 4.x SDK
  • Warp3D Nova (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 libraries that the app will need. It needs 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 <Warp3DNova/Warp3DNova.h>

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

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

While it's possible to get the compiler to automatically open & close the graphics and intuition libraries, we're going to open them manually. This allows us to have complete control over versions. So, 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 *Warp3DNovaBase = NULL;
struct Warp3DNovaIFace *IW3DNova = NULL;

And, we'll also specify which versions to use:

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

Now, let's write the code to open the libraries:

/** 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
 */
static 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);
	if(!IntuitionBase) { 
		closeLibs();
		return FALSE; 
	}
	GfxBase = openLib("graphics.library", MIN_GRAPHICS_VERSION, 
		(struct Interface**)&IGraphics);
	if(!GfxBase) { 
		closeLibs();
		return FALSE; 
	}
	Warp3DNovaBase = openLib("Warp3DNova.library", MIN_W3DNOVA_VERSION, 
		(struct Interface**)&IW3DNova);
	if(!Warp3DNovaBase) { 
		closeLibs();
		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.

Once the program is done, the libraries also need to be closed:

/** Closes all libraries.
 */
static void closeLibs() {
	if(IW3DNova) {
		IExec->DropInterface((struct Interface*)IW3DNova);
		IW3DNova = NULL;
	}
	if(Warp3DNovaBase) {
		IExec->CloseLibrary(Warp3DNovaBase);
		Warp3DNovaBase = 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 Render Context

With the libraries opened and ready to go, the next task is to open a window and set up a Warp3D Nova render context. To make life easier later, lets create a RenderContext structure to encapsulate everything needed to handle the window and basic rendering:

/** The display context.
 */
typedef struct RenderContext_s {
	// The window 
	struct Window *window;
	
	// The Warp3D Nova context
	W3DN_Context *context;
	
	// The "back-buffer." We render to this.
	struct BitMap *backBuffer;
	
	// The display's width in pixels
	int32 width;
	
	// The display's height in pixels
	int32 height;
} RenderContext;

Rather than rendering to the window directly, we render to an off-screen bitmap (the backBuffer in the RenderContext structure above) using the Warp3D Nova context, and then copy it to the window once we're done. Rendering directly to the window risks having partially rendered scenes displayed to the user, which would be rather ugly.

Creating the Context

The first step to getting this RenderContext working, is to write a function that creates a new context:

/** Creates a render context. 
 * This opens a window and creates a corresponding Warp3D Nova 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_NEWSIZE | 
		                      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
	W3DN_ErrorCode errCode;
	renderContext->context = IW3DNova->W3DN_CreateContextTags(
		&errCode, W3DNTag_Screen, NULL, TAG_DONE);
	if(!renderContext->context) {
		rcDestroy(renderContext);
		showErrorRequester("Couldn't create a Warp3D Nova context for\n"
			"the Workbench (or default public) screen.\n"
			"Error %u: %s", errCode, 
			IW3DNova->W3DN_GetErrorString(errCode));
		return NULL;
	}
	
	// Create the "back-buffer" that we'll be rendering to
	if(!rcDoResize(renderContext)) {
		rcDestroy(renderContext);
		// NOTE: Error message already displayed
		return NULL;
	}
	
	return renderContext;
}

Let's go through the most important parts of this function. The OpenWindowTags() call creates a window on the Workbench screen (or the default public screen). In its tag-list there are IDCMP flags:

IDCMP_REFRESHWINDOW | IDCMP_NEWSIZE | IDCMP_CLOSEWINDOW

These flags indicate that our application wants to receive events when the window needs to be refreshed, the window's size changes, or the window's close gadget is clicked. If you wish to listen for mouse and/or keyboard events, then you should add the appropriate flags here.

You may also have spotted the following tag at the end of the OpenWindowTags() call:

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

By default, a window's background is cleared to gray (or some other colour). We don't want this because our app will be filling the entire window with its own imagery. The line above disables the default back-fill.

With the window created, it's time to create a Warp3D Nova context. This task is performed by the following line:

	renderContext->context = IW3DNova->W3DN_CreateContextTags(
		&errCode, W3DNTag_Screen, NULL, TAG_DONE);

This creates a context that can render to the Workbench/default-public screen. If no suitable driver can be found, then this will fail and return a suitable error code.

IMPORTANT: If you open a window on a different screen, then the W3DNTag_Screen tag's should have a pointer to that screen rather than NULL.

The final step in creating the RenderContext, is to create the back-buffer. This task is delegated to the rcDoResize() function in the following line:

	if(!rcDoResize(renderContext)) {

RcDoResize()'s main task is to reallocate the back buffer when the window is enlarged. Using it to allocate the initial back-buffer as well, avoids having duplicate code.

NOTE: The rest of the code in the function, is error-handling code. I'm skipping over it for this tutorial, but checking for errors is critical to making reliable code.

Resizing the Context

Now that the render context is ready, I bet that you're itching to start drawing stuff. Not so fast; rcCreate() depends on rcDoResize(), remember? This function is also needed when the user resizes the window. It resizes the back-buffer if necessary, and will also adjust the Warp3D Nova viewport (in future tutorials; we don't need to worry about the viewport just yet). So, here's the resize function:

/** Resizes a render context's back buffer to match the window size.
 *
 * NOTE: Displays an error message if failed
 *
 * @param renderContext pointer to the render context
 *
 * @return struct BitMap* pointer to the back-buffer if successful, and NULL if
 * failed.
 */
static struct BitMap* rcDoResize(RenderContext *renderContext) {
	// Get the window's current dimensions
	if(IIntuition->GetWindowAttrs(renderContext->window, 
			WA_InnerWidth, &renderContext->width, 
			WA_InnerHeight, &renderContext->height, TAG_DONE) != 0) {
		showErrorRequester("Couldn't get the window's dimensions");
		return NULL;
	}

	// Resize the back-buffer if necessary
	// NOTE: BMA_ACTUALWIDTH gets the width that we allocated rather than the
	// padded width.
	if(!renderContext->backBuffer || 
			IGraphics->GetBitMapAttr(renderContext->backBuffer,
				BMA_ACTUALWIDTH) < renderContext->width ||
			IGraphics->GetBitMapAttr(renderContext->backBuffer,
				BMA_HEIGHT) < renderContext->height) {
		IGraphics->FreeBitMap(renderContext->backBuffer);
		renderContext->backBuffer = IGraphics->AllocBitMapTags(
			renderContext->width, renderContext->height, 0, 
			BMATags_Friend, renderContext->window->RPort->BitMap,
			BMATags_Displayable, TRUE, TAG_DONE);
		if(!renderContext->backBuffer) {
			showErrorRequester("Couldn't allocate the back-buffer");
			return NULL;
		}
		
		// Bind the new back-buffer to the default Render State Object (RSO)
		renderContext->context->FBBindBufferTags(NULL, 
			W3DN_FB_COLOUR_BUFFER_0, W3DNTag_BitMap, 
			renderContext->backBuffer, TAG_DONE);
	}
	
	// Success
	return renderContext->backBuffer;
}

 Lets break this function down:

  • First, it extracts the window's inner dimensions using GetWindowAttrs()
  • Next, it checks if backBuffer exists, and is large enough
    if(!renderContext->backBuffer || 
    		IGraphics->GetBitMapAttr(renderContext->backBuffer,
    			BMA_ACTUALWIDTH) < renderContext->width ||
    		IGraphics->GetBitMapAttr(renderContext->backBuffer,
    			BMA_HEIGHT) < renderContext->height) {
    NOTE: BMA_ACTUALWIDTH is used rather than BMA_WIDTH, because that's what Warp3D Nova uses to determine the bitmap's actual dimensions
  • If not, then it is reallocated with FreeBitMap() and AllocBitMapTags()
  • Finally, the new bitmap needs to be bound to the Warp3D Nova context, so that we can draw to it. This is done using FBBindBufferTags(), and the back buffer is bound to the main colour buffer (W3DN_FB_COLOUR_BUFFER_0)

Displaying the Rendered Image (Swapping the Buffers)

We're almost ready to render something. First, though, let's implement the code that will copy the final rendered image from backBuffer to the window:

/** Swaps the display buffers so that the rendered image is displayed.
 * NOTE: This will flush the render context before performing the swap.
 *
 * @param renderContext pointer to the context to destroy
 */
static void rcSwapBuffers(RenderContext *renderContext) {
	W3DN_ErrorCode errCode;
	W3DN_Context *context = renderContext->context;
	struct Window *window = renderContext->window;
	struct BitMap *backBuffer = renderContext->backBuffer;
	
	// First flush the Warp3D Nova render pipeline, so that everything gets drawn
	uint32 submitID = context->Submit(&errCode);
	if(!submitID) {
		// This should never happen...
		printf("context->Submit() failed (%u): %s\n", 
			errCode, IW3DNova->W3DN_GetErrorString(errCode));
		return;
	}
				
	// Blit to the window
	// NOTE: GPU operations occur in order, so we can safely do a blit without
	// performing a WaitIdle()/WaitDone() first
	uint32 winWidth = window->Width - (window->BorderLeft + window->BorderRight);
	uint32 winHeight = window->Height - (window->BorderTop + window->BorderBottom);
	renderContext->width = MIN(renderContext->width, winWidth);
	renderContext->height = MIN(renderContext->height, winHeight);
	IGraphics->BltBitMapRastPort(backBuffer, 0, 0, window->RPort, 
				window->BorderLeft, window->BorderTop, 
				renderContext->width, renderContext->height, 0xC0);
}

This is a two step process. First, the Warp3D Nova context's pipeline needs to be flushed. Warp3D Nova submits commands to the GPU in batches for efficiency. Any queued commands need to be sent to the GPU now, or our rendered image will be incomplete:

uint32 submitID = context->Submit(&errCode);

With that done, we can copy the result to the window:

uint32 winWidth = window->Width - (window->BorderLeft + window->BorderRight);
uint32 winHeight = window->Height - (window->BorderTop + window->BorderBottom);
renderContext->width = MIN(renderContext->width, winWidth);
renderContext->height = MIN(renderContext->height, winHeight);
IGraphics->BltBitMapRastPort(backBuffer, 0, 0, window->RPort, 
			window->BorderLeft, window->BorderTop, 
			renderContext->width, renderContext->height, 0xC0);

NOTE: The function is called rcSwapBuffers(), because we're effectively using double-buffering. In full-screen double-buffering, the "front-buffer" and "back-buffer" are swapped, so that the back buffer is displayed. In windowed mode, the window itself is the front-buffer.

Destroying the Context

There's one more RenderContext function to write: the function that destroys it and cleans up all resources.

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

The Main Loop

Right! Let's put everything together and build a working app. It's taken a while to get to this point, but the advantage is that the app's main function will be clean and compact.

Setup

First, the libraries need to be opened:

int main(int argc, const char **argv) {
	RenderContext *renderContext = NULL;
	BOOL quit = FALSE;
	BOOL refreshWindow = FALSE;
	BOOL resizeWindow = FALSE;
	
	// -- Set up --
	// Set up the callback for cleanup on exit
	atexit(closeLibs);
	
	// 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;
	}

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...
			W3DN_Context *context = renderContext->context;
			context->Clear(NULL, opaqueBlack, NULL, NULL);
			
			// Swap the buffers, so that our newly rendered image is displayed
			rcSwapBuffers(renderContext);

			// All done
			refreshWindow = FALSE;
		}

I've kept the draw code incredibly simple; all it does is clear the screen to black:

context->Clear(NULL, opaqueBlack, NULL, NULL);

NOTE: OpaqueBlack is defined as a constant at the top of the source file as (we'll likely be reusing it in multiple places):

const float opaqueBlack[4] = {0.0f, 0.0f, 0.0f, 1.0f};

With our gloriously empty scene rendered, it's time to "swap" the back-buffer to the front, so that it's displayed:

rcSwapBuffers(renderContext);

Now that the window has been updated, the render code doesn't need to be executed again until something changes. So, 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_NEWSIZE:
				// A resize occurred, need to refresh the window
				resizeWindow = TRUE;
				break;
			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 was resized, then resizeWindow is set to TRUE; 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.

The final bit of code in the main loop responds to the window resize event:

		if(resizeWindow) {
			// Adjust the render context for the resize
			rcDoResize(renderContext);
			
			// Now trigger a refresh
			refreshWindow = TRUE;
			
			// All done
			resizeWindow = FALSE;
		}

As you can see in the code above, a window resize also triggers a refresh (refreshWindow = TRUE). Any change in size means that the window needs to be redrawn.

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

That's it! We have a basic working Warp3D Nova app. It has a window, an event loop, and renders a blank screen.
NOTE: You can download the entire code below.

W3DNova Tutorial1 ScreenshotMost of the code in this tutorial is generic AmigaOS code that could easily be reused in non Warp3D Nova projects. If you're used to OpenGL and GLUT, then this is the kind of low-level code that's hidden within the GLUT library.

The next tutorial will build on this one, and we'll render something more interesting than a black screen.

Download the complete code for this tutorial: W3DNovaTutorial1.lha