Amiga Demo, Part 4

April 20, 2019

The fourth part of this blog series about the development of our demo. This part if focused on the GOA library and the Amiga implementation of the library.

#Introduction The GOA library was developed in 1998-2001, with the bulk written in 1999 and then refreshed in 2001 were parts got refactored to the better, making the library more flexible. Since 2003 the library has in practice been froozen. Personally I’ve used it as a “side-library” when doing other things. It’s nice, very portable and pretty well tested/documented (i.e. easy to read).

#GOA Layout The library consists of two main modules

  • GOA Core - responsible for OS interfacing stuff and providing the essentials of a graphics engine
  • GOA Extras - all nice-not-needed stuff

##GOA Core Library

GOA Core

The following APIs are provided

  • GFX API, with underlying driver structure (red/orange)
    • Display
    • Mouse/Keyboard API
    • OS Integration
  • I/O API, with underlying driver structure (green)
    • STDIO is provided by default (a wrapper around fopen/fclose and friends)
  • Timers API with static driver structure (yellow)
  • Pixmap, frambuffer handling for 8bit and 32bit (blue)
  • Intermediate 3D Pipline (blue)
    • Frustum and Clipping
    • Lighting
    • Materials and Textures
    • Transformation

Not in the picture:

  • Core containers (array, hash, list, frameheap)
  • Memory allocation wrapping (for tracking stats) and memset/memcpy

Besides the GFX part of Core I really like the I/O API. Specifically when dealing with binary streams. The API provides a way to do read specific data types and to do automatic little/big endinan conversion. This proved VERY useful when implementing assets loading in the demo as the M68k (big endian) is different from x86 (little endian).

##Example of usage

int main(int argc, char **args) {
    // Select driver depending on which platform we are compiling for
#ifdef AMIGA
	if (!goa_gfx_setdriver("agac2p8bpl")) {
		printf("Failed to set driver, err: %s\n",goa_gfx_geterror());
		return;
	}
#else
	if (!goa_gfx_setdriver("glfw")) {
		printf("Failed to set driver, err: %s\n",goa_gfx_geterror());
		return 0;
	}
#endif
    // Open screen
	if (!goa_gfx_open(320,256,"test", GOA_GFXOPEN_8BIT)) {
		printf("Failed to open screen, err: %s\n", goa_gfx_geterror());
		return;
	}
    // Create the backbuffer
	GOA_PIXMAP8 *myBackbuffer = goa_pixmap_create_8bit(320,256, GOA_PIXMAPCREATE_CLEAR);

	while(!goa_key_ispressed(GOA_KEY_ESC)) {
        // Do effect here in backbuffer
        // ...
        // Send backbuffer to screen
		goa_gfx_update(0, 0, myBackbuffer, 0,0,320,256);
    }
    // Close display
   	goa_gfx_close();
}

The framework is pretty straight forward and doesn’t really annoy you. There are a few things to keep in mind when setting it up, especially if you want to support multiple platforms and have interchangeable I/O. But besides that it really just stays out of your way and does what you expect it to do.

GOA Extras Library

While the core library has been very stable since the freeze in 2003 the extras library is more in a state of flux.

  • 3d party wrappers (jpeg/png)
  • File formats loading (tga, pcx, etc..)
  • Keyframe handling
  • LZ Packing
  • … and a lot of other things …

Basically, the extras library is “pick and choose” what you want to include. Nothing in CORE is allowed to depend on the EXTRAS but EXTRAS can (and should) use CORE whenever possible.

Also the EXTRAS is sometimes used as a staging area before promoted to Core.

#Amiga Driver The Amiga Graphics Driver is perhaps the most interesting part in the library for the ‘Dark Goat Rises’ demo. While most demos use a different model where the C runtime startup is replaced with the demo-startup and instead of calling main (as a regular program) they call something like demo_main(). There are some nice properties with this. However for our model that doesn’t work too well. I rather enjoy having access to the console to print values and have the basic OS intact until I decide not to. You can ofcourse achieve this anyway - but I just don’t see the point. We initate the Amiga Display context as a regular function: goa_gfx_open. The end result of this call is down to the agac2p8bpl driver and the Amiga OS core assembler functions.

goa_gfx_open is a very thin function and basically just pass through.

int GOAAPI goa_gfx_open(int width, int height, char *appname, int flags)
{
    if (driver == NULL) { error = GOA_GFX_ERR_NODRV; return FALSE; }
	return driver->open(width, height, appname, flags);
}

AGAc2p8bpl driver

The aga driver implements the GOA Interface, and it looks exactly like this:

GOA_GFXDRV agaDRV =
{
	"agac2p8bpl",
	"Amiga AGA driver for 8 bpl (256 colors) / v0.1 / Gnilk of Noice",
	aga_open,
	aga_close,
	aga_update,
	aga_cntl,
	aga_enumerate,
	aga_geterror,
	aga_key_ispressed,
	aga_key_get,
	aga_key_flush,
	aga_mouse_x,
	aga_mouse_y,
	aga_mouse_xmickey,
	aga_mouse_ymickey,
	aga_mouse_button,
	aga_joystick_x,
	aga_joystick_y,
	aga_joystick_button
};

When development started we locked the resolution to 320x256 physical and 320x180 (16:9) for the effects. In order to avoid any problems the driver will simply discard any other request.

