Monday, 30 August 2021

Galaxian Part 4 - Rounding out the framework for a port/transcode

Some of what I'll discuss is more pertinent to C than it is to Javascript. But there's no reason why you couldn't adopt the same architecture in Javascript anyway. In fact, it may still be a good idea for a couple of reasons, including future porting, or even enhancement much like the 'DX' emulations popular for a brief amount of time a ways back.

The main aim is to produce a C transcode of Galaxian core logic that is completely platform-independent; in this case a single C source file with just two entry points - the main function galaxian (), and the interrupt handler galaxian_nmi () - that can be included verbatim on each and every host platform port, whether that be PC, Neo Geo, Amiga etc. Not unlike the MAME driver, for example.

Coupled with this are a set of functions that are implemented in an OS-dependent file that are called from the core source file above. The functions include hardware I/O for reading dipswitch settings, control panel inputs (joystick, buttons) and miscellaneous hardware such as interrupt enable/disable, sound registers etc and of course graphics (display) output. Again, much like the MAME driver.

Here's a breakdown of the source files in my Galaxian project:

  • galaxian.c/h
  • galaxian_gfx.c
  • debug.c/h
  • galaxian_osd.h
  • main.cpp
As discussed, galaxian.c/h implements the platform-independent Galaxian core transcode with galaxian () and galaxian_nmi () entry points. The header contains prototypes for these two functions, plus any data structures or defines that are also required in the main.cpp file.

The galaxian_gfx.c file is the output of the graphics conversion utility discussed in Part 2 and referenced by main.cpp only.

The debug.c/h are simply auxiliary files used during development to implement any debug functions that may assist in the process. These may include functions to dump data structures for example.

The galaxian_osd.h header file contains prototypes for functions implemented in main.cpp. There is only one version of this file and is included on all host platforms.

Finally, main.cpp is where all the platform-dependent code is implemented that actually makes things happen on the host platform. Here the environment is initialised, the framework initialised (create timers and threads, for example), inputs like the PC keyboard mapped to Galaxian hardware I/O addresses, outputs like Galaxian hardware I/O addresses controlling sound mapped to host sound functions, and finally memory mapped areas like video attribute & tile data and sprite registers hooked to host graphics functions. This is the one and only file for which each host platform has its own version!

Since main.cpp is where everything is initialised and where the core is connected to inputs and outputs, most of the functionality is likely to be implemented fairly early-on in the transcode. Host platform initialisation aside, the graphics output is going to be the first functionality that is coded, because we want (need) to see what's happening in the Galaxian code as we transcode. Whilst we can ignore, or at least defer, other I/O like sound outputs and dipswitch or control panel inputs, it's likely we'll at least have to implement some stub code to return default (inactive) input values.

So for the bulk of the time (and effort) in this project, the only file we need to work on is galaxian.c.

For the most part, the core code directly interacts with the host platform hardware when it calls into the OS-dependent (osd) functions. For example when reading from the joystick input port, the osd function in main.cpp will read the state from the PC keyboard, translating back to port bits for the core to manipulate. Similarly when muting sound output for example, the osd function may toggle host sound on/off depending on the state of a control bit written by the core.

For the video though, we may not necessarily want to do this. We may wish to, for example, synchronise host video updates with the host VBLANK as provided in host graphics libraries.

As discussed in Part 3, the first thing Galaxian does in the NMI is update tile attribute (colour & scroll) registers and sprite registers. This avoids flickering and tearing as the beam is still in the vertical retrace at the start of the NMI. It does this by a block copy from a memory buffer into the tile attribute and sprite register memory space (aka object RAM or OBJRAM).

Writing to OBJRAM requires osd functions because ultimately it's accessing hardware, but there are a few options for exactly what is implemented in the osd function, and to some extent depends on the host platform and how the framework is implemented. I've chosen to simply store it in an intermediate buffer which is subsequently used to update the tiles and sprites on the screen elsewhere in the NMI handling of the framework, for maximum flexibility.

Similarly, writing to the tilemap (character RAM aka CRAM) also requires an osd function and again I've chosen to write it to an intermediate buffer and update the host display elsewhere. However I would note that Galaxian does not update the CRAM during VBLANK, so it would be perfectly acceptable to have the osd function update the host display then and there. There's no right or wrong solution here.

So that's pretty much all there is to the framework. Ideally we have a single core Galaxian C implementation and just a single cpp file that must be implemented for each host platform that for the most part handles inputs and graphics. Porting to subsequent hosts should be almost trivial!

Next I'll discuss some of the main aims of the transcode and the general architecture of the galaxian.c implementation. I've actually found my transcodes to be an evolving process and as a result of my most recent Scramble project I'll be tweaking again it for this project.

No comments:

Post a Comment