As mentioned the v2 firmware was developed completely anew specifically with modern vector applications in mind. So today I wanted to offer a sneak peek under the hood of this new runtime environment.
Any application needs to have access to and deal with several basic things: a sense of time, available user input and a reaction to this input via output mechanisms. For a Vectrex the input would usually be one or two Vectrex controllers but also custom controllers or something special, say, a temperature measurement or real time clock. Equally output can be multiple things – e.g. audio, video and/or LEDs. The most important being video – using a vector display – of course, that’s what it is all about.
The VF firmware now uses an abstraction layer for both inputs and outputs and includes a runtime environment which handles any necessary conversion work and also supplies additional, basic functionality (e.g. debug_printf(), file i/o, sprintf, unzip, crc, options handling, localization etc.). The time itself is defined by the application: it specifies a refresh rate (or uses the default 50Hz) and is called once for every frame with standardized input data and expected to generate output data which in turn is converted to whatever form necessary and ..is then magically output by the abstraction layer.
The application’s frame handler can opt to be tied closely to the actual hardware it is running on to squeeze out the last bit of performance with hand-written code – quite a few of the arcade emulators use this – but by now the majority does not and also does not need to since the standard renderer is so fast that the performance is more than sufficient. These are the ones that quite clearly stepped over the ‘Vectrex cartridge’ line in the sand in my mind and are now obviously ‘applications’. They do not really care about hardware anymore but about higher level problems – what to do for different aspect ratios/orientations, which inputs and outputs are available, whether to handle a specific string a bit differently in French etc. And the actual work they are supposed to do, of course. Here are two examples, first Jeroen Domburg’s 2015 tech demo, ‘Voom’, a Doom lvl renderer using some nifty data handholding and custom z-buffer code to generate an actual 3D environment on the Vectrex.
This demo already has a sense of time, input abstraction, and spews out vectors to use. So all there was initially to do was hooking up the vf inputs:
if (vf_lib_joy_x < 0) keys |= VOOM_KEY_LEFT;
else if (vf_lib_joy_x > 0) keys |= VOOM_KEY_RIGHT;
[...]
and the vectors, just added a manual ‘zref’ for stabilization and handed them over to the vf lib.:
void linesDraw(void) {
int i, j, k, d,dx, dy;
int cd, ci, inv=0;
int x=SIZEX/2,y=SIZEY/2;
// vf_debug("lines %i, objects %i\n", lineIdx, objectIdx);
int object_id = 0;
if (object_start[objectIdx] != lineIdx) {
object_start[++objectIdx] = lineIdx;
}
while (object_id < objectIdx) {
vf_zref(vf_mem);
for (i=object_start[object_id]; i<object_start[object_id+1]; i++) {
//Find closest line
cd=9999; ci=0;
for (j=0; j<lineIdx; j++) {
if (lines[j].p[0].x!=-1) {
for (k=0; k<2; k++) {
dx=abs(lines[j].p[k].x-x);
dy=abs(lines[j].p[k].y-y);
if (dx>dy) d=dx; else d=dy;
if (d<cd) {
cd=d;
ci=j;
inv=k;
}
}
}
}
float x0 = (float)lines[ci].p[inv].x / (float)SIZEX;
float x1 = (float)lines[ci].p[inv^1].x / (float)SIZEX;
float y0 = 1.0-(float)lines[ci].p[inv].y / (float)SIZEY;
float y1 = 1.0-(float)lines[ci].p[inv^1].y / (float)SIZEY;
vf_linef(vf_mem, x0, y0, x1, y1, (voom_low_precision) ? 0x7f : 0x5f, false);
lines[ci].p[0].x=-1; //disable line
x=lines[ci].p[inv^1].x;
y=lines[ci].p[inv^1].y;
}
object_id++;
}
}
As a side note: this will already output in both landscape and portrait mode – but there is no automatic aspect ratio correction nor scaling between the two, since what to do really depends upon the application. Voom once again is supremely easy – just needs a different projection matrix for portrait and landscape, that’s it.
The other application turned out to be more work – but none of it for hardware reasons, mostly polishing and localization since I by now quite like the game. This is “Akalabeth, World of Doom”, the ancestor of all those Ultima games I’ve played (and finished !) back in the day.
Now this is one interesting dungeon crawler to me by now so I went all the way towards making this as nice as possible, also including music and localization (the above screenshot is in German, in case you are wondering). The original game is by none other than Lord British (in teenage form) and was ported to C by Paul Robson. This codebase however does not have the necessary sense of time – like the original it depends upon the existence of a persistent visual output and ‘time’ is just the next thing to do upon entering a command. Might be whenever. For a vector output actually very convenient: add a sense of time, most of the time doing nothing except just waiting for some input. And in those waiting periods display the previous frame again – which results in the best possible performance imaginable. This is also why I chose this example: when you think about it you immediately will realize that very few things are necessary to define a vector image – really just draw from here to there with brightness X – and the performance hinges on not moving the beam around more than necessary. Additionally the stability on analog vector hardware depends on frequent ‘zref’ commands, both of which Akalabeth needs handled (and as usual: on some Vectrex more than others. Remember: .analog. hw). But what happens if the output produced is too much for the hardware trying to display it at the requested refresh rate ?
Akalabeth does drop below 50Hz sometimes, esp. if not using ‘fast’ bitmap texts (btw, the bitmap text option is inherited from the menu), and since it plays music and uses optional caching functionality of the abstraction layer it is a good test case for this. You would see and esp. hear any problems. So..: its frame handler is still called 50 times per second – but if not all supplied output data can be used because the display hardware cannot keep up occassionally frame data must be discarded. The audio data of discarded frames however is merged with newer ones. This is a bit more complex in actuality – e.g. parts of the discarded frame might have been requested to be kept as cached data – but in essence is as straightforward as mentioned. Which is exactly what you want: Akalabeth is also blissfully unaware of any intermittent performance problems, those are also magically handled below, and the game keeps running in (its requested) real-time.