int aga_open(int width, int height, char *appname, int flags)
{
	// Amiga AGA is a bit more limited then others - and we simply don't support much
	if (width != 320) {
		error = AGA_ERR_INITSYSTEM;
		return FALSE;
	}
	if (height > 256) {
		error = AGA_ERR_INITSYSTEM;
		return FALSE;
	}

	ptrActiveBackBuffer_a = alignPtr(backbuffer_a);
	ptrActiveBackBuffer_b = alignPtr(backbuffer_b);

	// Sanity if this is in chip mem..
	if (0x1FFFFF < (DWORD)ptrActiveBackBuffer_a) {
		printf("[GOA] AGA Warninig: Bitplane's not in CHIP mem!\n");
	}
	if (0x1FFFFF < (DWORD)ptrActiveBackBuffer_b) {
		printf("[GOA] AGA Warninig: Bitplane's not in CHIP mem!\n");
	}

	// Write to our copper
	amiga_set_bitplanes(BPL_COUNT-1,ptrActiveBackBuffer_a,BPL_WIDTH*BPL_HEIGHT/8);	

	// Initialize C2P
	c2p1x1_8_c5_040_init(width,height,0,0, width/8, height*width/8, width);

	// Take control over OS
	if (!amiga_init_system()) {
		error = AGA_ERR_INITSYSTEM;
		return FALSE;
	}

	win_width = width;
	win_height = height;

	// Set copper..
	amiga_set_copper();

	// Wait for one vertical blank, this ensures our copper is active
	amiga_wait_vsync();

	return TRUE;
}

Any amiga_xxx function call is a reference to the assembler implementation.

The driver implements a dual double buffer. This allows swapping a buffer directly after VSYNC without the C2P delay. You can control this behaviour if you want. General layout of how we swap buffers goes like:

  1. Render stuff
  2. Frame A converted to planar
  3. Vsync happens
  4. Palette is set if dirty
  5. Frame A displayed
  6. Render stuff
  7. Frame B is converted to planar

The behaviour is controlled through a range of control functions in the graphics driver.

#Resolution and 4:3 vs 16:9 Having the resolution locked to 320x256 (physical) and 320x180 (effective) would have been fine if I had understood that $DIWSTRT and $DIWSTOP can’t really be changed dynamically. Well, they can actually but the output never looked good as I wanted perfect vertical centering of the screen. I discovered this fairly late during development and we had no time to change the graphics at that point. That’s why we do 16:9 on our own and don’t use the DIWSTRT/DIWSTOP big-screen “trick”.

C2P is reinitialized automatically by the demo-system for effects requiring the full 320x256. This happens during runtime. I have no idea if it is recommended or not. Looking at the C2P code it seemed fine to me and it worked out ok.

#Vertical blank The VBI handling is fully done in C and at the application level. There is a small proxy handler in Assembler (doing nothing unless the callback is set).

void initfunc() {
    // tell driver to set the callback
 	goa_gfx_cntl(GOA_GFXCNTL_AMIGA_VBICALLBACK, (void *)IRQCallback);
}
// Actual VBI function from 'Dark Goat Rises'
static volatile void IRQCallback() {
	myFrameCounter++;
	if (slowmotion)
		myTime += 0.1 / 50.0f;	// PAL system is 50hz
	else
		myTime += 1.0 / 50.0f;	// PAL system is 50hz
	if (bPlayMusic) {
		adpcm_player_update();
	}
}

#Calculating Frames Per Second This turned out to be more complicated than I initially thought. Using the VBI for reliable FPS is not the way to do it - resolution is way too low. Instead you need to use the CIA/B high resolution 24bit timer. While that sounded easy enough in theory my limited skills in Amiga System knowledge really showed. I struggled for a couple of evenings to get this working but in the end I simply ripped/stole/borrowed the code from WOS by Haujobb (thanks guys). You can find the original code at github: https://github.com/leifo/haujobb-amiga specifically you are looking for two functions: startHardwareTimer and readTimer. They are implemented in: https://github.com/leifo/haujobb-amiga/blob/master/demo/shared/profile/profile.c Don’t use the readTimer_c for some reason it doesn’t return any data - and I never bothered to check why (could very well be my fault).

// returns time as float seconds
float readHardwareTimerSec() {
	unsigned int i_val = readHardwareTimer();
	// this is from Haujobb
	float val = i_val;
	val = (val * 1000) / 709;
	val = val / (1000000);
	return val;
}

Sadly I added this very late in the development of the demo. Simply wasn’t needed (we lacked a 060 machine) and therefore we didn’t have an understanding of the performance until later. The hardware timer values are however perfectly fine to use. We calculate FPS over a sliding window of 16 frames. In dev-mode the demosystem spits out a file when you exit the demo with details of the framerate over the whole execution time as a CSV with partname, frame start, frame stop and fps. The graph below show the output from the Release Candidate 2 on the compo machine at Revision.

FPS Compo

The spikes are related to switching between 320x256 and 320x180 effective resolution. Main reason is the need to clear (fully) both backbuffers in the driver when going to a lower resolution otherwise the upper/lower part of the screen would be filled with crap (left over) data. Instead of clearing the buffers fully we could simply have cleared the upper and lower 38 scanlines. We actually forgot about it during the deadline stress and realized it first during the compo.


Profile picture

Written by Fredrik Kling. I live and work in Switzerland. Follow me Twitter